TL;DR — MCP (Model Context Protocol) es una interfaz estable entre LLMs y herramientas. Resuelve un problema real. Pero el stack de producción es más ruidoso que los ejemplos del sitio oficial. Esto es lo que funcionó, lo que rompió, y lo que cambiaría.
El primer MCP server que deployé en producción servía un CRM conversacional. Tres herramientas: lookup_contact, log_interaction, schedule_followup. Un prompt de 400 tokens. Un modelo Haiku. 4× el benchmark humano de conversión en dos semanas.
El tercero tiene 47 herramientas, rutea entre tres modelos según complejidad, y corre detrás de una cola Redis con BullMQ. El salto de complejidad no fue lineal — fue logarítmico en los errores y exponencial en las cosas que no me había preguntado antes.
El problema real que MCP resuelve
Antes de MCP, integrar herramientas a un LLM era un trabajo de plomería. Function calling de OpenAI, tool use de Anthropic, plugins de Gemini — cada vendor con su SDK, su formato de schemas, sus convenciones de error. Un CRM que habla con tres modelos diferentes tenía tres adapters, tres tipos de tests, tres formas de fallar.
MCP estandariza la conversación entre cliente y herramienta, no entre cliente y modelo. Eso importa. El modelo sigue siendo reemplazable; el server se queda.
// server.ts — un MCP server en Hono + Bun
import { Hono } from 'hono';
import { createMCPServer } from '@modelcontextprotocol/sdk';
const server = createMCPServer({
name: 'crm-agent',
version: '2.1.0',
});
server.tool('lookup_contact', {
description: 'Busca un contacto por email, teléfono o ID',
input: z.object({
query: z.string(),
fields: z.array(z.string()).optional(),
}),
handler: async ({ query, fields }) => {
const contact = await db.contacts.findFirst({ query });
return { content: contact, fields };
},
});
El schema es Zod, el runtime es Bun, el transporte es SSE sobre HTTPS. Nada exótico.
Qué rompió en producción
1. Timeouts de herramientas vs timeouts del modelo
El modelo tiene 60s para responder. La herramienta tiene 30s para devolver. Las dos colas de espera no están coordinadas. Resultado: el modelo reintenta con otra herramienta antes de que la primera devuelva, y terminás con side-effects duplicados.
La fix: un orquestador con idempotency keys derivadas del call_id del modelo, y un circuit breaker por herramienta que trunca antes del timeout del modelo.\
2. El retry explosion
Un bug sutil: un endpoint downstream estaba devolviendo 500 con Retry-After: 30. El server MCP respetaba el header. El modelo no — hacía retry inmediato, 5 veces, con exponential backoff de 200ms. En 3 segundos se comían el rate limit del endpoint.
3. Observabilidad
No podés mirar logs del modelo y logs del server como si fueran el mismo sistema. Son dos procesos distintos con dos ideas distintas de "tiempo".
| Capa | Latency budget | Error rate objetivo | Observado producción |
|---|---|---|---|
| Modelo | < 2.5s | < 0.5% | 1.2% |
| MCP server | < 400ms | < 0.1% | 0.3% |
| Herramientas | < 250ms | < 0.05% | 0.08% |
Esa tabla es el KPI semanal. El número que importa es la fila de abajo — los errores de herramienta son los que el usuario siente.
Lo que cambiaría si empezara hoy
- Separar tools de "lectura" vs tools de "escritura" desde el día uno. Ruteo, reintentos, idempotencia y auditoría son distintos para cada familia.
- Un MCP server por bounded context, no un mega-server. La arquitectura hexagonal no es negociable cuando el número de herramientas cruza ~15.
- Tests de contract entre el server y el modelo. Grabar traces reales, replayarlos en CI, fallar si el schema cambia.
- No uses JSON Schema directo — usa Zod con un
.describe()generoso en cada campo. El modelo lee los describes, no los types.
Footnote técnico
El MCP SDK oficial en TypeScript tiene un bug conocido en streaming responses cuando el modelo hace parallel tool calls con más de 4 herramientas simultáneas ¹. Workaround: wrappear el dispatcher con Promise.allSettled y tu propio collector.
Notas
- Reportado en el repo oficial, abierto desde marzo 2026. Yo parché localmente.