← SCRAM AI Lab
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

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.
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) |
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");
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.
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.
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.
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.
fetch sin AbortController con timeout de 5-10s te deja MCPs colgados y conversaciones muertas.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.
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?
Artículos relacionados
Claude Code en octubre: mods, control de modelos y 20 versiones en tres semanas
De 2.1.272 a 2.1.291: mods en TypeScript, sec-default para equipos, deniedModels, allowedProviders, AGENTS.md y Opus 5.5 por defecto. Qué configurar.
Cómo usamos Claude Code en SCRAM todos los días: el repo como memoria, skills propios y reglas que no se negocian
No usamos Claude Code para "generar código". Lo usamos para operar un sistema de 127 modelos de datos y 655 endpoints con un equipo chico. Así está montado: memoria en el repo, skills escritos desde el código real, hooks, y tres reglas que aprendimos a golpes en producción.
Claude Code en septiembre de 2026: MCP administrado, modo restringido, /skill-doctor y Fable 5.1 por defecto
Once versiones de Claude Code entre el 25 de agosto y el 9 de septiembre de 2026 (2.1.243 a 2.1.267): Fable 5.1 por defecto, managedMcpServers para toda la organización, --restricted, /skill-doctor, /diff, hooks de cambio de modelo y maxEffortLevel. Qué usar y qué configurar.