// 🚨 Nuevos logs en puntos clave para saber qué se pide y cuándo import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; import { z } from "zod"; // ------------------------------- // 🔧 VARIABLES Y HELPERS GLOBALES // ------------------------------- const API_BASE_URL = process.env.PLANILLA_API_URL || "http://localhost:4000"; const log = (...args) => console.log("[MCP]", ...args); const bootLog = (...args) => console.log("[BOOT]", ...args); // Imprime detalles del runtime apenas carga el módulo (function printBootInfo () { bootLog("Iniciando Planilla MCP v0.1.0"); bootLog(`Node : ${process.version} (${process.platform}-${process.arch})`); bootLog(`Working directory : ${process.cwd()}`); bootLog(`PLANILLA_API_URL : ${API_BASE_URL}`); bootLog(`PORT (HTTP) : ${process.env.PORT ?? "5000 (default)"}`); bootLog(`Args : ${process.argv.slice(2).join(" ") || ""}`); const rss = (process.memoryUsage().rss / 1024 ** 2).toFixed(1); bootLog(`Boot RSS Memory : ${rss} MB`); })(); // -------------------------------- // ⛑️ FETCH HELPER CON LOGGING // -------------------------------- async function fetchJSON (path, options = {}) { const method = options.method || "GET"; console.log(`[API] ${method} ${API_BASE_URL}${path}`); const res = await fetch(`${API_BASE_URL}${path}`, { headers: { "Content-Type": "application/json" }, ...options, }); if (!res.ok) { const txt = await res.text(); throw new Error(`API request failed (${res.status}): ${txt}`); } return res.json(); } // ------------------------------- // 🏗️ CREACIÓN DEL MCP SERVER // ------------------------------- function createServer () { const server = new McpServer({ name: "planilla-mcp", version: "0.1.0" }); log("Servidor MCP instanciado"); // ----- Resources ----- server.resource("planilla-list", "planilla://list", async (uri) => { log("Recurso solicitado", "planilla-list"); const planillas = await fetchJSON("/api/planillas"); return { contents: [{ uri: uri.href, text: JSON.stringify(planillas) }] }; }); server.resource( "planilla", new ResourceTemplate("planilla://{id}", { list: undefined }), async (uri, { id }) => { log("Recurso solicitado", `planilla ${id}`); const planilla = await fetchJSON(`/api/planillas/${id}`); return { contents: [{ uri: uri.href, text: JSON.stringify(planilla) }] }; } ); // ----- Tools ----- server.tool( "create-planilla", "Crea una planilla", { empleado_id: z.number(), fecha_desde: z.string(), fecha_hasta: z.string(), titulo: z.string(), total: z.number().optional(), estado: z.string().optional(), fecha_anulado:z.string().optional(), creador_id: z.number().optional(), anulador_id: z.number().optional(), }, async (params) => { log("Tool invocada", "create-planilla", params); const planilla = await fetchJSON("/api/planillas", { method: "POST", body: JSON.stringify(params), }); return { content: [{ type: "text", text: JSON.stringify(planilla) }] }; } ); server.tool( "update-planilla", "Actualiza una planilla existente", { id: z.number(), empleado_id: z.number().optional(), fecha_desde: z.string().optional(), fecha_hasta: z.string().optional(), titulo: z.string().optional(), total: z.number().optional(), estado: z.string().optional(), fecha_anulado:z.string().optional(), anulador_id: z.number().optional(), }, async ({ id, ...updates }) => { log("Tool invocada", "update-planilla", { id, ...updates }); const planilla = await fetchJSON(`/api/planillas/${id}`, { method: "PUT", body: JSON.stringify(updates), }); return { content: [{ type: "text", text: JSON.stringify(planilla) }] }; } ); server.tool( "delete-planilla", "Elimina una planilla", { id: z.number() }, async ({ id }) => { log("Tool invocada", "delete-planilla", { id }); await fetchJSON(`/api/planillas/${id}`, { method: "DELETE" }); return { content: [{ type: "text", text: `Planilla ${id} eliminada` }] }; } ); server.tool( "search-planillas", "Busca planillas. `q` es un texto libre que matchea id, empleado_id, título o estado. Si no mandás ningún argumento devuelve los primeros 100 registros.", { q: z.string().optional(), empleado_id: z.number().optional(), estado: z.string().optional(), titulo: z.string().optional(), fecha_desde_desde: z.string().optional(), fecha_desde_hasta: z.string().optional(), fecha_hasta_desde: z.string().optional(), fecha_hasta_hasta: z.string().optional(), }, async (params) => { log("Tool invocada", "search-planillas", params); const qs = new URLSearchParams( Object.entries(params).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)]) ); // 👇 Si no hay filtros, limit=100 if (qs.toString() === "") qs.append("limit", "100"); const planillas = await fetchJSON(`/api/planillas/search?${qs.toString()}`); return { content: [{ type: "text", text: JSON.stringify(planillas) }] }; } ); return server; } // -------------------------------- // 🚀 PUNTO DE ENTRADA // -------------------------------- async function main () { const useStdio = process.argv.includes("--stdio"); if (useStdio) { bootLog("Modo transporte: stdio"); const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.log("MCP Planilla server listo (stdio)"); } else { bootLog("Modo transporte: HTTP streamable"); const app = express(); app.use(express.json()); // 🌐 Log de cada request HTTP entrante app.use((req, _res, next) => { console.log(`[HTTP] ${req.method} ${req.originalUrl}`); next(); }); const port = process.env.PORT || 5000; app.post("/mcp", async (req, res) => { try { const server = createServer(); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on("close", () => { transport.close(); server.close(); }); await server.connect(transport); await transport.handleRequest(req, res, req.body); } catch (error) { console.error("Error handling MCP request:", error); if (!res.headersSent) { res.status(500).json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null, }); } } }); ["get", "delete"].forEach((m) => app[m]("/mcp", (_req, res) => res.status(405).json({ jsonrpc: "2.0", error: { code: -32000, message: "Method not allowed." }, id: null, }) ) ); app.listen(port, () => { console.log(`MCP Planilla HTTP server listening on port ${port}`); }); } } main().catch((err) => { console.error("Error fatal en MCP server:", err); process.exit(1); });