import axios from "axios"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { sessionApiClients, getSessionCredentials, setCredentials, findRoleByToken } from "./credentials.js"; import { assertSafeCmsTarget } from "../utils/cmsTargetSafety.js"; const DEFAULT_ROLE = 'developer'; /** * Check if session is configured with valid credentials */ export const ensureConfigured = async (sessionId) => { const creds = await getSessionCredentials(sessionId); if (!creds.token || !creds.web_url || !creds.api_web_url) { throw new McpError( ErrorCode.InvalidRequest, "Acai site not configured for safe local execution. Use the project MCP config/select_project flow so ACAI_API_WEB_URL points to the local environment." ); } try { assertSafeCmsTarget(creds, "apiClient"); } catch (error) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } }; /** * Rebuild API client for a session */ export const rebuildApiClient = async (sessionId) => { const creds = await getSessionCredentials(sessionId); if (!creds.token || !creds.web_url || !creds.api_web_url) { return null; } assertSafeCmsTarget(creds, "apiClient"); const client = axios.create({ baseURL: creds.api_web_url, headers: { "Content-Type": "application/json", "X-Acai-Token": creds.token, ...(creds.forge_host ? { Host: creds.forge_host } : {}), }, }); // Request interceptor: always send latest token client.interceptors.request.use((config) => { if (creds.token) { config.headers["X-Acai-Token"] = creds.token; } return config; }); sessionApiClients.set(sessionId, client); return client; }; /** * Get or create API client for a session * @param {string} sessionId - The session ID */ export const getApiClient = async (sessionId) => { const creds = await getSessionCredentials(sessionId); console.error(`[API Client] getApiClient called for session ${sessionId}`); console.error(`[API Client] Current creds: token=${!!creds.token}, web_url=${creds.web_url}`); await ensureConfigured(sessionId); let client = sessionApiClients.get(sessionId); if (!client) { console.error(`[API Client] No cached client, rebuilding...`); client = await rebuildApiClient(sessionId); } if (!client) { throw new McpError( ErrorCode.InvalidRequest, "Unable to create API client. Verify credentials and try again." ); } console.error(`[API Client] Returning client for session ${sessionId}`); return client; }; /** * Set credentials and rebuild API client * @param {Object} credentials - The credentials object * @param {string} sessionId - The session ID * @param {string} mcpSessionId - Optional MCP-Session-Id for persistence across SSE reconnections */ export const setCredentialsAndRebuild = async (credentials, sessionId, mcpSessionId = null) => { await setCredentials(credentials, sessionId, mcpSessionId); await rebuildApiClient(sessionId); }; /** * Wrapper for authenticated handlers * Supports both session-based auth and inline credentials (stateless mode). * * If args contains acaiToken + acaiWebsite, these are used directly, * allowing Claude to send credentials with each request. */ export const withAuth = (handler) => { return async (args, extra) => { const sessionId = extra?.sessionId || "_default"; console.error(`[withAuth] Called with sessionId: ${sessionId}`); // Check for inline credentials (stateless mode) const inlineCredentials = { acaiToken: args.acaiToken, acaiWebsite: args.acaiWebsite, acaiTokenHash: args.acaiTokenHash }; const hasInlineCredentials = inlineCredentials.acaiToken && inlineCredentials.acaiWebsite; if (hasInlineCredentials) { // Lookup role by token before storing credentials const role = findRoleByToken(inlineCredentials.acaiToken) || DEFAULT_ROLE; console.error(`[withAuth] Using INLINE credentials: website=${inlineCredentials.acaiWebsite}, role=${role}`); // Temporarily store inline credentials in session for this request await setCredentials({ token: inlineCredentials.acaiToken, website: inlineCredentials.acaiWebsite, web_url: `https://${inlineCredentials.acaiWebsite}`, api_web_url: null, forge_host: null, tokenHash: inlineCredentials.acaiTokenHash || null, profileName: 'inline', role: role }, sessionId); } console.error(`[withAuth] Getting API client for session ${sessionId}...`); await getApiClient(sessionId); console.error(`[withAuth] API client ready, calling handler...`); return handler(args, { ...extra, sessionId, inlineCredentials: hasInlineCredentials ? inlineCredentials : null }); }; };