cambios mcp remoto
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "./auth/index.js";
|
||||
import { fetchProjectInfo } from "./auth/localClient.js";
|
||||
import { createSessionServer } from "./server.js";
|
||||
import { runWithSession } from "./utils/sessionContext.js";
|
||||
|
||||
// Active sessions - stores { transport, server, type, heartbeatInterval }
|
||||
const activeSessions = new Map();
|
||||
@@ -72,9 +73,9 @@ const verifyJwt = (token) => {
|
||||
}
|
||||
};
|
||||
|
||||
const resolveProjectCredentials = async (projectName) => {
|
||||
const resolveProjectCredentials = async (projectName, acaiUser = null) => {
|
||||
try {
|
||||
const info = await fetchProjectInfo(projectName);
|
||||
const info = await fetchProjectInfo(projectName, acaiUser);
|
||||
if (!info.success) {
|
||||
throw new Error(info.error || "Failed to resolve project info");
|
||||
}
|
||||
@@ -86,37 +87,44 @@ const resolveProjectCredentials = async (projectName) => {
|
||||
api_web_url: info.api_web_url || info.web_url,
|
||||
forge_host: info.forge_host || null,
|
||||
mode: info.mode || "local",
|
||||
project_dir: info.project_dir || null,
|
||||
acai_user: acaiUser || info.acai_user || null,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to resolve project '${projectName}': ${error.message}`);
|
||||
// Si el Python devuelve 404, propagamos el estado original para que el
|
||||
// cliente MCP reciba el fallo real (no lo enmascaramos como 500).
|
||||
if (error.response?.status === 404) {
|
||||
const details = error.response.data?.error || error.response.data?.message || "project not found";
|
||||
const err = new Error(`Project '${typeof projectName === "string" ? projectName : (projectName?.project || projectName?.project_dir || "?")}' not found: ${details}`);
|
||||
err.status = 404;
|
||||
throw err;
|
||||
}
|
||||
throw new Error(`Failed to resolve project '${typeof projectName === "string" ? projectName : (projectName?.project || projectName?.project_dir || "?")}': ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure credentials from request headers/query params for a session
|
||||
*/
|
||||
const configureSessionCredentials = async (sessionId, { token, tokenHash, website, web_url, userToken, projectName }) => {
|
||||
const configureSessionCredentials = async (sessionId, { token, tokenHash, website, web_url, userToken, projectName, acaiUser }) => {
|
||||
// Priority 1: Resolve via project name from local Python server
|
||||
if (projectName) {
|
||||
try {
|
||||
const projectCreds = await resolveProjectCredentials(projectName);
|
||||
sessionCredentials.set(sessionId, {
|
||||
token: projectCreds.token,
|
||||
tokenHash: projectCreds.tokenHash || null,
|
||||
website: projectCreds.website,
|
||||
web_url: projectCreds.web_url,
|
||||
api_web_url: projectCreds.api_web_url || projectCreds.web_url,
|
||||
forge_host: projectCreds.forge_host || null,
|
||||
mode: projectCreds.mode || "local",
|
||||
profileName: 'project-' + projectName,
|
||||
role: 'developer',
|
||||
});
|
||||
console.log(`[MCP] Session ${sessionId} authenticated via project '${projectName}' - web_url: ${projectCreds.web_url}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[MCP] Failed to resolve project '${projectName}': ${error.message}`);
|
||||
// Fall through to try other auth methods
|
||||
}
|
||||
const projectCreds = await resolveProjectCredentials(projectName, acaiUser);
|
||||
sessionCredentials.set(sessionId, {
|
||||
token: projectCreds.token,
|
||||
tokenHash: projectCreds.tokenHash || null,
|
||||
website: projectCreds.website,
|
||||
web_url: projectCreds.web_url,
|
||||
api_web_url: projectCreds.api_web_url || projectCreds.web_url,
|
||||
forge_host: projectCreds.forge_host || null,
|
||||
mode: projectCreds.mode || "local",
|
||||
project_dir: projectCreds.project_dir || null,
|
||||
acai_user: acaiUser || projectCreds.acai_user || null,
|
||||
profileName: 'project-' + projectName,
|
||||
role: 'developer',
|
||||
});
|
||||
console.log(`[MCP] Session ${sessionId} authenticated via project '${projectName}' (user=${acaiUser || "-"}) - web_url: ${projectCreds.web_url}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Priority 2: Direct credentials (legacy header-based)
|
||||
@@ -151,7 +159,10 @@ const extractCredentialsFromRequest = (req) => {
|
||||
token: url.searchParams.get('token') || req.headers['x-acai-token'],
|
||||
tokenHash: url.searchParams.get('tokenHash') || req.headers['x-acai-token-hash'],
|
||||
website: url.searchParams.get('website') || req.headers['x-acai-website'],
|
||||
userToken: url.searchParams.get('userToken') || req.headers['x-user-token']
|
||||
userToken: url.searchParams.get('userToken') || req.headers['x-user-token'],
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -169,7 +180,7 @@ export function startHttpServer() {
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'X-Acai-Token', 'X-Acai-Website', 'X-Acai-Token-Hash', 'X-User-Token', 'X-Project-Name', 'Authorization', 'Mcp-Session-Id'],
|
||||
allowedHeaders: ['Content-Type', 'X-Acai-Token', 'X-Acai-Website', 'X-Acai-Token-Hash', 'X-User-Token', 'X-Project-Name', 'X-Acai-User', 'Authorization', 'Mcp-Session-Id'],
|
||||
exposedHeaders: ['Mcp-Session-Id'],
|
||||
credentials: true
|
||||
}));
|
||||
@@ -209,6 +220,29 @@ export function startHttpServer() {
|
||||
// Extract credentials from request
|
||||
const credentials = extractCredentialsFromRequest(req);
|
||||
|
||||
// Si la request trae projectName, resolvemos credenciales ANTES de
|
||||
// crear el transport. Si falla (p.ej. proyecto no existe o no
|
||||
// pertenece al usuario), propagamos el error al cliente MCP en
|
||||
// lugar de enmascararlo como initialize OK.
|
||||
let resolvedCreds = null;
|
||||
if (credentials.projectName) {
|
||||
try {
|
||||
resolvedCreds = await resolveProjectCredentials(credentials.projectName, credentials.acaiUser);
|
||||
} catch (err) {
|
||||
const status = err.status === 404 ? 404 : 400;
|
||||
console.error(`[MCP Streamable] Credential resolution failed (user=${credentials.acaiUser || "-"}, project=${credentials.projectName}): ${err.message}`);
|
||||
res.status(status).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: status === 404 ? -32004 : -32000,
|
||||
message: err.message,
|
||||
},
|
||||
id: req.body?.id ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => crypto.randomUUID(),
|
||||
onsessioninitialized: (sessionId) => {
|
||||
@@ -222,18 +256,36 @@ export function startHttpServer() {
|
||||
startTime: Date.now()
|
||||
});
|
||||
|
||||
// Configure credentials for this session (async, fire-and-forget)
|
||||
configureSessionCredentials(sessionId, credentials).then((configured) => {
|
||||
if (configured) {
|
||||
// Also store credentials by MCP-Session-Id for persistence
|
||||
const creds = sessionCredentials.get(sessionId);
|
||||
if (creds) {
|
||||
setMcpSessionCredentials(sessionId, creds);
|
||||
// Guardar credenciales ya resueltas, o caer a otros metodos
|
||||
// (token directo, userToken) de forma sincrona.
|
||||
if (resolvedCreds) {
|
||||
const creds = {
|
||||
token: resolvedCreds.token,
|
||||
tokenHash: resolvedCreds.tokenHash || null,
|
||||
website: resolvedCreds.website,
|
||||
web_url: resolvedCreds.web_url,
|
||||
api_web_url: resolvedCreds.api_web_url || resolvedCreds.web_url,
|
||||
forge_host: resolvedCreds.forge_host || null,
|
||||
mode: resolvedCreds.mode || "local",
|
||||
project_dir: resolvedCreds.project_dir || null,
|
||||
acai_user: credentials.acaiUser || resolvedCreds.acai_user || null,
|
||||
profileName: 'project-' + credentials.projectName,
|
||||
role: 'developer',
|
||||
};
|
||||
sessionCredentials.set(sessionId, creds);
|
||||
setMcpSessionCredentials(sessionId, creds);
|
||||
console.log(`[MCP] Session ${sessionId} authenticated via project '${credentials.projectName}' (user=${credentials.acaiUser || "-"}) - web_url: ${creds.web_url}`);
|
||||
} else {
|
||||
// Sin projectName -> usar fallback de credenciales directas
|
||||
configureSessionCredentials(sessionId, credentials).then((configured) => {
|
||||
if (configured) {
|
||||
const creds = sessionCredentials.get(sessionId);
|
||||
if (creds) setMcpSessionCredentials(sessionId, creds);
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(`[MCP Streamable] Error configuring credentials for session ${sessionId}:`, err.message);
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error(`[MCP Streamable] Error configuring credentials for session ${sessionId}:`, err.message);
|
||||
});
|
||||
}
|
||||
},
|
||||
onsessionclosed: (sessionId) => {
|
||||
console.log(`[MCP Streamable] Session closed: ${sessionId.substring(0, 8)}...`);
|
||||
@@ -297,8 +349,21 @@ export function startHttpServer() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request with the transport
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
// Resolver el mcpSessionId definitivo para propagar via AsyncLocalStorage.
|
||||
// En initialize hay que esperar a que el transport asigne el sessionId
|
||||
// (lo hace sincronicamente en handleRequest via onsessioninitialized).
|
||||
const resolvedSessionId = mcpSessionId || transport.sessionId || null;
|
||||
|
||||
if (resolvedSessionId) {
|
||||
await runWithSession(resolvedSessionId, async () => {
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
} else {
|
||||
// En la request de initialize todavia no existe sessionId — el
|
||||
// transport lo genera internamente. Las tools no se llaman en
|
||||
// initialize, por lo que podemos invocar sin contexto.
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[MCP Streamable] Error:', error);
|
||||
if (!res.headersSent) {
|
||||
@@ -351,8 +416,20 @@ export function startHttpServer() {
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Configure credentials
|
||||
await configureSessionCredentials(sessionId, credentials);
|
||||
// Configure credentials (si falla devolvemos 4xx antes de abrir el SSE)
|
||||
try {
|
||||
await configureSessionCredentials(sessionId, credentials);
|
||||
} catch (err) {
|
||||
const status = err.status === 404 ? 404 : 400;
|
||||
console.error(`[MCP SSE] Credential resolution failed: ${err.message}`);
|
||||
clearInterval(heartbeatInterval);
|
||||
if (!res.headersSent) {
|
||||
res.status(status).json({ error: err.message });
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Store session
|
||||
activeSessions.set(sessionId, {
|
||||
@@ -421,7 +498,9 @@ export function startHttpServer() {
|
||||
}
|
||||
|
||||
try {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
await runWithSession(sessionId, async () => {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[MCP SSE] POST error for session ${sessionId}:`, error.message);
|
||||
if (!res.headersSent) {
|
||||
|
||||
Reference in New Issue
Block a user