FC-0042 · 12 ABR 2026 · técnico

MCP servers en producción: lo que aprendí shipeando tres

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.

PALABRAS 1.840
LECTURA ~8 MIN
CATEGORÍA técnico
TAGS MCP · Agentes IA · Producción · TypeScript

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. 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".

CapaLatency budgetError rate objetivoObservado 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

  1. Separar tools de "lectura" vs tools de "escritura" desde el día uno. Ruteo, reintentos, idempotencia y auditoría son distintos para cada familia.
  2. Un MCP server por bounded context, no un mega-server. La arquitectura hexagonal no es negociable cuando el número de herramientas cruza ~15.
  3. Tests de contract entre el server y el modelo. Grabar traces reales, replayarlos en CI, fallar si el schema cambia.
  4. No uses JSON Schema directo: usa Zod con un .describe() generoso en cada campo. El modelo lee los describes, no los types.

Tres MCP servers en producción. El primero validó que la idea servía. El tercero es lo que tengo que sostener cuando rompe a las 3am. La diferencia entre los dos no la explica ningún tutorial.

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

  1. Reportado en el repo oficial, abierto desde marzo 2026. Yo parché localmente.
REL — RELACIONADOS
FC-0041 · 28 MAR 2026
¿Vale la pena la arquitectura hexagonal para un CRM chico?
FC-0040 · 14 FEB 2026
"Ship" no es "done": el costo escondido de features en producción