fix MCP auth token refresh and disable legacy SSE

This commit is contained in:
Jordan Diaz
2026-07-18 10:03:12 +00:00
parent 9c3d9fb999
commit 987e050539
12 changed files with 437 additions and 128 deletions

View File

@@ -2,8 +2,30 @@ 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";
import { ensureFreshSessionCredentials, refreshSessionCredentials } from "./sessionRefresh.js";
const DEFAULT_ROLE = 'developer';
export const isAcaiTokenFailure = (error) => {
const status = error?.response?.status;
const responseText = JSON.stringify(error?.response?.data || "");
return status === 401
|| (status === 403 && /token|jwt|auth|unauthoriz|expired|no v[aá]lido/i.test(responseText));
};
export const runWithTokenRefreshRetry = async (
sessionId,
operation,
{ refreshCredentials = refreshSessionCredentials } = {},
) => {
try {
return await operation();
} catch (error) {
if (!isAcaiTokenFailure(error) || error?.config?._acaiTokenRetry) throw error;
await refreshCredentials(sessionId, { force: true });
return operation();
}
};
/**
* Check if session is configured with valid credentials
*/
@@ -29,7 +51,7 @@ export const ensureConfigured = async (sessionId) => {
/**
* Rebuild API client for a session
*/
export const rebuildApiClient = async (sessionId) => {
export const rebuildApiClient = async (sessionId, { refreshCredentials = refreshSessionCredentials } = {}) => {
const creds = await getSessionCredentials(sessionId);
if (!creds.token || !creds.web_url || !creds.api_web_url) {
return null;
@@ -44,14 +66,29 @@ export const rebuildApiClient = async (sessionId) => {
},
});
// Request interceptor: always send latest token
client.interceptors.request.use((config) => {
if (creds.token) {
config.headers["X-Acai-Token"] = creds.token;
// Always resolve the latest session token. The client outlives individual
// JWTs, so capturing `creds` here would keep sending a rotated token.
client.interceptors.request.use(async (config) => {
const latest = await getSessionCredentials(sessionId);
if (latest.token) {
config.headers["X-Acai-Token"] = latest.token;
}
return config;
});
client.interceptors.response.use(undefined, async (error) => {
const original = error.config;
if (!isAcaiTokenFailure(error) || !original || original._acaiTokenRetry) {
throw error;
}
original._acaiTokenRetry = true;
const fresh = await refreshCredentials(sessionId, { force: true });
original.headers = original.headers || {};
original.headers["X-Acai-Token"] = fresh.token;
return client.request(original);
});
sessionApiClients.set(sessionId, client);
return client;
};
@@ -133,10 +170,19 @@ export const withAuth = (handler) => {
}, sessionId);
}
await ensureFreshSessionCredentials(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 });
const handlerExtra = {
...extra,
sessionId,
inlineCredentials: hasInlineCredentials ? inlineCredentials : null,
};
return runWithTokenRefreshRetry(
sessionId,
() => handler(args, handlerExtra),
);
};
};