From 907ac9da0e87147e601c34c07cf0a8fac73b7618 Mon Sep 17 00:00:00 2001 From: josedario87 <71241187+josedario87@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:02:37 -0600 Subject: [PATCH] Expand MCP server with modular routers --- mcp/createServer.js | 16 +++++ mcp/index.js | 140 +------------------------------------ mcp/lib/api.js | 15 ++++ mcp/modules/asistencias.js | 84 ++++++++++++++++++++++ mcp/modules/empleados.js | 79 +++++++++++++++++++++ mcp/modules/planillas.js | 110 +++++++++++++++++++++++++++++ mcp/modules/tareas.js | 86 +++++++++++++++++++++++ 7 files changed, 391 insertions(+), 139 deletions(-) create mode 100644 mcp/createServer.js create mode 100644 mcp/lib/api.js create mode 100644 mcp/modules/asistencias.js create mode 100644 mcp/modules/empleados.js create mode 100644 mcp/modules/planillas.js create mode 100644 mcp/modules/tareas.js diff --git a/mcp/createServer.js b/mcp/createServer.js new file mode 100644 index 0000000..cd8442f --- /dev/null +++ b/mcp/createServer.js @@ -0,0 +1,16 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import registerPlanillas from "./modules/planillas.js"; +import registerEmpleados from "./modules/empleados.js"; +import registerAsistencias from "./modules/asistencias.js"; +import registerTareas from "./modules/tareas.js"; + +export function createServer() { + const server = new McpServer({ name: "planilla-mcp", version: "0.1.0" }); + + registerPlanillas(server); + registerEmpleados(server); + registerAsistencias(server); + registerTareas(server); + + return server; +} diff --git a/mcp/index.js b/mcp/index.js index 2430bdf..88b1175 100644 --- a/mcp/index.js +++ b/mcp/index.js @@ -1,144 +1,7 @@ -// 🚹 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"; - -const API_BASE_URL = process.env.PLANILLA_API_URL || "http://localhost:4000"; -const log = (...args) => console.log("[MCP]", ...args); - -// 👀 Log de cada request al API -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(); -} - -function createServer() { - const server = new McpServer({ name: "planilla-mcp", version: "0.1.0" }); - - // ----- 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; -} - - - +import { createServer } from "./createServer.js"; async function main() { const useStdio = process.argv.includes("--stdio"); @@ -151,7 +14,6 @@ async function main() { 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(); diff --git a/mcp/lib/api.js b/mcp/lib/api.js new file mode 100644 index 0000000..d7364f1 --- /dev/null +++ b/mcp/lib/api.js @@ -0,0 +1,15 @@ +export const API_BASE_URL = process.env.PLANILLA_API_URL || "http://localhost:4000"; + +export 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(); +} diff --git a/mcp/modules/asistencias.js b/mcp/modules/asistencias.js new file mode 100644 index 0000000..edc1812 --- /dev/null +++ b/mcp/modules/asistencias.js @@ -0,0 +1,84 @@ +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { fetchJSON } from "../lib/api.js"; + +const log = (...args) => console.log("[MCP]", ...args); + +export default function registerAsistencias(server) { + server.resource("asistencia-list", "asistencia://list", async (uri) => { + log("Recurso solicitado", "asistencia-list"); + const asistencias = await fetchJSON("/api/asistencias"); + return { contents: [{ uri: uri.href, text: JSON.stringify(asistencias) }] }; + }); + + server.resource( + "asistencia", + new ResourceTemplate("asistencia://{id}", { list: undefined }), + async (uri, { id }) => { + log("Recurso solicitado", `asistencia ${id}`); + const asistencia = await fetchJSON(`/api/asistencias/${id}`); + return { contents: [{ uri: uri.href, text: JSON.stringify(asistencia) }] }; + } + ); + + server.tool( + "create-asistencia", + "Crea una asistencia", + { + empleado_id: z.number(), + entrada: z.string().optional(), + salida: z.string().optional(), + historial: z.string().optional(), + observacion: z.string().optional(), + estado: z.string().optional(), + fecha_anulado: z.string().optional(), + creador_id: z.number().optional(), + modificado_id: z.number().optional(), + anulador_id: z.number().optional(), + }, + async (params) => { + log("Tool invocada", "create-asistencia", params); + const asistencia = await fetchJSON("/api/asistencias", { + method: "POST", + body: JSON.stringify(params), + }); + return { content: [{ type: "text", text: JSON.stringify(asistencia) }] }; + } + ); + + server.tool( + "update-asistencia", + "Actualiza una asistencia", + { + id: z.number(), + empleado_id: z.number().optional(), + entrada: z.string().optional(), + salida: z.string().optional(), + historial: z.string().optional(), + observacion: z.string().optional(), + estado: z.string().optional(), + fecha_anulado: z.string().optional(), + modificado_id: z.number().optional(), + anulador_id: z.number().optional(), + }, + async ({ id, ...updates }) => { + log("Tool invocada", "update-asistencia", { id, ...updates }); + const asistencia = await fetchJSON(`/api/asistencias/${id}`, { + method: "PUT", + body: JSON.stringify(updates), + }); + return { content: [{ type: "text", text: JSON.stringify(asistencia) }] }; + } + ); + + server.tool( + "delete-asistencia", + "Elimina una asistencia", + { id: z.number() }, + async ({ id }) => { + log("Tool invocada", "delete-asistencia", { id }); + await fetchJSON(`/api/asistencias/${id}`, { method: "DELETE" }); + return { content: [{ type: "text", text: `Asistencia ${id} eliminada` }] }; + } + ); +} diff --git a/mcp/modules/empleados.js b/mcp/modules/empleados.js new file mode 100644 index 0000000..73cef7e --- /dev/null +++ b/mcp/modules/empleados.js @@ -0,0 +1,79 @@ +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { fetchJSON } from "../lib/api.js"; + +const log = (...args) => console.log("[MCP]", ...args); + +export default function registerEmpleados(server) { + server.resource("empleado-list", "empleado://list", async (uri) => { + log("Recurso solicitado", "empleado-list"); + const empleados = await fetchJSON("/api/empleados"); + return { contents: [{ uri: uri.href, text: JSON.stringify(empleados) }] }; + }); + + server.resource( + "empleado", + new ResourceTemplate("empleado://{id}", { list: undefined }), + async (uri, { id }) => { + log("Recurso solicitado", `empleado ${id}`); + const empleado = await fetchJSON(`/api/empleados/${id}`); + return { contents: [{ uri: uri.href, text: JSON.stringify(empleado) }] }; + } + ); + + server.tool( + "create-empleado", + "Crea un empleado", + { + name: z.string(), + cedula: z.number(), + telefono: z.string().optional(), + ubicacion: z.string().optional(), + grupo_estudio: z.string().optional(), + avatar_url: z.string().optional(), + idciat: z.number().optional(), + }, + async (params) => { + log("Tool invocada", "create-empleado", params); + const empleado = await fetchJSON("/api/empleados", { + method: "POST", + body: JSON.stringify(params), + }); + return { content: [{ type: "text", text: JSON.stringify(empleado) }] }; + } + ); + + server.tool( + "update-empleado", + "Actualiza un empleado existente", + { + id: z.number(), + name: z.string().optional(), + cedula: z.number().optional(), + telefono: z.string().optional(), + ubicacion: z.string().optional(), + grupo_estudio: z.string().optional(), + avatar_url: z.string().optional(), + idciat: z.number().optional(), + }, + async ({ id, ...updates }) => { + log("Tool invocada", "update-empleado", { id, ...updates }); + const empleado = await fetchJSON(`/api/empleados/${id}`, { + method: "PUT", + body: JSON.stringify(updates), + }); + return { content: [{ type: "text", text: JSON.stringify(empleado) }] }; + } + ); + + server.tool( + "delete-empleado", + "Elimina un empleado", + { id: z.number() }, + async ({ id }) => { + log("Tool invocada", "delete-empleado", { id }); + await fetchJSON(`/api/empleados/${id}`, { method: "DELETE" }); + return { content: [{ type: "text", text: `Empleado ${id} eliminado` }] }; + } + ); +} diff --git a/mcp/modules/planillas.js b/mcp/modules/planillas.js new file mode 100644 index 0000000..2c4ba32 --- /dev/null +++ b/mcp/modules/planillas.js @@ -0,0 +1,110 @@ +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { fetchJSON } from "../lib/api.js"; + +const log = (...args) => console.log("[MCP]", ...args); + +export default function registerPlanillas(server) { + // ----- 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)]) + ); + if (qs.toString() === "") qs.append("limit", "100"); + const planillas = await fetchJSON(`/api/planillas/search?${qs.toString()}`); + return { content: [{ type: "text", text: JSON.stringify(planillas) }] }; + } + ); +} diff --git a/mcp/modules/tareas.js b/mcp/modules/tareas.js new file mode 100644 index 0000000..b401b3b --- /dev/null +++ b/mcp/modules/tareas.js @@ -0,0 +1,86 @@ +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { fetchJSON } from "../lib/api.js"; + +const log = (...args) => console.log("[MCP]", ...args); + +export default function registerTareas(server) { + server.resource("tarea-list", "tarea://list", async (uri) => { + log("Recurso solicitado", "tarea-list"); + const tareas = await fetchJSON("/api/tareas"); + return { contents: [{ uri: uri.href, text: JSON.stringify(tareas) }] }; + }); + + server.resource( + "tarea", + new ResourceTemplate("tarea://{id}", { list: undefined }), + async (uri, { id }) => { + log("Recurso solicitado", `tarea ${id}`); + const tarea = await fetchJSON(`/api/tareas/${id}`); + return { contents: [{ uri: uri.href, text: JSON.stringify(tarea) }] }; + } + ); + + server.tool( + "create-tarea", + "Crea una tarea", + { + empleado_id: z.number(), + planilla_id: z.number().optional(), + titulo: z.string(), + precio: z.number().optional(), + estado: z.string().optional(), + observacion: z.string().optional(), + fecha: z.string(), + tipo: z.string().optional(), + fecha_anulado: z.string().optional(), + creador_id: z.number().optional(), + anulador_id: z.number().optional(), + }, + async (params) => { + log("Tool invocada", "create-tarea", params); + const tarea = await fetchJSON("/api/tareas", { + method: "POST", + body: JSON.stringify(params), + }); + return { content: [{ type: "text", text: JSON.stringify(tarea) }] }; + } + ); + + server.tool( + "update-tarea", + "Actualiza una tarea", + { + id: z.number(), + empleado_id: z.number().optional(), + planilla_id: z.number().optional(), + titulo: z.string().optional(), + precio: z.number().optional(), + estado: z.string().optional(), + observacion: z.string().optional(), + fecha: z.string().optional(), + tipo: z.string().optional(), + fecha_anulado: z.string().optional(), + anulador_id: z.number().optional(), + }, + async ({ id, ...updates }) => { + log("Tool invocada", "update-tarea", { id, ...updates }); + const tarea = await fetchJSON(`/api/tareas/${id}`, { + method: "PUT", + body: JSON.stringify(updates), + }); + return { content: [{ type: "text", text: JSON.stringify(tarea) }] }; + } + ); + + server.tool( + "delete-tarea", + "Elimina una tarea", + { id: z.number() }, + async ({ id }) => { + log("Tool invocada", "delete-tarea", { id }); + await fetchJSON(`/api/tareas/${id}`, { method: "DELETE" }); + return { content: [{ type: "text", text: `Tarea ${id} eliminada` }] }; + } + ); +}