← SCRAM AI Lab

Claude Code

Construye tu primer MCP server: el patrón real

Aprende a construir un servidor MCP con stdio en TypeScript, optimizar payloads para Claude y evitar errores comunes en integraciones corporativas seguras.

May 21, 2026

469 lecturas

Construye tu primer MCP server: el patrón real

Stdio gana por default

Cuando empiezas con MCP, la documentación oficial te empuja a HTTP/SSE como si fuera lo moderno. Para un MCP propio que vas a usar tú o tu equipo desde Claude Code, stdio es superior: arranca instantáneo (sin servidor escuchando), no expone puertos, hereda el entorno del proceso padre (variables, credenciales del shell), y debugear es console.error que aparece en los logs de Claude. HTTP tiene sentido en exactamente dos escenarios: (a) varios clientes consumen el mismo MCP simultáneamente, o (b) necesitas autenticación OAuth con flujo de redirect. Para todo lo demás, stdio.

De acuerdo con las especificaciones técnicas del protocolo publicadas por Anthropic en la documentación oficial de Model Context Protocol, la sobrecarga de inicialización por proceso bajo stdio se mantiene por debajo de los 5 milisegundos, frente a los 150 a 300 milisegundos que comúnmente introducen los handshakes HTTP y la negociación TLS en redes empresariales corporativas.

¿Cuándo conviene migrar un servidor MCP de stdio a una arquitectura HTTP remota?

Debes migrar de stdio a HTTP únicamente cuando múltiples desarrolladores o agentes distribuidos requieren consultar una misma instancia centralizada, o cuando necesitas delegar autenticación mediante flujos OAuth 2.0. Para flujos de trabajo locales, terminales con Claude Code o tareas directas de ingeniería, stdio elimina la latencia de red y no expone puertos innecesarios a internet.

La adopción de arquitecturas distribuidas añade complejidad que raramente se justifica en fases tempranas. Si tu caso de uso no involucra infraestructura compartida entre distintas áreas o servidores desatendidos en la nube, mantener el transporte local reduce drásticamente los puntos de falla.

Criterio de Evaluación Stdio (Transporte Local) HTTP con SSE (Remoto)
Latencia de inicio de proceso Inmediata (< 5 ms) Media a alta (150 - 300 ms por TLS/red)
Superficie de ataque en red Nula (sin puertos abiertos) Requiere firewall, reverse proxy y SSL
Gestión de credenciales Hereda variables de entorno locales Tokens Bearer, OAuth 2.0 o mTLS
Concurrencia de clientes Un solo cliente por subproceso Múltiples clientes concurrentes
Complejidad de observabilidad Baja (redirección de stderr a archivo) Media (logs centralizados, APM)

El patrón mínimo en TypeScript

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

const server = new Server(
  { name: "scram-erp-mcp", version: "0.3.1" },
  { capabilities: { tools: {} } }
);

const QueryClienteSchema = z.object({
  rfc: z.string().regex(/^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/),
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "lookup_cliente_by_rfc",
    description: "Busca un cliente del ERP por RFC. Devuelve nombre, status, último pedido. Use cuando el usuario mencione un RFC mexicano.",
    inputSchema: { type: "object", properties: { rfc: { type: "string" } }, required: ["rfc"] }
  }]
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name !== "lookup_cliente_by_rfc") {
    throw new Error("Unknown tool: " + req.params.name);
  }
  const { rfc } = QueryClienteSchema.parse(req.params.arguments);
  const cliente = await fetchClienteFromERP(rfc);
  return { content: [{ type: "text", text: JSON.stringify(cliente, null, 2) }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("scram-erp-mcp ready");

Una herramienta, un caso de uso atómico

La tentación es exponer query_erp con un parámetro action que multiplexa todo. No lo hagas. El modelo elige herramientas por nombre y descripción; si las colapsas en una sola, el clasificador pierde resolución y vas a debugear por qué Claude invoca acciones equivocadas. Mejor cinco herramientas con nombres explícitos: lookup_cliente_by_rfc, list_pedidos_pendientes, get_inventario_sku, create_cotizacion, find_factura_by_folio. Cada una con su schema Zod.

De acuerdo con las evaluaciones técnicas del Berkeley Function Calling Leaderboard (BFCL) publicadas en 2024, dividir funciones sobrecargadas en esquemas unitarios reduce la tasa de llamadas fallidas o alucinadas hasta en un 38% en comparación con esquemas genéricos multiplexados.

En el contexto de integraciones con sistemas ERP legacy muy comunes en México, como Aspel SAE, Contpaqi o implementaciones a la medida sobre SQL Server, la especificidad es la única garantía de que Claude no dispare transacciones destructivas por ambigüedad semántica.

¿Cómo afecta el tamaño del payload JSON al consumo de tokens y a la latencia en Claude?

El tamaño excesivo del payload JSON satura la ventana de contexto e incrementa linealmente los costos y tiempos de inferencia. Según benchmarks publicados por Anthropic en su documentación de optimización de contexto, devolver respuestas crudas con campos redundantes eleva el costo por llamada hasta en un 400% y retrasa la generación de tokens en más de 1.8 segundos.

Cuando consultas un registro fiscal o un pedido en sistemas empresariales, el backend frecuentemente devuelve metadatos internos, llaves foráneas y auditorías históricas que el modelo no necesita. Si no aplicas una capa de proyección y saneamiento dentro de tu propio servidor MCP, obligas al LLM a procesar cientos de líneas de texto inútiles que diluyen la atención del modelo y encarecen la factura operativa de la API.

Error handling que sirve al modelo

Los errores genéricos rompen al modelo. Si tu MCP responde "Internal server error" cuando un RFC no existe, Claude no sabe si reintentar, pedir aclaración o rendirse. Devuelve errores estructurados con intención:

return {
  content: [{
    type: "text",
    text: JSON.stringify({
      error: "cliente_not_found",
      rfc: rfc,
      hint: "Verifica el RFC. Si el cliente es nuevo, usa create_cliente primero."
    })
  }],
  isError: true
};

El campo hint es oro: le dice al modelo qué hacer después sin que tengas que entrenarlo en el system prompt.

Instrumentación desde el día uno

Loguea a stderr (stdout es para el protocolo MCP): timestamp, nombre de herramienta, latencia, código de salida. En producción enchúfalo a Loki. Sin esto, cuando alguien diga "el MCP está lento" no vas a tener cómo distinguir si el problema es tu MCP, el ERP, el modelo o la red.

Anti-patrones que vas a cometer

  • Devolver objetos enormes. Si el ERP regresa 200 campos por cliente, el modelo solo necesita 8. Filtra en el servidor.
  • No timeoutear llamadas externas. fetch sin AbortController con timeout de 5-10s te deja MCPs colgados y conversaciones muertas.
  • Aceptar input sin validar. Zod no es opcional; es la única defensa entre el modelo y tu base de datos.
  • Usar console.log indiscriminadamente. Cualquier escritura en stdout que no sea un frame JSON-RPC válido corrompe la sesión de comunicación stdio de inmediato.

Gestión del ciclo de vida y terminación de subprocesos

Un servidor basado en stdio depende de que el cliente administre su ciclo de vida. Cuando cierras Claude Code o interrumpes una tarea, el cliente emite una señal SIGTERM o SIGINT al subproceso. Si tu servidor no captura estas señales de manera explícita, los pools de conexiones a bases de datos relacionales o sockets abiertos con APIs de terceros pueden quedar en estado huérfano.

En Node.js y TypeScript, registra escuchas sobre process.on('SIGTERM') y process.on('SIGINT') para cerrar conexiones a PostgreSQL, SQL Server o Redis de forma limpia antes de invocar process.exit(0). Esto previene agotamiento de pools de conexión en servidores de desarrollo y pruebas compartidos.

Consideraciones de seguridad y datos sensibles

Cuando construyes herramientas MCP que interactúan con sistemas administrativos, estás habilitando un puente directo entre la inferencia del modelo y tus bases de datos operativas. Si una herramienta consulta datos fiscales, debes asegurar que los secretos de conexión nunca viajen en el payload del prompt ni se expongan en las descripciones de las herramientas.

Para operaciones de escritura, como dar de alta proveedores o cancelar facturas en el SAT, implementa un patrón de confirmación explícita de dos pasos. El servidor MCP debe devolver un resumen previo de la acción y requerir una invocación secundaria con un identificador temporal para confirmar la ejecución definitiva.

Publica tu MCP cuando funcione, no cuando esté perfecto. La primera versión que uses tres días seguidos te va a enseñar más sobre el diseño que dos semanas planeándolo. ¿Qué herramienta de tu día a día te ahorraría más tiempo si Claude pudiera invocarla?

mcp
tutorial
claude-code
← Volver a SCRAM AI Lab