143 lines
5.1 KiB
JavaScript
143 lines
5.1 KiB
JavaScript
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 });
|
|
};
|
|
};
|