Eran las 2am y tenía un agente que llevaba 40 minutos procesando un flujo de reconciliación financiera. Todo verde en tests. Todo verde en staging. En producción, después de la llamada número diecisiete a una herramienta MCP, empezó a tomar decisiones basadas en datos que ya no existían en el sistema fuente.
No crasheó. No tiró una excepción. Simplemente siguió trabajando con un modelo del mundo que había quedado desactualizado tres herramientas atrás.
Tardé dos días en entender qué estaba pasando. Tardé otros tres en aceptar que el problema no era mi código.
MCP protocol gaps en agentes: la asunción silenciosa que nadie documenta
Hay un paper conceptual implícito en cómo MCP está diseñado: el contexto que le pasás a una herramienta en la llamada 1 sigue siendo válido cuando llegás a la llamada 17. El protocolo no tiene mecanismo nativo para expresar que el mundo cambió mientras el agente estaba trabajando.
Esto no es un bug de implementación. Es una decisión de diseño. Y tiene sentido en el 80% de los casos de uso para los que MCP fue pensado: herramientas de lectura, búsquedas, transformaciones de datos estáticos.
Pero mis agentes no viven en ese 80%.
Viven en sistemas donde:
- Un registro puede ser modificado por otro proceso mientras el agente lo está analizando
- El estado de una entidad cambia como side effect de la propia herramienta que el agente acaba de llamar
- Hay múltiples agentes corriendo en paralelo sobre el mismo dataset
En esos contextos, la asunción de contexto estacionario se convierte en una trampa silenciosa.
Los tres bugs que documenté (con código real)
Bug 1: El fantasma de la entidad eliminada
Este fue el primero. Tenía un agente que procesaba órdenes de compra. El flujo era:
-
get_pending_orders()— trae lista de órdenes pendientes - Para cada orden:
get_order_details(order_id)— trae detalle completo -
validate_order(order_id, validation_rules)— valida contra reglas de negocio -
approve_or_reject_order(order_id, decision)— ejecuta la decisión
// Lo que el agente hacía internamente — esto es pseudocódigo
// simplificado de cómo el LLM construía su plan
const orders = await mcp.call('get_pending_orders');
// orders = [{ id: 'ORD-001' }, { id: 'ORD-002' }, { id: 'ORD-003' }]
for (const order of orders) {
// Entre get_pending_orders y este punto, ORD-002 puede haber
// sido cancelada por otro proceso — MCP no sabe eso
const details = await mcp.call('get_order_details', { id: order.id });
const validation = await mcp.call('validate_order', {
id: order.id,
rules: details.applicable_rules
});
// Si ORD-002 fue cancelada después de get_order_details,
// approve_or_reject va a operar sobre una entidad que ya no existe
// en el estado que el agente cree que existe
await mcp.call('approve_or_reject_order', {
id: order.id,
decision: validation.recommendation
});
}
El problema: en staging el dataset era estático. En producción, otros usuarios estaban cancelando órdenes mientras el agente procesaba. El agente llamaba approve_or_reject_order con datos de validación calculados sobre una entidad que el sistema ya consideraba en otro estado.
No tiraba error porque el sistema aceptaba la operación (diseño defensivo del backend). Pero el resultado era lógicamente incorrecto.
Los tests pasaron porque nadie testea concurrencia real en el contexto MCP.
Bug 2: El side effect que el agente no vio
Este fue más sutil. Tenía una herramienta process_payment(invoice_id) que, como side effect, marcaba la factura como "en procesamiento" y le asignaba un lock temporal de 5 minutos.
// La herramienta MCP — definición del servidor
{
name: 'process_payment',
description: 'Procesa el pago de una factura por su ID',
inputSchema: {
type: 'object',
properties: {
invoice_id: { type: 'string' }
}
}
// PROBLEMA: la descripción no menciona el side effect
// MCP no tiene forma nativa de expresar que esta herramienta
// muta el estado de la entidad para llamadas subsiguientes
}
// Lo que el agente intentaba hacer después
// (en el mismo flujo, 3 herramientas más tarde)
const invoiceStatus = await mcp.call('get_invoice_status', {
id: invoice_id
});
// Devuelve: { status: 'processing', locked: true, locked_until: ... }
// El agente interpretaba 'processing' como un estado previo
// no relacionado con su propia acción de hace 3 llamadas
// y tomaba decisiones erróneas en consecuencia
El agente no tenía forma de saber que el estado "processing" era consecuencia directa de su propia llamada anterior. MCP no tiene un mecanismo para expresar "esta herramienta muta el estado y estas son las entidades afectadas".
El resultado: el agente interpretaba su propio side effect como evidencia de un problema externo y tomaba decisiones de retry que generaban loops.
Bug 3: El contexto que viajó entre sesiones
Este fue el más raro y el que más tardé en encontrar.
Tenía un agente con memoria persistente entre sesiones (usando un store externo). El agente guardaba referencias a IDs de entidades que había procesado. El problema: los IDs en el sistema fuente eran reutilizables después de cierto tiempo de inactividad.
// Sesión 1 — el agente guarda contexto
const memory = {
last_processed_batch: 'BATCH-2024-001',
processed_item_ids: ['ITEM-4521', 'ITEM-4522', 'ITEM-4523'],
processing_rules_version: 'v2.1'
};
await persistMemory(agentId, memory);
// Sesión 2 — 6 semanas después
// El agente recupera su contexto
const memory = await getMemory(agentId);
// memory.processed_item_ids sigue siendo ['ITEM-4521', 'ITEM-4522'...]
// PERO el sistema fuente reutilizó esos IDs para entidades nuevas
// MCP no tiene TTL de contexto. No tiene invalidación de referencias.
// El agente llama a la herramienta con IDs que ahora apuntan
// a entidades completamente diferentes
const itemDetails = await mcp.call('get_item_details', {
id: 'ITEM-4521'
});
// Devuelve datos de una entidad nueva que tiene el mismo ID
// El agente cree que está viendo algo que ya procesó
Este bug era especialmente difícil porque dependía de la combinación de tres factores: memoria persistente del agente, reutilización de IDs en el sistema fuente, y la asunción implícita de MCP de que las referencias son estables.
Los errores comunes cuando descubrís este gap
Error 1: Intentar resolver esto en el LLM.
Mi primer instinto fue agregar instrucciones en el system prompt: "siempre verificá el estado actual de una entidad antes de operar sobre ella". Funcionó para algunos casos. Creó overhead en todos. Y eventualmente el LLM encontraba rutas de razonamiento donde igual se saltaba la verificación porque "lógicamente parecía innecesaria".
El LLM no es el lugar correcto para resolver problemas de infraestructura de datos.
Error 2: Agregar versioning al contexto manualmente.
Intente serializar un "snapshot timestamp" en cada llamada MCP y compararlo en el servidor. Funcionó. También agregó complejidad de estado que básicamente reinventaba transacciones distribuidas, muy pobremente.
Error 3: Ignorarlo y agregar reintentos.
Esta fue la peor decisión. Los reintentos ocultaron el síntoma durante semanas hasta que el bug se manifestó en un contexto donde el retry hacía el problema más grande, no más chico.
Lo que funciona (parcialmente):
Modeling explícito de mutabilidad en las descripciones de herramientas. No es elegante, pero es honesto:
{
name: 'process_payment',
description: `
Procesa el pago de una factura.
EFECTOS DE ESTADO: Esta herramienta marca la factura como 'processing'
y aplica un lock de 5 minutos. Las llamadas subsiguientes a
get_invoice_status para esta factura reflejarán estos cambios.
VALIDEZ DE CONTEXTO: El resultado de esta herramienta asume que
el estado de la factura no cambió desde la última llamada a
get_invoice_details. Si el flujo tardó más de 2 minutos desde
esa llamada, re-verificar el estado antes de llamar esta herramienta.
`,
// ...
}
No es la solución. Es una muleta que documenta el problema hasta que el protocolo tenga una respuesta mejor.
Por qué esto importa más allá de MCP
Este problema no es exclusivo de MCP. Es un problema de cualquier sistema que expone herramientas con estado a agentes que operan en el tiempo.
Cuando escribí sobre el problema de confianza que Emacs resolvió y los agentes ignoran, estaba rozando el mismo tema: la confianza implícita en que el entorno se comporta consistentemente. MCP tiene el mismo problema en la dimensión temporal.
Y cuando discutí los cambios entre Claude Opus 4.6 y 4.7, una de las cosas que observé es que los cambios en el modelo también mutan el "contexto estacionario" que tus herramientas asumen. Un modelo que razona diferente sobre las descripciones de tus herramientas es otro vector de contexto mutante.
El patrón se repite: construimos sistemas asumiendo estabilidad en capas que no son estables.
FAQ: MCP protocol gaps en agentes reales
¿MCP tiene planes de agregar soporte para contexto mutable o versioning de estado?
Al momento de escribir esto, la spec de MCP no tiene mecanismos nativos para expresar mutabilidad de estado, TTL de referencias, o invalidación de contexto. Hay discusiones en el repo de Anthropic sobre extensiones del protocolo, pero nada concreto en el roadmap público. Es un problema conocido en la comunidad pero no está priorizado porque la mayoría de los casos de uso actuales son sobre datos relativamente estáticos.
¿Estos bugs se pueden detectar con tests unitarios de las herramientas?
No, y ese es exactamente el problema. Los tests unitarios de herramientas MCP prueban cada herramienta de forma aislada con contexto estático. Los bugs que describí emergen de la interacción temporal entre herramientas en flujos multi-step. Necesitás tests de integración que simulen concurrencia real y mutación de estado entre llamadas. La mayoría de los frameworks de testing para agentes no tienen buen soporte para esto todavía.
¿Estos problemas aplican igual a todos los LLMs o son específicos de cómo Claude razona sobre herramientas?
El problema es del protocolo, no del modelo. Pero los modelos diferentes tienen distintas tendencias a re-verificar estado vs. asumir continuidad. En mi experiencia, los modelos más grandes tienden a ser más conservadores y a re-verificar, mientras que los modelos más pequeños (más eficientes en tokens) tienden a asumir que el contexto previo sigue siendo válido. Esto hace que los bugs sean más frecuentes cuando optimizás por velocidad/costo y usás modelos más chicos.
¿Hay workarounds a nivel de servidor MCP que resuelvan esto sin modificar el protocolo?
Sí, pero todos tienen tradeoffs. El más robusto es implementar un middleware de contexto en el servidor MCP que trackee el estado de las entidades relevantes y inyecte warnings en las respuestas cuando detecta divergencia. Es trabajo extra y no es portable entre implementaciones. Otro approach es diseñar las herramientas como "snapshot-first": toda herramienta que lee estado devuelve un token de versión, y toda herramienta que escribe acepta ese token y falla si el estado cambió (estilo optimistic concurrency). Funciona bien, pero requiere que el sistema subyacente soporte ese patrón.
¿Cómo sabés si tu caso de uso está en el 80% seguro o en el 20% problemático?
Pregunta simple: ¿alguna entidad que tu agente procesa puede ser modificada por un proceso externo durante la ejecución del flujo? ¿Alguna herramienta tiene side effects sobre entidades que otras herramientas en el mismo flujo también leen? ¿Tu agente tiene memoria persistente con referencias a IDs de sistemas que reutilizan identificadores? Si respondiste sí a cualquiera de las tres, estás en el 20% y tenés que diseñar explícitamente para el problema.
¿No es esto básicamente el problema de transacciones distribuidas? ¿Por qué no usar las soluciones existentes?
Sí y no. Superficialmente se parece, pero el contexto es diferente: en transacciones distribuidas los participantes son sistemas deterministas que podés coordinar. Acá uno de los participantes es un LLM con razonamiento probabilístico. Las soluciones clásicas (two-phase commit, sagas, etc.) asumen que podés rollback de forma limpia. Con un agente que ya tomó decisiones basadas en contexto incorrecto, el "rollback" no es técnico, es semántico. Es mucho más complicado.
Lo que cambié en mi stack y lo que todavía no resolví
Después de documentar estos tres casos, hice tres cambios concretos:
Todas las herramientas que leen estado ahora devuelven un
context_versionopaco. Las herramientas que escriben lo aceptan como parámetro opcional y loguean divergencia si el estado cambió.Agregué un
context_ttlexplícito en las descripciones de herramientas. Le digo al LLM cuánto tiempo puede asumir que el contexto sigue siendo válido antes de re-verificar.Para agentes con memoria persistente, agregué hashing de las propiedades clave de las entidades referenciadas. Si el hash cambia entre sesiones, el agente recibe un warning explícito antes de operar.
Lo que todavía no resolví: concurrencia entre múltiples instancias del mismo agente. Si tenés dos instancias procesando el mismo dataset en paralelo, el problema de contexto mutante se multiplica. No encontré una solución elegante que no requiera coordinación centralizada, lo cual destruye buena parte del valor de tener agentes distribuidos.
MCP es un protocolo joven. Estos gaps son esperables. Lo que no es aceptable es no documentarlos, porque en producción los paga alguien — generalmente a las 2am, con un agente que sigue trabajando con un modelo del mundo que ya no existe.
Si estás construyendo con MCP en sistemas con estado mutable, revisá también cómo el contexto de configuración afecta a los agentes — hay otro vector de contexto silenciosamente estacionario que probablemente no estás testeando.
Y si encontraste otros gaps que yo no cubrí: me interesa saber. Este es un área donde la documentación colectiva vale más que cualquier post individual.
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)