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: más errores de los que preví, y más preguntas sin respuesta de las que quería admitir.
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 uno con su SDK y su forma de fallar. Un CRM que habla con tres modelos diferentes tenía el triple del trabajo sin el triple del resultado.
MCP estandariza la conversación entre cliente y herramienta, no entre cliente y modelo. El cliente puede cambiar de modelo sin cambiar el server.
// 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 usa Zod, el runtime es Bun y el transporte es SSE sobre HTTPS.
Fallos 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 solución: un orquestador con idempotency keys derivadas del call_id del modelo y un circuit breaker por herramienta que corta antes del timeout del modelo.\
2. Reintentos en cadena
Un endpoint downstream devolvía 500 con Retry-After: 30. El server MCP respetaba el header; el modelo reintentaba de inmediato cinco veces con exponential backoff de 200ms. En tres segundos agotaba el rate limit del endpoint.
3. Observabilidad
Los logs del modelo y del server describen procesos distintos. Cada uno registra el tiempo desde su propio punto de vista.
| 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% |
La fila de herramientas concentra el error que llega al usuario. Ese dato guía la revisión semanal.
Cambios para el próximo server
- Separar tools de lectura y escritura desde el primer commit. Ruteo, reintentos, idempotencia y auditoría cambian entre ambas familias.
- Crear un MCP server por bounded context. La arquitectura hexagonal ayuda cuando el número de herramientas supera unas 15.
- Escribir tests de contrato entre el server y el modelo. Grabar traces reales, reproducirlos en CI y fallar si cambia el schema.
- Usar Zod con
.describe()en cada campo. El modelo recibe esas descripciones junto con el schema.
Con el primer server confirmé el caso de uso. El tercero requiere runbooks, límites y métricas para sostenerlo cuando falla de madrugada.
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.