Todos los recursos

El servidor MCP más pequeño que de verdad funciona: una sola herramienta, todo el código en TypeScript listo para copiar y pegar, cómo correrlo y cómo conectarlo a Claude Code. Cada pieza explicada, con lo básico de seguridad para que una herramienta real no termine haciendo algo irreversible.

Construye tu propio servidor MCP, paso a paso

En resumen

  • Vas a construir un servidor MCP funcional por stdio con exactamente una herramienta, «add_numbers», en unas 40 líneas de TypeScript.
  • El SDK oficial se encarga de toda la tubería del protocolo; tú solo escribes el nombre de la herramienta, su esquema de entrada y qué hace.
  • Córrelo primero con el MCP Inspector para comprobar que funciona antes de que Claude lo vea. Depurar así es muchísimo más fácil.
  • Conéctalo a Claude Code con una sola línea de «claude mcp add» y confírmalo con «/mcp».
  • La seguridad va metida en la construcción, no es un agregado al final: valida las entradas, devuelve los errores como datos y nunca dejes que una herramienta llegue más lejos de lo que debería.

Un servidor MCP suena más pesado de lo que es. Quítale la jerga y es un programa pequeño que dice "aquí tienes unas herramientas, esta es la forma de sus entradas y esto pasa cuando las llamas". El Model Context Protocol no es más que la manera acordada en que ese programa le habla a un cliente como Claude Code. El protocolo no lo implementas tú (lo hace el SDK oficial) así que el código que de verdad escribes es mínimo. Esta guía construye el servidor más pequeño que funciona de verdad: una herramienta que suma dos números. Es matemática aburrida a propósito, para que nada te distraiga de las piezas que importan. Una vez que veas la forma completa de punta a punta, cambiarla por una herramienta real (consultar una base de datos, llamar a una API, leer un archivo) es el mismo esqueleto con otro cuerpo.

01 · Qué estás construyendo en realidad

Tres piezas, y solo tres. Un objeto server que anuncia quién es. Una herramienta registrada en ese servidor: un nombre, una descripción que Claude lee para decidir cuándo llamarla, un esquema de entrada que dice qué argumentos son válidos y una función que se ejecuta cuando se llama la herramienta. Y un transport que conecta el servidor con el cliente. Vamos a usar stdio (entrada y salida estándar) lo que significa que Claude Code arranca tu servidor como un proceso hijo y se hablan por los mismos canales que usa una terminal. Sin puertos, sin URLs, sin red. Es el transport más simple y el más sensato por defecto para una herramienta local.

El cambio mental clave: no estás escribiendo código que llame a Claude. Estás escribiendo código que espera a que Claude lo llame. Tu servidor arranca, anuncia sus herramientas y luego se queda tranquilo hasta que el cliente le pide hacer algo. Esa inversión es toda la idea. El modelo decide cuándo tu herramienta es útil, y tu trabajo es solo hacerla confiable para cuando se dispare.

Nota

La descripción de una herramienta no es documentación para humanos. Es el prompt que Claude usa para decidir si la llama. Escríbela como si le estuvieras explicando a un colega que sabe lo que hace: qué hace la herramienta, cuándo usarla y qué devuelve. Las descripciones vagas son la razón número uno de que una herramienta que funciona perfecto nunca se llegue a llamar.

02 · Prepara el proyecto

Necesitas Node.js 18 o más reciente. Crea un directorio limpio, inicialízalo e instala las dos dependencias: el SDK de MCP y zod, una librería de esquemas que el SDK usa para validar las entradas.

mkdir add-server && cd add-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

Aquí, tsx nos deja correr TypeScript directo, sin un paso de build aparte, y eso mantiene el ciclo rápido mientras iteras. Agrega una config mínima de TypeScript para que el estilo de módulos moderno del SDK resuelva sin enredos:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Guarda eso como tsconfig.json. Una cosa más: abre package.json y agrega la línea "type": "module" en el nivel raíz, porque el SDK viene como módulos ES. Y eso es todo el setup. Sin pipeline de build, sin framework.

Consejo

Mantén este directorio pequeño y desechable mientras aprendes. La idea es ver el servidor completo en un solo archivo que puedas leer de arriba abajo, no montar un proyecto de producción. Ya después lo gradúas, cuando tengas la forma bien clara.

03 · El servidor completo, en un solo archivo

Crea server.ts con esto. Es el programa completo. Léelo una vez y luego repasamos cada línea.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 1. Crea el servidor y dile al cliente quién es.
const server = new McpServer({
  name: "add-server",
  version: "1.0.0",
});

// 2. Registra una herramienta: nombre, descripción, esquema de entrada, handler.
server.tool(
  "add_numbers",
  "Suma dos números y devuelve el resultado. Úsala cada vez que el usuario pida el total de dos valores.",
  {
    a: z.number().describe("El primer número a sumar"),
    b: z.number().describe("El segundo número a sumar"),
  },
  async ({ a, b }) => {
    const sum = a + b;
    return {
      content: [{ type: "text", text: String(sum) }],
    };
  }
);

// 3. Conecta por stdio y ponte a escuchar.
const transport = new StdioServerTransport();
await server.connect(transport);

Eso es todo. Cuarenta líneas contando comentarios, y la mayoría es la definición de la herramienta. Ahora el repaso.

El objeto server

new McpServer({ name, version }) crea el servidor y le da una identidad. El name es lo que aparece en la lista de /mcp de Claude Code, y la version es solo metadata, pero ponla con honestidad, porque tu yo del futuro va a querer saber qué build está corriendo.

La herramienta

server.tool(...) recibe cuatro argumentos, y cada uno se gana su lugar:

  1. El nombre, "add_numbers", es el identificador estable que llama el modelo. Mantenlo en minúsculas con guiones bajos y que describa la acción.
  2. La descripción es el string más importante del archivo. Claude la lee para decidir cuándo usar la herramienta. Fíjate que dice tanto qué hace como cuándo echar mano de ella. Esto es prompt engineering, no un comentario de código.
  3. El esquema de entrada es un objeto plano de validadores de zod. Cada z.number() dice "este argumento tiene que ser un número"; el .describe() de cada uno le explica al modelo qué significa ese argumento. El SDK lo convierte en el JSON Schema que el protocolo espera y, esto es clave, rechaza las llamadas que no cuadran antes de que tu handler llegue a correr.
  4. El handler es tu lógica de verdad. Recibe los argumentos ya validados y devuelve un arreglo content. El tipo text es el más simple: un string que el modelo lee de vuelta como resultado de la herramienta. Devolver String(sum) en vez del número crudo es a propósito, porque los resultados de las herramientas viajan como texto.

El transport y connect

StdioServerTransport conecta el servidor a la entrada/salida estándar, y server.connect(transport) arranca el ciclo. A partir de aquí el programa no hace nada por su cuenta. Espera a que el cliente le mande peticiones y las responde. Esa actitud pasiva es la correcta. El servidor no tiene opinión sobre cuándo se ejecuta; la tiene el cliente.

Importante

El handler es donde vive cada decisión de seguridad, porque es el único código que corre con efecto real. Para «add_numbers» el riesgo es cero. Es aritmética. Para una herramienta real, este es justo el lugar donde validas con más fuerza, revisas permisos y rechazas cualquier cosa fuera del alcance previsto. Volvemos a esto en la sección 06; tenlo presente mientras lees el handler ahora.

04 · Comprueba que funciona antes de que Claude lo vea

No conectes esto a Claude primero. Pruébalo aislado, porque depurar un servidor dentro de un ciclo de agente es una tortura. No logras saber si el problema es tu herramienta, el registro o que el modelo decidió no llamarla. El MCP Inspector es una interfaz web pequeña hecha exactamente para esto: arranca tu servidor y te deja llamar herramientas a mano.

npx @modelcontextprotocol/inspector npx tsx server.ts

Esto arranca tu servidor (npx tsx server.ts) dentro del Inspector y abre una pestaña en el navegador. En la interfaz, dale a conectar, entra a la lista de herramientas y deberías ver add_numbers con sus dos argumentos. Pon a = 2 y b = 3, ejecútala, y deberías recibir de vuelta exactamente esto:

5

Si ves el 5, tu servidor está bien: el protocolo, el esquema y el handler funcionan. Si la herramienta no aparece en la lista, el servidor no arrancó: corre npx tsx server.ts por su cuenta y lee el error (una dependencia que falta o una errata en una ruta de import suelen ser lo de siempre). Arréglalo aquí, en el Inspector, donde el ciclo de feedback dura segundos. Solo avanza cuando una llamada manual devuelva la respuesta correcta.

Consejo

Cada servidor MCP que construyo lo pruebo en el Inspector antes de que toque un agente. Es el mismo instinto de probar una función antes de meterla en un pipeline: aísla la unidad, confirma que se porta bien y luego integra. Saltarte este paso es así como terminas culpando al modelo por una errata.

05 · Conéctalo a Claude Code

Ahora que la herramienta funciona aislada, registrarla en Claude Code es una sola línea. Desde dentro del directorio de tu proyecto, apunta Claude Code al comando de arranque de tu servidor:

claude mcp add add-server -- npx tsx /ruta/absoluta/a/server.ts

Usa la ruta absoluta a server.ts, porque Claude Code puede arrancar el servidor desde un directorio de trabajo distinto al que tú estás parado, y una ruta relativa va a fallar sin avisar. Luego reinicia Claude Code y corre el comando slash /mcp. Deberías ver add-server en la lista como conectado, exponiendo add_numbers.

Ahora pídele a Claude algo que necesite la herramienta, en lenguaje natural:

¿Cuánto es 128 más 947? Usa la herramienta add_numbers.

La primera vez que la herramienta se dispare, Claude Code puede pedirte que la apruebes. Ese es el control de permisos, no un bug. Apruébala, y la respuesta vuelve desde tu handler corriendo código real, no del modelo calculando de cabeza. Acabas de darle a Claude una capacidad que no tenía hace diez minutos.

Si la herramienta nunca se dispara, revísalo en orden: corre /mcp para confirmar que el servidor de verdad esté conectado (un servidor sin conectar es la causa más común), verifica que la ruta sea absoluta y vuelve a leer la descripción de tu herramienta. Si es vaga, el modelo puede no darse cuenta de que la herramienta aplica. El arreglo casi nunca está en el prompt que escribiste; está en el registro o en la descripción.

06 · Lo básico de seguridad que no es opcional

Sumar dos números es inofensivo. En el instante en que tu herramienta toque algo real, la cosa cambia, así que cultiva estos hábitos mientras lo que está en juego es cero.

  • Deja que el esquema sea la primera línea de defensa. El esquema de zod rechaza las llamadas mal formadas antes de que tu handler corra. Mientras más preciso el esquema, menos basura le llega a tu lógica. Prefiere z.number() antes que z.string() y parsear; prefiere un enum antes que un campo de texto libre cuando ya conoces los valores.
  • Devuelve los errores como datos, no como crash. Si algo sale mal dentro del handler, no lances una excepción que tumbe todo. Devuelve un payload de content que explique la falla y activa la bandera de error para que el modelo sepa que la llamada falló. Un servidor que se cae tira toda la conexión; un error devuelto le permite al modelo recuperarse o reportarlo limpio.
  • Acota la herramienta a exactamente lo que necesita. Una herramienta que lee archivos debería estar amarrada a un solo directorio, no a todo el disco. Una herramienta de base de datos debería arrancar en read-only. El handler es la frontera: lo que él pueda alcanzar, el modelo lo alcanza a través de él. Trata ese alcance como el permiso real que es.
  • Nunca registres ni devuelvas secretos. Si tu herramienta usa una credencial, léela de una variable de entorno y mantenla fuera de la respuesta y de los logs. Esta es la misma disciplina detrás de Infuse, el gestor de secretos que construyo: la credencial existe en tiempo de ejecución y en ningún lugar donde pueda filtrarse.
  • Pon difíciles las acciones destructivas. Las lecturas perdonan; las escrituras y los borrados no. Si una herramienta puede modificar datos reales, diséñala para que sea confirmable e idealmente idempotente, así una llamada reintentada o mal leída no puede hacer daño dos veces sin que nadie se entere.

Aquí está el patrón de error-como-dato, que es justo el que la mayoría se salta:

async ({ a, b }) => {
  if (!Number.isFinite(a) || !Number.isFinite(b)) {
    return {
      content: [{ type: "text", text: "Ambas entradas deben ser números finitos." }],
      isError: true,
    };
  }
  return { content: [{ type: "text", text: String(a + b) }] };
}

La bandera isError: true le dice al cliente que la llamada falló sin desarmar la conexión, y el mensaje le da al modelo algo sobre lo cual actuar. Para aritmética esto es exagerado, pero la memoria muscular importa, porque la primera vez que escribas una herramienta que borra una fila, vas a querer tenerla lista.

Acabas de construir un servidor MCP real, lo probaste aislado, lo conectaste a Claude Code y viste la forma de seguridad que escala desde "suma dos números" hasta "actúa en producción". El esqueleto nunca cambia: un servidor se anuncia, registra herramientas con nombres, descripciones y esquemas claros, y un handler hace el trabajo detrás de una frontera que tú controlas. Cambia la aritmética por una capacidad real y el resto de la estructura se traslada tal cual. El ejemplo más pequeño que funciona es toda la lección. Todo lo más grande es solo un handler mejor.

Puntos clave

  • Un servidor MCP son solo tres piezas: un servidor que se anuncia, herramientas con nombre, descripción y esquema, y un transport, mientras del protocolo se encarga el SDK.
  • La descripción de la herramienta es prompt engineering, no un comentario de código; una vaga es justo la razón de que una herramienta que funciona nunca se llegue a llamar.
  • Prueba en el MCP Inspector antes de conectarlo a Claude: aislar la unidad hace que depurar tome segundos en vez de quedarte adivinando.
  • Regístralo con una sola línea de «claude mcp add» usando una ruta absoluta, reinicia y verifica con «/mcp».
  • Mete la seguridad desde el inicio: valida con el esquema, acota el alcance del handler, mantén los secretos fuera de las respuestas y devuelve los errores como datos.

Preguntas frecuentes

¿Necesito conocer los detalles del protocolo MCP para construir un servidor?

No. El SDK oficial implementa el protocolo (el formato de los mensajes, la negociación de esquemas, el manejo de las llamadas) así que nunca lo tocas directamente. Tu trabajo son las tres piezas de la guía: crear el servidor, registrar herramientas con nombre, descripción y esquema, y conectar un transport. Si sabes escribir una función que devuelva un valor, sabes escribir una herramienta MCP.

¿Cuál es la diferencia entre el transport stdio y el HTTP, y cuál debo elegir?

Un servidor stdio es un proceso local que el cliente arranca y con el que habla por entrada/salida estándar, sin puertos ni red, ideal para una herramienta que corre en tu propia máquina. Un servidor HTTP es un endpoint remoto al que llegas por URL, útil para servicios alojados o compartidos con el equipo. Para aprender y para cualquier herramienta local, empieza con stdio: es más simple y tiene menos piezas que se te puedan configurar mal. Pásate a HTTP solo cuando de verdad necesites un servidor que viva en otro lado que no sea la máquina del cliente.

Mi herramienta aparece en el Inspector pero Claude nunca la llama. ¿Por qué?

Si funciona en el Inspector, el servidor está bien. El problema es una de dos cosas. O el servidor no está conectado en Claude Code (corre «/mcp» para revisar), o la descripción de la herramienta es demasiado vaga para que el modelo sepa cuándo aplica. Reescribe la descripción para que diga claramente qué hace la herramienta y cuándo usarla. Ese string es la única señal que tiene el modelo sobre si tu herramienta viene al caso.

¿Por qué usar zod en vez de leer los argumentos directamente?

Dos razones. Primero, el SDK convierte tu esquema de zod en el JSON Schema que el protocolo anuncia, así el modelo sabe exactamente qué argumentos espera tu herramienta. Segundo, zod valida las llamadas entrantes y rechaza las mal formadas antes de que tu handler corra, y eso es seguridad de entrada gratis. Leer los argumentos a mano significa escribir esa validación tú mismo y exponer al modelo a una herramienta que se cae con entradas malas. El esquema hace trabajo de verdad, no es puro adorno.

¿Cómo convierto esto en una herramienta real que haga algo útil?

Mantén exactamente el mismo esqueleto y cambia solo el handler. En vez de «a + b», consulta una base de datos, llama a una API o lee un archivo, y luego devuelve el resultado como content de texto. Actualiza el nombre, la descripción y el esquema para que cuadren con la nueva herramienta, y aplica las reglas de seguridad de la sección 06: valida las entradas, acota el alcance, lee los secretos del entorno y devuelve los errores como datos. La estructura que acabas de construir se traslada sin cambios; lo único que crece es el cuerpo del handler.

¿El mismo servidor funciona en otros clientes MCP, no solo en Claude Code?

Sí, de eso se trata todo el estándar. Un servidor que habla MCP se conecta a cualquier cliente compatible con MCP de la misma forma. El servidor que construiste aquí funciona en Claude Code, en la app de escritorio de Claude, en el Inspector y en cualquier otro cliente que implemente el protocolo, sin cambiarle nada a tu código. Escribes la herramienta una sola vez y es portable entre clientes.

¿Prefieres que lo hagamos por ti?

Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.

Escríbenos por WhatsApp

Escríbenos por WhatsApp

Escanéalo con tu teléfono para escribirnos por WhatsApp.

Escanéalo con tu teléfono para escribirnos por WhatsApp.

¿Estás desde el teléfono y no puedes escanear? Escríbenos a info@ilustrari.com

Primera conversación gratis. Te responde el fundador.

Recursos relacionados