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

@@ -18,7 +18,7 @@ import {
getMcpSessionCredentials
} from "./auth/index.js";
import { fetchProjectInfo } from "./auth/localClient.js";
import { validateMcpToken } from "./auth/mcpTokens.js";
import { createMcpAuthMiddleware } from "./auth/mcpAuthMiddleware.js";
import { createSessionServer } from "./server.js";
import { runWithSession } from "./utils/sessionContext.js";
@@ -112,7 +112,7 @@ const resolveProjectCredentials = async (projectName, acaiUser = null) => {
/**
* Configure credentials from request headers/query params for a session
*/
const configureSessionCredentials = async (sessionId, { token, tokenHash, website, web_url, userToken, projectName, acaiUser }) => {
const configureSessionCredentials = async (sessionId, { token, tokenHash, website, web_url, userToken, projectName, acaiUser, mcpSecret }) => {
// Priority 1: Resolve via project name from local Python server
if (projectName) {
const projectCreds = await resolveProjectCredentials(projectName, acaiUser);
@@ -126,6 +126,7 @@ const configureSessionCredentials = async (sessionId, { token, tokenHash, websit
mode: projectCreds.mode || "local",
project_dir: projectCreds.project_dir || null,
acai_user: acaiUser || projectCreds.acai_user || null,
mcp_secret: mcpSecret || null,
profileName: 'project-' + projectName,
role: 'developer',
});
@@ -169,6 +170,7 @@ const extractCredentialsFromRequest = (req) => {
// Header inyectado por nginx tras validar el secret contra su mapa.
// Se usa para aislar la resolucion del proyecto a /opt/acai/webs/<user>/.
acaiUser: req.headers['x-acai-user'] || null,
mcpSecret: req.headers['x-mcp-secret'] || null,
};
};
@@ -191,58 +193,9 @@ export function startHttpServer() {
credentials: true
}));
//=============================================================================
// MCP SECRET MIDDLEWARE
// Si llega X-MCP-Secret, lo validamos contra Redis (mcp_tokens:<sha256>) y
// reemplazamos los headers de identidad con los del token. El cliente NO
// puede forzar X-Acai-User / X-Project-Name si esta usando X-MCP-Secret.
// Si NO llega X-MCP-Secret, pasa de largo (modo legacy/dev: el cliente se
// identifica manualmente con X-Acai-User + X-Project-Name).
//=============================================================================
app.use(async (req, res, next) => {
// DEBUG temporal: loguear TODA request que llegue. Quitar cuando este
// claro el flujo del cliente.
const secretPresent = !!req.headers["x-mcp-secret"];
const authPresent = !!req.headers["authorization"];
console.error(
`[MCP req] ${req.method} ${req.url} - X-MCP-Secret=${secretPresent ? "yes" : "MISSING"}, Authorization=${authPresent ? "yes" : "MISSING"}, UA=${(req.headers["user-agent"] || "").substring(0, 60)}`,
);
const secret = req.headers["x-mcp-secret"];
if (!secret) {
return next();
}
try {
const auth = await validateMcpToken(secret);
if (!auth) {
console.error("[MCP middleware] Invalid X-MCP-Secret rejected");
res.status(401)
.setHeader("Content-Type", "application/json")
.end(JSON.stringify({ error: "Invalid MCP token" }));
return;
}
// Sobrescribe los headers de identidad con los del token validado.
req.headers["x-acai-user"] = auth.user;
// `auth.project` solo se sobrescribe si el token es project-scoped.
// Si es user-wide (auth.project === ""), preservamos el
// `X-Project-Name` que el cliente envio (la extension VS Code
// Acai Forge lo manda con el slug del proyecto descargado).
if (auth.project) {
req.headers["x-project-name"] = auth.project;
}
console.error(
`[MCP middleware] Auth OK user=${auth.user} ` +
`tokenScope=${auth.project || "user-wide"} ` +
`clientProject=${req.headers["x-project-name"] || "(none)"}`,
);
return next();
} catch (err) {
console.error("[MCP] mcpSecretMiddleware error:", err.message);
res.status(401)
.setHeader("Content-Type", "application/json")
.end(JSON.stringify({ error: "Invalid MCP token" }));
return;
}
});
// Streamable HTTP always authenticates with a Redis-backed X-MCP-Secret.
// Legacy SSE remains on its existing auth path for now.
app.use(createMcpAuthMiddleware());
//=============================================================================
// STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-03-26)
@@ -328,6 +281,7 @@ export function startHttpServer() {
mode: resolvedCreds.mode || "local",
project_dir: resolvedCreds.project_dir || null,
acai_user: credentials.acaiUser || resolvedCreds.acai_user || null,
mcp_secret: credentials.mcpSecret || null,
profileName: 'project-' + credentials.projectName,
role: 'developer',
};
@@ -439,11 +393,18 @@ export function startHttpServer() {
});
//=============================================================================
// DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05)
// Kept for backwards compatibility with older clients
// DISABLED LEGACY HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05)
// Retained below only as dead source-compatible code
//=============================================================================
// SSE connection endpoint (GET /sse)
// Legacy SSE transport is intentionally disabled. The supported public
// transport is Streamable HTTP at /mcp; the frontend chat uses /api/events.
app.all(['/sse', '/message'], (req, res) => {
res.status(404).json({ error: "Legacy SSE transport disabled; use /mcp" });
});
// SSE implementation retained below only as source-compatible dead code;
// the route above terminates all public SSE/message requests first.
app.get('/sse', async (req, res) => {
console.log(`[MCP SSE] New SSE connection`);
@@ -570,8 +531,7 @@ export function startHttpServer() {
// Root path normalization (for clients that call "/" instead of /sse or /mcp)
app.get('/', (req, res) => {
// Redirect to SSE for backwards compatibility
res.redirect('/sse');
res.status(404).json({ error: "MCP endpoint is /mcp" });
});
app.post('/', async (req, res) => {
@@ -924,7 +884,7 @@ export function startHttpServer() {
const server = app.listen(MCP_PORT, '0.0.0.0', () => {
console.error(`[MCP] Server listening on http://0.0.0.0:${MCP_PORT}`);
console.error(`[MCP] Streamable HTTP endpoint: /mcp (recommended)`);
console.error(`[MCP] Legacy SSE endpoint: /sse (backwards compatible)`);
console.error(`[MCP] Legacy SSE endpoint disabled`);
console.error(`[MCP] Provide credentials via headers: X-Acai-Token, X-Acai-Website, X-Acai-Token-Hash`);
});