# Introducción Source: https://picomcp.github.io/picomcp/es/docs/ > Qué es picomcp, qué no es, y las diez líneas que lo explican. picomcp es un framework de TypeScript para construir servidores [Model Context Protocol](https://modelcontextprotocol.io). Un servidor MCP le da a una aplicación de IA (Claude, Cursor, VS Code, tu propio agente) **herramientas** que puede llamar, **recursos** que puede leer y **prompts** que puede ofrecer al usuario. ```ts import { pico } from "picomcp"; import { z } from "zod"; pico({ name: "weather" }) .tool("forecast", { description: "Get the forecast for a city.", input: z.object({ city: z.string() }), run: ({ city }) => getForecast(city), }) .serve(); ``` Eso es un servidor completo. Ejecútalo con `node server.ts` y conéctalo a cualquier cliente MCP. ## Por qué picomcp - **Pequeño.** Unos 4 kB con gzip, cero dependencias. La librería entera cabe en tu cabeza y en una revisión de código. - **Tipado desde tu esquema.** Pasa un esquema de Zod, Valibot o ArkType. Tu handler recibe argumentos tipados, el cliente recibe un JSON Schema y las llamadas inválidas reciben un error legible. - **Funciona en todas partes.** Node 22+, Bun, Deno, Cloudflare Workers y cualquier runtime con `fetch`. Ningún módulo integrado de Node en el núcleo. - **Dos transportes, un solo servidor.** stdio para clientes de escritorio, Streamable HTTP para servidores remotos. Se cambia con una opción. - **Se prueba sin cliente.** `server.call("forecast", { city: "Lyon" })` ejecuta una herramienta en el mismo proceso, totalmente tipada. - **Verificado con el SDK oficial.** La suite de tests controla picomcp con `@modelcontextprotocol/sdk` en ambos transportes. ## Qué no es picomcp picomcp implementa el lado servidor de MCP y nada más. No incluye cliente, ni bucle de agente, ni SDK de LLM. Se centra en la mitad sin estado, de petición y respuesta, del protocolo, que es lo que necesitan la mayoría de los servidores. Las funciones iniciadas por el servidor, como sampling y elicitation, aún no están soportadas: la lista exacta está en [Cobertura del protocolo](https://picomcp.github.io/picomcp/es/docs/protocol). ## Siguientes pasos - [Inicio rápido](https://picomcp.github.io/picomcp/es/docs/quickstart): construye y conecta un servidor en cinco minutos. - [Herramientas](https://picomcp.github.io/picomcp/es/docs/tools): todo lo que puede hacer una herramienta. - [Para agentes de IA](https://picomcp.github.io/picomcp/es/docs/agents): dale la documentación a tu agente de código con una sola URL. --- # Inicio rápido Source: https://picomcp.github.io/picomcp/es/docs/quickstart > Crea, prueba y conecta tu primer servidor MCP en cinco minutos. Necesitas Node 22.18 o posterior (ejecuta archivos TypeScript directamente), Bun o Deno. ## Crear un proyecto ```sh npx picomcp init my-server cd my-server npm install ``` Obtienes un servidor listo para funcionar: | Archivo | Para qué sirve | | --- | --- | | `server.ts` | La definición de tu servidor: herramientas, recursos, prompts | | `main.ts` | Arranca el servidor por stdio | | `server.test.ts` | Un test que llama a una herramienta en el mismo proceso | ¿Prefieres empezar desde cero? Instala los dos paquetes y escribe `server.ts` tú mismo: ```sh npm install picomcp zod ``` ## Escribir una herramienta Abre `server.ts`: ```ts import { pico } from "picomcp"; import { z } from "zod"; export const server = pico({ name: "my-server", version: "0.1.0" }) .tool("greet", { description: "Greets someone by name.", input: z.object({ name: z.string().describe("Who to greet") }), run: ({ name }) => `Hello, ${name}!`, }); ``` - `description` es lo que lee el modelo para decidir cuándo llamar a la herramienta. Escríbela para el modelo. - `input` es cualquier [Standard Schema](https://picomcp.github.io/picomcp/es/docs/schemas). Dentro de `run`, `name` es de tipo `string`. - Aquí `run` devuelve una cadena. También puede devolver objetos, bloques de contenido o lanzar un error: ver [Herramientas](https://picomcp.github.io/picomcp/es/docs/tools). ## Probarla ```ts import assert from "node:assert/strict"; import { test } from "node:test"; import { server } from "./server.ts"; test("greet says hello", async () => { const result = await server.call("greet", { name: "Ada" }); assert.deepEqual(result.content, [{ type: "text", text: "Hello, Ada!" }]); }); ``` ```sh npm test ``` `server.call` se comprueba con los tipos: una errata en el nombre de la herramienta o un argumento que falta no compila. ## Conectarla Para Claude Code: ```sh claude mcp add my-server -- node /absolute/path/to/my-server/main.ts ``` Después pídele a Claude que salude a alguien. Para Claude Desktop, Cursor, VS Code y otros, ver [Conectar un cliente](https://picomcp.github.io/picomcp/es/docs/clients). ## Inspeccionarla MCP Inspector te permite llamar a las herramientas a mano y ver los mensajes en bruto: ```sh npx @modelcontextprotocol/inspector node main.ts ``` --- # Conectar un cliente Source: https://picomcp.github.io/picomcp/es/docs/clients > Configuración para Claude Code, Claude Desktop, Cursor, VS Code y MCP Inspector. Todos los clientes MCP arrancan un servidor local de la misma forma: ejecutan un comando y le hablan por stdio. A los servidores remotos se llega por URL, con Streamable HTTP. Usa rutas absolutas en todas las configuraciones de abajo. ## Claude Code ```sh # local, over stdio claude mcp add weather -- node /absolute/path/main.ts # remote, over HTTP claude mcp add --transport http weather https://example.com/mcp ``` Añade `--scope project` para escribir el servidor en `.mcp.json` y compartirlo con tu equipo. ## Claude Desktop Edita `claude_desktop_config.json` (Ajustes → Desarrollador → Editar configuración) y reinicia la aplicación: ```json { "mcpServers": { "weather": { "command": "node", "args": ["/absolute/path/main.ts"] } } } ``` ## Cursor Crea `.cursor/mcp.json` en tu proyecto, o `~/.cursor/mcp.json` para todos tus proyectos: ```json { "mcpServers": { "weather": { "command": "node", "args": ["/absolute/path/main.ts"] }, "remote-weather": { "url": "https://example.com/mcp" } } } ``` ## VS Code Crea `.vscode/mcp.json`: ```json { "servers": { "weather": { "type": "stdio", "command": "node", "args": ["/absolute/path/main.ts"] }, "remote-weather": { "type": "http", "url": "https://example.com/mcp" } } } ``` ## MCP Inspector ```sh npx @modelcontextprotocol/inspector node /absolute/path/main.ts ``` Para inspeccionar un servidor HTTP, arráncalo con `serve({ http: 3000 })`, abre el Inspector, elige **Streamable HTTP** e introduce `http://127.0.0.1:3000/mcp`. El Inspector se ejecuta en localhost, que picomcp permite por defecto. ## Bun y Deno Sustituye `node` por tu runtime: ```json { "command": "bun", "args": ["/absolute/path/main.ts"] } ``` ```json { "command": "deno", "args": ["run", "--allow-net", "/absolute/path/main.ts"] } ``` ## Solución de problemas **El cliente no muestra ninguna herramienta.** Ejecuta tú mismo el comando de la configuración. Si termina o muestra un error, el cliente ve lo mismo. **«Unexpected token» o errores de JSON en el log del cliente.** Algo ha escrito en stdout. picomcp redirige `console.log` a stderr cuando sirve por stdio, pero otras librerías pueden escribir directamente en `process.stdout`. **Node no puede ejecutar archivos `.ts`.** Actualiza a Node 22.18 o posterior, o ejecuta el servidor con Bun. --- # Herramientas Source: https://picomcp.github.io/picomcp/es/docs/tools > Definir herramientas, validar la entrada, devolver texto, objetos o medios, informar de errores y gestionar la cancelación. Una herramienta es una función que el modelo puede llamar. Registra una con `server.tool(name, config)`: ```ts server.tool("search_issues", { title: "Search issues", description: "Search open GitHub issues in a repository. Use it before filing a new issue.", input: z.object({ repo: z.string().describe("owner/name"), query: z.string(), limit: z.number().int().max(50).default(10), }), annotations: { readOnlyHint: true, openWorldHint: true }, run: async ({ repo, query, limit }, { signal }) => { const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(`${query} repo:${repo}`)}&per_page=${limit}`, { signal }); const { items } = await res.json(); return items.map((i: { title: string; html_url: string }) => `${i.title} — ${i.html_url}`).join("\n"); }, }); ``` ## Configuración | Opción | Tipo | Descripción | | --- | --- | --- | | `description` | `string` | Qué hace la herramienta y cuándo usarla. El modelo lee este texto. | | `title` | `string` | Nombre que muestran los clientes. | | `input` | Standard Schema | Esquema de los argumentos. Omítelo en herramientas sin argumentos. | | `output` | Standard Schema | Esquema del resultado estructurado. Ver [Salida estructurada](#salida-estructurada). | | `annotations` | `ToolAnnotations` | Pistas para los clientes: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. | | `run` | `(input, ctx) => value` | El handler. Puede ser asíncrono. | Los nombres de herramienta tienen de 1 a 128 caracteres y solo usan letras, dígitos, `_`, `.` y `-`. Registrar un nombre inválido o repetido lanza un error enseguida, así que los fallos aparecen al arrancar el servidor y no cuando un modelo llama a la herramienta. ## Validación de la entrada picomcp valida los argumentos con tu esquema antes de llamar a `run`. Se aplican valores por defecto y transformaciones, así que en el ejemplo `limit` siempre es un número dentro de `run`. Si la validación falla, no se llama a la herramienta. El cliente recibe un error de herramienta que nombra el campo incorrecto, para que el modelo corrija su llamada y vuelva a intentarlo: ```text Invalid arguments for tool "search_issues": limit: Too big: expected number to be <=50 ``` ## Valores de retorno Devuelve lo que te resulte natural. picomcp lo convierte en un resultado MCP: | `run` devuelve | El cliente recibe | | --- | --- | | `string` | Un bloque de texto | | un objeto, un array o un número | Su JSON como texto. Los objetos también se envían como `structuredContent`. | | un bloque de contenido, o un array de bloques | Esos bloques tal cual | | `{ content: [...] }` | El resultado tal cual, para tener control total | | `undefined` | Un resultado vacío | Construye bloques de contenido con los helpers: ```ts import { audio, image, link, text } from "picomcp"; run: async () => [ text("Here is the chart:"), image(await renderPng(), "image/png"), // Uint8Array or base64 string link("file:///reports/q3.csv", "q3.csv", { mimeType: "text/csv" }), ]; ``` ## Salida estructurada Declara un esquema `output` cuando el resultado lo consumen clientes o código, no solo el modelo: ```ts server.tool("get_price", { input: z.object({ sku: z.string() }), output: z.object({ sku: z.string(), cents: z.number().int(), currency: z.literal("EUR") }), run: async ({ sku }) => ({ sku, cents: await priceOf(sku), currency: "EUR" }), }); ``` Entonces picomcp: 1. anuncia el esquema como `outputSchema` en `tools/list`, 2. comprueba con los tipos el valor que devuelve `run`, 3. valida el valor en tiempo de ejecución y lo envía como `structuredContent`, con una copia en texto JSON para clientes antiguos. Si el valor no encaja, el cliente recibe un error de herramienta en lugar de datos mal formados. ## Errores Lanza un error desde `run`. El mensaje se convierte en un error de herramienta (`isError: true`) que el modelo puede leer y al que puede reaccionar: ```ts run: async ({ id }) => { const user = await db.users.find(id); if (!user) throw new Error(`No user with id ${id}. Call list_users to see valid ids.`); return user; }; ``` Escribe los mensajes de error para el modelo: di qué ha fallado y qué hacer a continuación. Los errores de herramienta forman parte de una conversación normal. Los errores de protocolo no: usa `McpError` solo cuando la petición en sí es incorrecta, por ejemplo cuando el usuario no tiene acceso: ```ts import { McpError } from "picomcp"; throw new McpError(-32001, "Not authorised for this workspace"); ``` ## Cancelación El segundo argumento de `run` lleva un `AbortSignal`. Se activa cuando el cliente envía `notifications/cancelled`, normalmente porque el usuario ha pulsado detener. Pásalo a `fetch` y a otras API que lo acepten: ```ts run: async ({ url }, { signal }) => { const res = await fetch(url, { signal }); return res.text(); }; ``` Una vez cancelada una petición, picomcp no envía respuesta para ella, como exige el protocolo. ## Metadatos `ctx.meta` contiene el objeto `_meta` que el cliente envió con la petición, si lo hay. Los clientes lo usan para trazas y tokens de progreso. ## Escribir buenas herramientas - **Una tarea por herramienta.** `create_invoice` y `send_invoice` son mejores que `invoice(action)`. - **Describe los argumentos.** `.describe()` en cada campo acaba en el JSON Schema que ve el modelo. - **Devuelve menos.** Los modelos pagan cada token. Devuelve los campos que importan, no toda la respuesta de la API. - **Anota con honestidad.** `readOnlyHint: true` permite a los clientes saltarse la confirmación. Nunca lo pongas en una herramienta que escribe. --- # Recursos Source: https://picomcp.github.io/picomcp/es/docs/resources > Exponer archivos, registros y datos en vivo que el cliente puede leer, con plantillas de URI tipadas. Los recursos son datos que el cliente puede leer y adjuntar a una conversación: archivos, filas de base de datos, respuestas de API. Las herramientas las llama el modelo; los recursos suele elegirlos el usuario o la aplicación. ```ts server.resource("config://app", { description: "The app configuration", read: () => loadConfig(), }); ``` ## Plantillas de URI Pon `{variables}` en la URI para servir toda una familia de recursos. Las variables se tipan a partir de la propia cadena: ```ts server.resource("github://{owner}/{repo}/readme", { mimeType: "text/markdown", read: async ({ owner, repo }) => { // ^? { owner: string; repo: string } const res = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/README.md`); return res.text(); }, }); ``` | Sintaxis | Coincide con | Ejemplo | | --- | --- | --- | | `{name}` | Un segmento de ruta, sin `/`, `?` ni `#` | `users://{id}` coincide con `users://42` | | `{+name}` | Cualquier cosa, incluido `/` | `files://{+path}` coincide con `files://src/index.ts` | Los valores se decodifican (URL) antes de llegar a `read`. Los recursos estáticos aparecen en `resources/list` y las plantillas en `resources/templates/list`. ## Valores de retorno | `read` devuelve | Contenido enviado | | --- | --- | | `string` | Texto, con `mimeType` o `text/plain` | | `Uint8Array` | Blob en base64, con `mimeType` o `application/octet-stream` | | un array de `{ uri, text }` o `{ uri, blob }` | Esos contenidos tal cual | | cualquier otro valor | Su JSON, como `application/json` | `read` recibe el contexto como segundo argumento, con la `uri` solicitada añadida: ```ts read: (params, { uri, signal }) => fetch(toHttp(uri), { signal }).then((r) => r.text()); ``` ## Opciones | Opción | Descripción | | --- | --- | | `name` | Identificador que se muestra a los usuarios. Por defecto, la URI. | | `title` | Nombre visible. | | `description` | Qué contiene el recurso. | | `mimeType` | Tipo de contenido de lo que devuelve `read`. | | `annotations` | Pistas `audience`, `priority` y `lastModified`. | ## No encontrado Cuando ningún recurso coincide con una URI, picomcp responde con el error de protocolo `-32002 Resource not found`. Para rechazar una URI que coincide con una plantilla pero no apunta a nada, lanza un `McpError` con el mismo código: ```ts import { McpError } from "picomcp"; read: async ({ id }) => { const user = await db.users.find(id); if (!user) throw new McpError(-32002, `Resource not found: users://${id}`); return user; }; ``` ## Enlazar desde herramientas Una herramienta puede señalar un recurso al modelo en lugar de incluirlo, lo que deja los datos grandes fuera del contexto hasta que hagan falta: ```ts import { link } from "picomcp"; run: async () => link("reports://2026-q3", "Q3 report", { mimeType: "application/pdf" }); ``` --- # Prompts Source: https://picomcp.github.io/picomcp/es/docs/prompts > Ofrecer plantillas de prompts reutilizables que el usuario elige en el cliente, con argumentos validados. Los prompts son plantillas que el usuario elige en el cliente, a menudo como comandos con barra. Convierten unos pocos argumentos en uno o varios mensajes. ```ts server.prompt("review", { title: "Code review", description: "Review a diff for bugs and style issues.", args: z.object({ diff: z.string().describe("The diff to review"), focus: z.string().optional().describe("What to pay attention to"), }), run: ({ diff, focus }) => `Review this diff${focus ? `, focusing on ${focus}` : ""}:\n\n${diff}`, }); ``` El cliente muestra `diff` como obligatorio y `focus` como opcional, con sus descripciones, todo deducido del esquema. ## Los argumentos son cadenas MCP envía los argumentos de los prompts como cadenas. Usa esquemas de cadena, o convierte: ```ts args: z.object({ count: z.coerce.number().int().min(1) }); ``` Los argumentos inválidos se rechazan con un error `Invalid params` antes de llamar a `run`. ## Valores de retorno | `run` devuelve | Mensajes enviados | | --- | --- | | `string` | Un mensaje de usuario | | `PromptMessage[]` | Esos mensajes | | `{ description, messages }` | El resultado completo | Usa un array para plantillas de varios turnos o para incluir medios: ```ts import { image, text } from "picomcp"; run: async ({ id }) => [ { role: "user", content: text("What is wrong with this screenshot?") }, { role: "user", content: image(await screenshot(id), "image/png") }, ]; ``` --- # Esquemas Source: https://picomcp.github.io/picomcp/es/docs/schemas > Usar Zod, Valibot, ArkType o cualquier librería Standard Schema, y cómo se genera el JSON Schema. picomcp no incluye una librería de esquemas. Acepta cualquier librería que implemente [Standard Schema](https://standardschema.dev) y genera el JSON Schema que necesitan los clientes MCP mediante [Standard JSON Schema](https://standardschema.dev/json-schema). Sigues usando la librería que ya tienes, y picomcp sigue siendo pequeño. ## Zod 4 Funciona tal cual: ```ts import { z } from "zod"; server.tool("forecast", { input: z.object({ city: z.string().describe("City name") }), run: ({ city }) => getForecast(city), }); ``` ## ArkType Funciona tal cual: ```ts import { type } from "arktype"; server.tool("forecast", { input: type({ city: "string" }), run: ({ city }) => getForecast(city), }); ``` ## Valibot Valibot tiene el soporte de JSON Schema en un paquete aparte. Envuelve el esquema con `toStandardJsonSchema`: ```ts import * as v from "valibot"; import { toStandardJsonSchema } from "@valibot/to-json-schema"; server.tool("forecast", { input: toStandardJsonSchema(v.object({ city: v.string() })), run: ({ city }) => getForecast(city), }); ``` ## Cualquier otra librería Si una librería valida mediante Standard Schema pero no puede producir JSON Schema (Zod 3, por ejemplo), añade uno tú con `withJsonSchema`: ```ts import { withJsonSchema } from "picomcp"; server.tool("forecast", { input: withJsonSchema(schema, { type: "object", properties: { city: { type: "string" } }, required: ["city"], }), run: ({ city }) => getForecast(city), }); ``` Sin esto, registrar la herramienta lanza un error que te dice exactamente esto, así que el problema nunca llega a un cliente. ## Cómo fluyen los tipos - `run` recibe el tipo de **salida** del esquema, después de valores por defecto y transformaciones. - `server.call()` acepta el tipo de **entrada** del esquema, así que los campos con valor por defecto son opcionales en los tests. - Con un esquema `output`, `run` debe devolver el tipo de entrada de ese esquema. ## Consejos para esquemas que los modelos entienden - Describe cada campo. Las descripciones son la documentación que lee el modelo. - Prefiere enums a texto libre cuando el conjunto de valores es conocido. - Usa valores por defecto para el comportamiento opcional, así el modelo puede omitir argumentos. - Mantén los objetos planos. Los argumentos muy anidados son más difíciles de rellenar para un modelo. --- # Transportes Source: https://picomcp.github.io/picomcp/es/docs/transports > stdio para clientes locales, Streamable HTTP para remotos, y el handler fetch que mueve ambos. Un transporte lleva los mensajes JSON-RPC entre el cliente y tu servidor. picomcp soporta los dos que define MCP. Tus herramientas no cambian de uno a otro. ## stdio ```ts await server.serve(); ``` El cliente arranca tu servidor como proceso hijo e intercambia JSON delimitado por saltos de línea por stdin y stdout. Es lo que usan los clientes de escritorio y los agentes de código para servidores locales. stdout pertenece al protocolo. Mientras sirve por stdio, picomcp redirige `console.log`, `console.info` y `console.debug` a stderr, para que una línea de log perdida no rompa la conexión. Los clientes suelen mostrar stderr en sus logs. ## Streamable HTTP ```ts const running = await server.serve({ http: 3000 }); console.error(`Listening on ${running.url}`); // http://127.0.0.1:3000/mcp ``` `serve({ http })` arranca un servidor en Bun, Deno o Node, el que esté en uso. Opciones: | Opción | Por defecto | Descripción | | --- | --- | --- | | `port` | `3000` | Puerto de escucha. `0` elige uno libre. | | `hostname` | `"127.0.0.1"` | Usa `"0.0.0.0"` para aceptar conexiones desde otras máquinas. | | `path` | `"/mcp"` | Ruta del endpoint. Las demás rutas responden 404. | Devuelve `{ url, close() }`. ## El handler fetch `server.fetch` es una función `(Request) => Promise` que sigue los estándares web. Pásala a cualquier runtime o framework que hable `fetch`: ```ts Bun.serve({ port: 3000, fetch: server.fetch }); ``` ```ts Deno.serve({ port: 3000 }, server.fetch); ``` ```ts export default { fetch: server.fetch }; // Cloudflare Workers ``` `server.fetch` no mira la ruta: dirige tú las peticiones hacia él. Más plataformas en [Despliegue](https://picomcp.github.io/picomcp/es/docs/deploy). ## Cómo habla picomcp HTTP picomcp usa Streamable HTTP en modo JSON sin estado: - `POST` con un mensaje JSON-RPC o un lote → `200` con una respuesta JSON, o `202` para notificaciones. - `GET` y `DELETE` → `405`, que indica a los clientes que no se ofrece un flujo del servidor al cliente. - `OPTIONS` → preflight CORS para los orígenes permitidos. - Una cabecera `MCP-Protocol-Version` no soportada → `400`. No hay sesiones que guardar, así que cualquier instancia puede responder a cualquier petición. Por eso picomcp encaja en plataformas serverless, balanceadores de carga y autoescalado sin trabajo extra. ## Orígenes y DNS rebinding Los navegadores envían una cabecera `Origin`. Una página maliciosa podría usarla para llegar a un servidor en tu máquina, así que picomcp la comprueba: - Las peticiones sin `Origin` (CLI, servidores, aplicaciones de escritorio) siempre se permiten. - Por defecto solo se permiten los orígenes `localhost`, `127.0.0.1` y `[::1]`, en cualquier puerto. - Los demás orígenes reciben `403`. Permite otros con la opción `origins`: ```ts pico({ name: "notes", origins: ["https://claude.ai", "https://app.example.com"] }); pico({ name: "public", origins: "*" }); // any origin: only for public, unauthenticated servers ``` ## Transportes personalizados `server.handle(message)` procesa un mensaje JSON-RPC, o un lote, y devuelve la respuesta. Devuelve `undefined` para las notificaciones. Úsalo para conectar picomcp a WebSockets, colas de mensajes o cualquier otra cosa: ```ts socket.on("message", async (data) => { const response = await server.handle(JSON.parse(data)); if (response) socket.send(JSON.stringify(response)); }); ``` --- # Tests Source: https://picomcp.github.io/picomcp/es/docs/testing > Probar las herramientas en el mismo proceso con tipos completos, y después el protocolo de extremo a extremo. ## Llamar a las herramientas en el mismo proceso `server.call(name, input)` ejecuta una herramienta exactamente como lo haría un cliente, sin transporte ni subproceso. Se aplican la validación, los valores por defecto, la comprobación de la salida y la gestión de errores. ```ts import { describe, expect, it } from "vitest"; import { server } from "./server.ts"; describe("forecast", () => { it("returns the forecast", async () => { const result = await server.call("forecast", { city: "Lyon" }); expect(result.isError).toBeUndefined(); expect(result.structuredContent).toMatchObject({ city: "Lyon" }); }); it("explains invalid input to the model", async () => { const result = await server.call("forecast", { city: "Lyon", days: 30 }); expect(result.isError).toBe(true); }); }); ``` Funciona con cualquier ejecutor de tests: Vitest, `node:test`, `bun test`, `deno test`. ## Llamadas comprobadas por los tipos Cuando las herramientas se registran encadenadas, `call` conoce sus nombres y entradas: ```ts const server = pico({ name: "weather" }).tool("forecast", { input: z.object({ city: z.string(), days: z.number().default(3) }), run: ({ city, days }) => `${city}: ${days} days`, }); server.call("forecast", { city: "Lyon" }); // ✓ days is optional: it has a default server.call("forecast", {}); // ✗ city is missing server.call("forcast", { city: "Lyon" }); // ✗ no tool named forcast ``` Si registras las herramientas en sentencias separadas, el tipo de `server` no puede seguirlas y `call` acepta cualquier nombre. Encadena las llamadas o reasigna: `const app = server.tool(...)`. ## Probar el protocolo `server.handle` recibe JSON-RPC en bruto, lo que viene bien para comprobar listados y esquemas: ```ts const res = await server.handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }); expect(res).toMatchSnapshot(); ``` Un snapshot de `tools/list` es una forma barata de notar cuándo un cambio de esquema altera lo que ven los modelos. ## Probar con un cliente real Para tests de extremo a extremo, controla el servidor con el cliente del SDK oficial por HTTP: ```ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const running = await server.serve({ http: { port: 0 } }); const client = new Client({ name: "test", version: "1.0.0" }); await client.connect(new StreamableHTTPClientTransport(new URL(running.url!))); const { tools } = await client.listTools(); await client.close(); await running.close(); ``` La propia suite de tests de picomcp hace exactamente esto, por stdio y por HTTP. --- # Despliegue Source: https://picomcp.github.io/picomcp/es/docs/deploy > Ejecutar un servidor MCP remoto en Cloudflare Workers, Deno Deploy, Bun, Node, Hono o Next.js, con autenticación. A un servidor remoto se llega por URL, con Streamable HTTP. Como picomcp no tiene estado, funciona en plataformas serverless sin configuración adicional. ## Cloudflare Workers ```ts import { server } from "./server.ts"; export default { fetch(request: Request) { if (new URL(request.url).pathname !== "/mcp") return new Response("Not found", { status: 404 }); return server.fetch(request); }, }; ``` ```sh npx wrangler deploy src/worker.ts ``` ## Deno Deploy ```ts import { server } from "./server.ts"; Deno.serve((request) => server.fetch(request)); ``` ## Bun ```ts Bun.serve({ port: Number(process.env.PORT ?? 3000), routes: { "/mcp": server.fetch }, }); ``` ## Node ```ts await server.serve({ http: { port: Number(process.env.PORT ?? 3000), hostname: "0.0.0.0" } }); ``` ## Hono ```ts import { Hono } from "hono"; const app = new Hono(); app.all("/mcp", (c) => server.fetch(c.req.raw)); export default app; ``` ## Next.js ```ts // app/mcp/route.ts import { server } from "@/lib/mcp"; export const POST = server.fetch; export const GET = server.fetch; export const OPTIONS = server.fetch; ``` ## Autenticación picomcp no gestiona la autenticación por sí mismo: envuelve `server.fetch` y comprueba la petición antes de que llegue a tus herramientas. ```ts export default { async fetch(request: Request, env: { TOKEN: string }) { if (request.headers.get("authorization") !== `Bearer ${env.TOKEN}`) { return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": "Bearer" } }); } return server.fetch(request); }, }; ``` Los clientes envían la cabecera que indica su configuración, por ejemplo: ```sh claude mcp add --transport http notes https://notes.example.com/mcp --header "Authorization: Bearer $TOKEN" ``` Para OAuth, pon el middleware de tu proveedor delante de `server.fetch` y sigue la [especificación de autorización de MCP](https://modelcontextprotocol.io/specification/latest/basic/authorization). ## Lista de comprobación - Sirve por HTTPS. - Exige autenticación, salvo que el servidor sea público y de solo lectura. - Configura `origins` con los orígenes de navegador que esperas, si los hay. - Guarda los secretos en variables de entorno, nunca en descripciones ni resultados de herramientas. - Trata los argumentos de las herramientas como entradas no fiables: el modelo puede dejarse influir por el contenido que lee. --- # Referencia de la API Source: https://picomcp.github.io/picomcp/es/docs/api > Todas las exportaciones de picomcp, con sus firmas. ```ts import { pico, McpError, PROTOCOL_VERSIONS, text, image, audio, link, withJsonSchema } from "picomcp"; import type { Server, ToolConfig, ResourceConfig, PromptConfig, Context, CallToolResult } from "picomcp"; ``` ## pico(info) Crea un servidor. ```ts function pico(info: ServerInfo): Server; interface ServerInfo { name: string; // machine-friendly name, e.g. "weather" version?: string; // defaults to "0.0.0" title?: string; // display name instructions?: string; // how the model should use the server as a whole origins?: string[] | "*"; // allowed browser origins for HTTP; defaults to localhost } ``` ## server.tool(name, config) Registra una herramienta y devuelve el servidor. Ver [Herramientas](https://picomcp.github.io/picomcp/es/docs/tools). ```ts interface ToolConfig { title?: string; description?: string; input?: I; // Standard Schema output?: O; // Standard Schema; enables structuredContent annotations?: ToolAnnotations; run(input: Infer, ctx: Context): Return | Promise; } interface ToolAnnotations { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; } ``` Lanza un `TypeError` ante un nombre inválido o repetido, o ante un esquema sin soporte de JSON Schema. ## server.resource(uri, config) Registra un recurso, o una plantilla de recurso cuando `uri` contiene `{variables}`. Devuelve el servidor. Ver [Recursos](https://picomcp.github.io/picomcp/es/docs/resources). ```ts interface ResourceConfig { name?: string; title?: string; description?: string; mimeType?: string; annotations?: Annotations; read(params: UriParams, ctx: Context & { uri: string }): ResourceReturn | Promise; } ``` ## server.prompt(name, config) Registra un prompt y devuelve el servidor. Ver [Prompts](https://picomcp.github.io/picomcp/es/docs/prompts). ```ts interface PromptConfig { title?: string; description?: string; args?: A; // Standard Schema of an object with string fields run(args: Infer, ctx: Context): string | PromptMessage[] | { description?: string; messages: PromptMessage[] }; } ``` ## server.call(name, input?) Ejecuta una herramienta en el mismo proceso y devuelve su `CallToolResult`. Se comprueba con los tipos cuando las herramientas se registraron encadenadas. Se rechaza ante un nombre de herramienta desconocido. Ver [Tests](https://picomcp.github.io/picomcp/es/docs/testing). ```ts call(name: string, input?: unknown): Promise; interface CallToolResult { content: ContentBlock[]; structuredContent?: Record; isError?: boolean; } ``` ## server.serve(options?) Arranca el servidor por stdio, o por Streamable HTTP cuando se define `http`. Ver [Transportes](https://picomcp.github.io/picomcp/es/docs/transports). ```ts serve(options?: { http?: number | { port?: number; hostname?: string; path?: string } }): Promise<{ url?: string; close(): Promise }>; ``` ## server.fetch(request) Handler Streamable HTTP que sigue los estándares web. Está ligado al servidor, así que se puede pasar como un valor. ```ts fetch(request: Request): Promise; ``` ## server.handle(message) Procesa un mensaje JSON-RPC o un lote. Devuelve la respuesta, o `undefined` para las notificaciones. ```ts handle(message: unknown): Promise; ``` ## Context El segundo argumento de cada handler. ```ts interface Context { signal: AbortSignal; // aborted when the client cancels meta: Record | undefined; // the request's _meta } ``` ## Helpers de contenido ```ts text(text: string, annotations?: Annotations): ContentBlock; image(data: string | Uint8Array, mimeType: string): ContentBlock; audio(data: string | Uint8Array, mimeType: string): ContentBlock; link(uri: string, name: string, extra?: { title?: string; description?: string; mimeType?: string }): ContentBlock; ``` `data` es base64 o bytes en bruto. Los bytes se codifican por ti en cualquier runtime. ## withJsonSchema(schema, jsonSchema, output?) Añade un JSON Schema a un Standard Schema que no puede generarlo. Ver [Esquemas](https://picomcp.github.io/picomcp/es/docs/schemas). ```ts withJsonSchema(schema: S, jsonSchema: JsonSchema, output?: JsonSchema): StandardSchemaV1, Infer>; ``` ## McpError Lánzalo desde un handler para responder con un error JSON-RPC en lugar de un resultado. ```ts new McpError(code: number, message: string, data?: unknown); ``` | Código | Significado | | --- | --- | | `-32602` | Parámetros inválidos | | `-32002` | Recurso no encontrado | | de `-32000` a `-32099` | Libres para los errores de tu servidor | ## PROTOCOL_VERSIONS Las versiones del protocolo MCP que habla picomcp, de la más reciente a la más antigua: `["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]`. ## Helpers de tipos | Tipo | Descripción | | --- | --- | | `Infer` | Tipo de salida de un esquema | | `InferInput` | Tipo de entrada de un esquema | | `UriParams<"a://{x}/{+y}">` | `{ x: string; y: string }` | | `StandardSchemaV1` | La interfaz Standard Schema, integrada | --- # Cobertura del protocolo Source: https://picomcp.github.io/picomcp/es/docs/protocol > Qué partes exactas de la especificación MCP implementa picomcp, y cuáles todavía no. picomcp apunta a MCP `2025-11-25` y negocia hacia `2025-06-18`, `2025-03-26` y `2024-11-05`. Cada función marcada como soportada está cubierta por tests, incluidos tests de extremo a extremo con el cliente del SDK oficial de TypeScript. ## Soportado | Área | Función | | --- | --- | | Ciclo de vida | `initialize` con negociación de versión, `notifications/initialized`, `ping` | | Herramientas | `tools/list`, `tools/call`, validación de entrada, `outputSchema` y `structuredContent`, anotaciones, títulos | | Contenido | texto, imagen, audio, enlaces a recursos, recursos incrustados | | Recursos | `resources/list`, `resources/templates/list`, `resources/read`, contenido de texto y binario | | Prompts | `prompts/list`, `prompts/get`, argumentos deducidos de los esquemas | | Utilidades | `notifications/cancelled` con `AbortSignal` | | JSON-RPC | Lotes, códigos de error, respuestas del cliente ignoradas | | stdio | JSON delimitado por saltos de línea, stdout protegido de logs perdidos | | Streamable HTTP | Respuestas JSON sin estado, `202` para notificaciones, `405` para `GET`, CORS, comprobación de `Origin` y `MCP-Protocol-Version` | ## Todavía no soportado | Función | Por qué | | --- | --- | | Sampling, elicitation, roots | Requieren que el servidor envíe peticiones al cliente. El diseño sin estado de picomcp aún no mantiene un canal abierto para ello. | | Notificaciones de log, de progreso y `list_changed` | Mismo motivo: mensajes del servidor al cliente. | | Suscripciones a recursos | Mismo motivo. | | Autocompletado | Previsto. | | Cursores de paginación | Las listas se devuelven completas, lo que va bien para servidores con hasta unos cientos de entradas. | | Sesiones (`Mcp-Session-Id`) y flujos SSE | No hacen falta en modo sin estado. | | Tasks | Experimental en la especificación. | ¿Necesitas alguna? [Abre una issue](https://github.com/picomcp/picomcp/issues) y describe tu caso de uso. Las funciones llegan cuando se pueden añadir sin hacer grande a picomcp. --- # Para agentes de IA Source: https://picomcp.github.io/picomcp/es/docs/agents > Documentación legible por máquinas, llms.txt, páginas Markdown y reglas para tu agente de código. Los agentes de código escriben muchos servidores MCP. Esta documentación está pensada para que la lean ellos tanto como tú. ## Documentación legible por máquinas | URL | Contenido | | --- | --- | | [`/llms.txt`](https://picomcp.github.io/picomcp/llms.txt) | Índice de todas las páginas con un resumen de una línea, en formato [llms.txt](https://llmstxt.org) | | [`/llms-full.txt`](https://picomcp.github.io/picomcp/llms-full.txt) | Toda la documentación en un solo archivo Markdown | | `/docs/.md` | Cualquier página en Markdown. Añade `.md` a su URL, por ejemplo [`/docs/tools.md`](https://picomcp.github.io/picomcp/es/docs/tools.md) | Cada página tiene además un botón **Copiar como Markdown**, para pegarla en un chat. Estos archivos están en inglés, el idioma de referencia y el que mejor siguen los modelos. Cada traducción tiene también su propio `llms-full.txt`, por ejemplo `/fr/llms-full.txt`. ## Indica la documentación a tu agente Pega esto en las instrucciones de tu agente, por ejemplo en `AGENTS.md` o `CLAUDE.md` en la raíz de tu proyecto: ```md ## MCP servers This project builds MCP servers with picomcp. - Read https://picomcp.github.io/picomcp/llms-full.txt before writing picomcp code. - Define servers with `pico({ name })` and chain `.tool()`, `.resource()`, `.prompt()`. - Use Zod 4 schemas with `.describe()` on every field. - Write tool descriptions and error messages for the model: say what the tool does, when to use it, and what to do after an error. - Never write to stdout in a stdio server. Use `console.error` for logs. - Test tools with `server.call(name, input)`. Do not spawn a client in unit tests. ``` ## Dale a tu agente un servidor de documentación La documentación también está disponible como servidor MCP, [`picomcp-docs`](https://www.npmjs.com/package/picomcp-docs), construido con picomcp. Da a los agentes tres herramientas, `list_docs`, `search_docs` y `read_doc`, y cada página como recurso. En Claude Code: ```sh claude mcp add picomcp-docs -- npx -y picomcp-docs ``` En Cursor, Windsurf, Claude Desktop y la mayoría de los demás clientes, añádelo al archivo de configuración MCP (`.cursor/mcp.json` en Cursor): ```json { "mcpServers": { "picomcp-docs": { "command": "npx", "args": ["-y", "picomcp-docs"] } } } ``` El código fuente está en [`packages/docs`](https://github.com/picomcp/picomcp/tree/main/packages/docs), unas 100 líneas. ## Lo que los agentes suelen hacer mal Estos son los errores que más cometen los modelos al escribir servidores MCP, y cómo funcionan las cosas en picomcp: - **En modo stdio, stdout es el protocolo.** Un `console.log` lo corrompería. picomcp redirige la salida de la consola a stderr mientras sirve por stdio, pero `process.stdout.write` sigue siendo peligroso. - **Los errores de herramienta son resultados, no excepciones.** Lanzar un error dentro de `run` produce `isError: true` con tu mensaje. El modelo lo ve y puede reintentar. No captures errores solo para devolver cadenas `"Error: ..."`. - **Los argumentos de los prompts son cadenas.** Usa `z.coerce.number()` para argumentos numéricos de prompts. - **El JSON Schema sale de la librería de esquemas.** Zod 4 y ArkType no necesitan nada. Valibot necesita `toStandardJsonSchema`. Los demás, `withJsonSchema`. - **`server.fetch` ignora la ruta.** Dirige tú `/mcp` hacia él, o usa `serve({ http })`, que solo responde en `/mcp`. - **El `server.call` tipado requiere encadenar.** `pico().tool().tool()` conserva los tipos. Las sentencias separadas los pierden. ## Contribuir con un agente El repositorio tiene un [`AGENTS.md`](https://github.com/picomcp/picomcp/blob/main/AGENTS.md) con las reglas para trabajar en picomcp: el presupuesto de tamaño, los comandos de test y las restricciones de diseño.