/** * MCP Token Validation * * Valida tokens X-MCP-Secret contra Redis. * El backend Python escribe la clave `mcp_tokens:` con metadata JSON: * { * "id": "...", * "user": "superadmin", * "project": "vegaasesores.com", * "label": "MacBook Pro", * "createdAt": 1234567890, * "lastUsedAt": null | 1234567900 * } * * El plaintext es "acai_<43 chars>" y se hashea con sha256 hex en minusculas. */ import Redis from "ioredis"; import crypto from "node:crypto"; const REDIS_URL = process.env.REDIS_URL || "redis://redis:6379"; // Cliente Redis compartido (lazy init para no conectar si el MCP corre en modo stdio // u otros escenarios donde el middleware HTTP nunca se invoque). let redisClient = null; function getRedis() { if (redisClient) return redisClient; try { redisClient = new Redis(REDIS_URL, { lazyConnect: false, maxRetriesPerRequest: 3, enableReadyCheck: false, }); redisClient.on("error", (err) => { console.error("[mcp-tokens] redis error:", err.message); }); } catch (e) { console.error("[mcp-tokens] no se pudo inicializar redis:", e.message); redisClient = null; } return redisClient; } /** * Hashea un string con SHA256 y devuelve hex en minusculas. */ export function sha256Hex(str) { return crypto.createHash("sha256").update(str, "utf8").digest("hex"); } /** * Valida un X-MCP-Secret plaintext contra Redis. * @param {string} secret - plaintext tipo "acai_xxx" * @returns {Promise<{user: string, project: string, id: string} | null>} */ export async function validateMcpToken(secret) { if (!secret || typeof secret !== "string") return null; const r = getRedis(); if (!r) return null; const sha = sha256Hex(secret); const key = `mcp_tokens:${sha}`; let raw; try { raw = await r.get(key); } catch (err) { console.error("[mcp-tokens] redis GET error:", err.message); return null; } if (!raw) return null; let meta; try { meta = JSON.parse(raw); } catch { return null; } if (!meta || !meta.user || !meta.project) return null; // Actualizacion asincrona de lastUsedAt — no bloqueamos la request. updateLastUsedAt(key, meta).catch((e) => { console.error("[mcp-tokens] lastUsedAt update failed:", e.message); }); return { user: meta.user, project: meta.project, id: meta.id || "" }; } async function updateLastUsedAt(key, meta) { const r = getRedis(); if (!r) return; const next = { ...meta, lastUsedAt: Math.floor(Date.now() / 1000) }; await r.set(key, JSON.stringify(next)); }