fix MCP auth token refresh and disable legacy SSE
This commit is contained in:
@@ -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`);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user