Construyendo Tu Primer Servidor MCP
El Model Context Protocol (MCP) es lo más parecido a un adaptador universal que existe en el ecosistema de IA. Define cómo los modelos de IA descubren y llaman herramientas, y cómo esas herramientas devuelven resultados estructurados. Una vez que tienes un servidor MCP funcionando, cualquier cliente compatible — Claude, Claude Code, o cualquier framework de agentes que hable MCP — puede usarlo sin código adicional de integración.
Esta guía recorre la construcción de un servidor MCP real desde cero. No un ejemplo de juguete, sino un patrón que puedes extender para uso en producción.
Qué Es MCP en Realidad
MCP es un protocolo JSON-RPC 2.0 sobre stdio (o HTTP+SSE para servidores remotos). El cliente y el servidor intercambian un pequeño conjunto de tipos de mensajes:
tools/list— el cliente pregunta qué herramientas están disponiblestools/call— el cliente invoca una herramienta con argumentosresources/list/resources/read— el cliente lee archivos, bases de datos, o cualquier contexto que el servidor exponga
El servidor declara sus herramientas de antemano con un JSON Schema para la entrada de cada herramienta. El cliente usa este esquema para construir llamadas válidas. Toda la interacción es sin estado desde la perspectiva del cliente — el servidor gestiona internamente cualquier estado necesario.
Esta simplicidad es el objetivo. MCP no es un framework. Es un contrato que te permite envolver cualquier cosa — una base de datos Postgres, un clúster Kubernetes, un repositorio GitHub, una API interna propietaria — y exponerla como un conjunto de funciones tipadas y llamables que cualquier modelo de IA puede usar.
Configuración
Necesitarás Node.js 18+ y un proyecto TypeScript. El SDK oficial de MCP minimiza el código repetitivo:
mkdir mcp-server && cd mcp-servernpm init -ynpm install @modelcontextprotocol/sdk zodnpm install -D typescript @types/node tsxAgrega un tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "outDir": "./dist" }, "include": ["src"]}Y un script de inicio en package.json:
{ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "start": "node dist/index.js" }}Construyendo el Servidor
Crea src/index.ts. La estructura siempre es la misma: inicializar el servidor, registrar herramientas, conectar al transporte stdio.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";
const server = new McpServer({ name: "my-mcp-server", version: "1.0.0",});Ahora registra herramientas. Cada herramienta tiene un nombre, una descripción que el modelo usa para decidir cuándo llamarla, y un esquema de entrada:
server.tool( "get_weather", "Obtener las condiciones meteorológicas actuales de una ciudad", { city: z.string().describe("El nombre de la ciudad para obtener el clima"), units: z.enum(["celsius", "fahrenheit"]).default("celsius"), }, async ({ city, units }) => { // Tu implementación real aquí const data = await fetchWeather(city, units); return { content: [ { type: "text", text: `Clima en ${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`, }, ], }; });Conecta el servidor al transporte stdio:
const transport = new StdioServerTransport();await server.connect(transport);Eso es el esqueleto completo del servidor. Todo lo demás es agregar herramientas.
Patrones de Diseño de Herramientas
La calidad de tu servidor MCP depende de qué tan bien diseñes las herramientas. Algunos patrones que importan:
Herramientas estrechas y componibles en lugar de monolitos amplios
Una herramienta que hace una sola cosa es más útil que una que hace muchas. El modelo puede combinar herramientas estrechas de formas inesperadas; una herramienta amplia lo limita.
Evita esto:
server.tool("manage_database", "Hacer cualquier cosa con la base de datos", { operation: z.enum(["read", "write", "delete", "schema"]), query: z.string(), // ... muchos parámetros opcionales})Prefiere esto:
server.tool("query_records", "Ejecutar una consulta SELECT y devolver filas como JSON", { table: z.string(), where: z.string().optional().describe("Cláusula SQL WHERE, ej. 'status = active'"), limit: z.number().default(50),})
server.tool("get_schema", "Devolver las definiciones de columnas de una tabla", { table: z.string(),})Describe las entradas con precisión
El modelo lee tus cadenas describe() para saber qué pasar. Trátelas como documentación para un desarrollador que nunca ha visto tu código:
{ date_range: z.string().describe( "Rango de fechas ISO 8601 en formato YYYY-MM-DD/YYYY-MM-DD. " + "Ejemplo: 2025-01-01/2025-03-31" ),}Las descripciones vagas producen entradas incorrectas. Las descripciones precisas producen entradas correctas.
Devuelve texto estructurado, no JSON crudo
Los modelos analizan texto de manera más confiable que los blobs de JSON crudo en los resultados de las herramientas. Formatea tu salida:
return { content: [ { type: "text", text: [ `Se encontraron ${rows.length} registros:`, ...rows.map(r => `- ${r.id}: ${r.name} (${r.status})`), ].join("\n"), }, ],};Manejo de errores
Devuelve errores como resultados de herramientas, no como excepciones lanzadas. Señala el fallo con isError: true para que el agente pueda decidir cómo recuperarse:
async ({ table, where }) => { try { const rows = await db.query(table, where); return { content: [{ type: "text", text: formatRows(rows) }] }; } catch (err) { return { content: [ { type: "text", text: `Consulta fallida: ${err instanceof Error ? err.message : "error desconocido"}. ` + `Verifica que el nombre de la tabla sea correcto y que la cláusula WHERE use nombres de columnas válidos.`, }, ], isError: true, }; }}Un Ejemplo Real: Servidor de Herramientas GitHub
Aquí hay una herramienta concreta que envuelve la API de GitHub para que un agente lea el contenido de los repositorios:
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
server.tool( "get_file_contents", "Leer el contenido de un archivo de un repositorio GitHub", { owner: z.string().describe("Propietario del repositorio (usuario u organización)"), repo: z.string().describe("Nombre del repositorio"), path: z.string().describe("Ruta del archivo dentro del repositorio"), ref: z.string().optional().describe("Rama, etiqueta o SHA de commit. Por defecto usa la rama principal."), }, async ({ owner, repo, path, ref }) => { try { const { data } = await octokit.rest.repos.getContent({ owner, repo, path, ref });
if (Array.isArray(data)) { const listing = data.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.name}`).join("\n"); return { content: [{ type: "text", text: `Listado de ${path}:\n${listing}` }], }; }
if (data.type !== "file" || !data.content) { return { content: [{ type: "text", text: `${path} no es un archivo legible` }], isError: true, }; }
const content = Buffer.from(data.content, "base64").toString("utf-8"); return { content: [{ type: "text", text: `Contenido de ${owner}/${repo}/${path}:\n\`\`\`\n${content}\n\`\`\`` }], }; } catch (err: unknown) { const message = err instanceof Error ? err.message : "Error desconocido"; return { content: [{ type: "text", text: `Error al leer ${path}: ${message}` }], isError: true, }; } });Conectando a Claude Desktop
Una vez que tu servidor está funcionando, conéctalo a Claude Desktop editando su configuración en ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) o %APPDATA%\Claude\claude_desktop_config.json (Windows):
{ "mcpServers": { "my-server": { "command": "node", "args": ["/ruta/absoluta/a/dist/index.js"], "env": { "GITHUB_TOKEN": "tu_token_aquí" } } }}Reinicia Claude Desktop. Tus herramientas aparecen automáticamente en el selector de herramientas.
Conectando a Claude Code
Los servidores MCP se conectan a Claude Code mediante la CLI:
claude mcp add my-server node /ruta/absoluta/a/dist/index.jsClaude Code usará automáticamente tus herramientas al trabajar en tareas que se beneficien de ellas.
Qué Construir a Continuación
Un servidor MCP es esencialmente un límite de capacidades: qué puede ver y hacer el agente. Algunas direcciones que vale la pena explorar:
- Servidores de bases de datos — Expone acceso de solo lectura a Postgres, SQLite o DynamoDB con herramientas de introspección de esquemas
- Servidores de análisis de código — Envuelve tree-sitter o un protocolo de servidor de lenguaje para dar a los agentes comprensión semántica de bases de código
- Servidores de monitoreo — Conecta sistemas de métricas (Prometheus, Datadog) para que los agentes investiguen incidentes con datos de observabilidad reales
- Servidores de documentación — Indexa y sirve documentación interna, permitiendo que los agentes respondan preguntas operativas
El ecosistema MCP todavía es temprano. La mayoría de los servidores útiles aún no se han construido. Construye el que resuelva un problema real en tu entorno, y el protocolo se encargará del resto.
Artículos Relacionados
- Introducción al Desarrollo Agéntico
- Patrones de Uso de Herramientas: Interfaces Agente-Herramienta Confiables
- Patrones Multi-Agente: Orquestadores, Workers y Pipelines