cambios mcp remoto

This commit is contained in:
Jordan Diaz
2026-04-17 20:03:02 +00:00
parent d41a94b57d
commit 2ac01acd61
15 changed files with 344 additions and 123 deletions

View File

@@ -1,97 +1,90 @@
import { z } from "zod";
import fs from "fs";
import path from "path";
import { sessionCredentials } from "../../auth/credentials.js";
import { sessionCredentials, setMcpSessionCredentials } from "../../auth/credentials.js";
import { withAuthParams } from "../helpers/authSchema.js";
import { fetchProjectInfo } from "../../auth/localClient.js";
import { resolveCurrentProjectDir } from "../files/helpers.js";
import { resolveCurrentAcaiUser } from "../helpers/sessionHelpers.js";
import { getCurrentSessionId } from "../../utils/sessionContext.js";
export function registerAuthTools(server) {
server.tool(
"refresh_acai_token",
`Refresh the Acai JWT token when it has expired (403 "Token no válido" errors). This re-reads the token from the .acai file on disk. If the token on disk is also expired, it calls the Python server to renew it. Use this tool when any other tool fails with a 403 token error.`,
`Refresh the Acai JWT token when it has expired (403 "Token no válido" errors). Delegates to the Python server which detects expiration, renews the token against the webservice if needed, persists the updated .acai file and returns fresh credentials. Use this tool when any other tool fails with a 403 token error.`,
withAuthParams({}),
{ readOnlyHint: false, destructiveHint: false },
async (_args, extra) => {
try {
const projectDir = process.env.ACAI_PROJECT_DIR || "";
const acaiFilePath = projectDir ? path.join(projectDir, ".acai") : "";
if (!acaiFilePath) {
const projectDir = resolveCurrentProjectDir();
if (!projectDir) {
return {
content: [{ type: "text", text: JSON.stringify({ success: false, error: "ACAI_PROJECT_DIR not set" }) }],
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Project dir no disponible en esta sesion" }) }],
isError: true,
};
}
// Step 1: Try reading fresh token from .acai (Python server may have already refreshed it)
let token = "";
let tokenHash = "";
let domain = "";
const acaiUser = resolveCurrentAcaiUser();
// Delegamos al Python que ya gestiona expiracion + refresh + persistencia
let info;
try {
const data = JSON.parse(fs.readFileSync(acaiFilePath, "utf-8"));
token = data.token || "";
tokenHash = data.tokenHash || "";
domain = data.domain || "";
info = await fetchProjectInfo({ project_dir: projectDir }, acaiUser);
} catch (e) {
return {
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Cannot read .acai: ${e.message}` }) }],
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token refresh failed: ${e.message}` }) }],
isError: true,
};
}
// Step 2: Check if token is expired by decoding JWT
let isExpired = false;
try {
const payload = token.split(".")[1];
const decoded = JSON.parse(Buffer.from(payload, "base64").toString());
isExpired = Date.now() / 1000 > (decoded.exp || 0) - 300;
} catch {
isExpired = true;
if (!info?.success) {
return {
content: [{ type: "text", text: JSON.stringify({ success: false, error: info?.error || "Project info resolution failed" }) }],
isError: true,
};
}
// Step 3: If expired, ask Python server to refresh it
if (isExpired) {
try {
const info = await fetchProjectInfo({ project_dir: projectDir });
token = info?.token || token;
tokenHash = info?.tokenHash || tokenHash;
domain = info?.domain || domain;
} catch (e) {
return {
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token refresh failed: ${e.message}` }) }],
isError: true,
};
}
// Comparamos token previo para saber si hubo renovacion
const mcpSessionId = getCurrentSessionId();
let previousToken = null;
if (mcpSessionId) {
// Leer creds previas sin tocar lastAccess via interno no expuesto:
// usamos sessionCredentials como espejo si existe, sino null.
const prev = sessionCredentials.get(mcpSessionId);
previousToken = prev?.token || null;
}
// Step 4: Update credentials in memory from the canonical server resolver
const info = await fetchProjectInfo({ project_dir: projectDir });
const webUrl = info?.web_url || process.env.ACAI_WEB_URL || "";
const apiWebUrl = info?.api_web_url || process.env.ACAI_API_WEB_URL || webUrl;
const website = info?.domain || domain || process.env.ACAI_WEBSITE || "";
const freshCreds = {
token,
tokenHash,
website,
web_url: webUrl,
api_web_url: apiWebUrl,
forge_host: info?.forge_host || null,
profileName: "stdio",
token: info.token || "",
tokenHash: info.tokenHash || "",
website: info.domain || "",
web_url: info.web_url || "",
api_web_url: info.api_web_url || info.web_url || "",
forge_host: info.forge_host || null,
project_dir: info.project_dir || projectDir,
acai_user: acaiUser || null,
profileName: acaiUser || "mcp-session",
role: "developer",
};
sessionCredentials.set("_default", freshCreds);
// Persistir en la sesion MCP activa (HTTP multi-tenant)
if (mcpSessionId) {
setMcpSessionCredentials(mcpSessionId, freshCreds);
sessionCredentials.set(mcpSessionId, freshCreds);
}
// Compatibilidad stdio (cuando extra.sessionId viene del SDK)
if (extra?.sessionId) {
sessionCredentials.set(extra.sessionId, freshCreds);
}
const rotated = previousToken && previousToken !== freshCreds.token;
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
message: "Token refreshed successfully",
expired_before: isExpired,
domain: website,
message: rotated ? "Token refreshed (rotated by Python)" : "Token refreshed successfully",
domain: freshCreds.website,
rotated: !!rotated,
}, null, 2),
}],
};