Las llamadas duplicadas a herramientas MCP necesitan una identidad de ejecución
Las llamadas duplicadas a herramientas MCP necesitan algo más que reintentos. Usa huellas de solicitud y registros de actividad para distinguir las reconexiones de las segundas acciones reales.

Una reconexión es un problema de transporte. Una segunda acción es un problema de ejecución. Los equipos sufren cuando tratan ambas cosas como si fueran iguales y dejan que el cliente reintente todas las solicitudes que no reciben una respuesta visible.
MCP facilita pasar esto por alto porque una llamada a una herramienta puede atravesar varios límites: un proceso de agente, un transporte MCP, una puerta de enlace de acciones y una API HTTP o un objetivo SSH. La conexión puede desaparecer después de que el objetivo haya aceptado el trabajo, pero antes de que el agente reciba el resultado. Si el sistema vuelve a enviar la llamada, el objetivo ve dos solicitudes válidas. No tiene ningún motivo para deducir que la segunda fue un accidente.
La solución no es aumentar el presupuesto de reintentos. Asigna una identidad de ejecución a cada acción solicitada, registra su ciclo de vida y decide los reintentos a partir de ese registro. Una huella de solicitud indica qué intentaba hacer el autor de la llamada. Un registro de actividad indica si el sistema ya la inició o completó. Necesitas ambas cosas.
Una reconexión no autoriza otra ejecución
Un cliente que se reconecta después de que se rompa el flujo tiene pruebas de que falló la comunicación. No tiene pruebas de que fallara la llamada original a la herramienta.
La diferencia parece obvia hasta que alguien añade un middleware genérico de reintentos bajo un cliente MCP. El middleware ve un tiempo de espera agotado, un reinicio de conexión o una respuesta ausente. No sabe si el POST era una lectura, una escritura, un comando remoto o una operación irreversible. Vuelve a enviar los bytes porque eso es lo que suele hacer el código de reintentos HTTP.
Para una lectura como GET /repos/acme/api/branches, puede ser tolerable. Para POST /payments, DELETE /projects/atlas o un comando SSH que cambia un host de producción, puede crear un segundo efecto secundario. La capa de herramientas no puede arreglarlo después devolviendo un único resultado al modelo.
La especificación de transporte Streamable HTTP de MCP permite explícitamente que los clientes reanuden la entrega de eventos del servidor al cliente con Last-Event-ID cuando se interrumpe un flujo. Es un mecanismo de recuperación para los mensajes de un flujo. No convierte una segunda solicitud JSON-RPC tools/call en la misma ejecución. La documentación del SDK de TypeScript también separa los tokens de reanudación de la ruta de solicitud y permite middleware del cliente alrededor de fetch. Precisamente por eso los equipos deben expresar la regla de reintentos en el código, en lugar de suponer que el transporte los protegerá.
Aplica esta regla:
Reanuda un flujo de respuestas cuando el protocolo lo admita. Vuelve a emitir una acción con efectos secundarios solo cuando la capa de acciones pueda identificarla como la misma ejecución.
Los casos difíciles no son los fallos limpios. Son aquellos en los que el servidor empieza a trabajar, la respuesta desaparece y el cliente no sabe si debe esperar, reanudar, consultar el estado o reintentar. El diseño debe hacer visible esa incertidumbre.
Los IDs JSON-RPC identifican mensajes, no acciones duraderas
Un ID de solicitud JSON-RPC sirve para relacionar una solicitud con su respuesta. No basta para deduplicar una acción entre reconexiones, reinicios del proceso o ejecuciones separadas de un agente.
Considera este par de llamadas:
{"jsonrpc":"2.0","id":41,"method":"tools/call","params":{"name":"deploy_release","arguments":{"service":"catalog","version":"2026.07.22"}}}
{"jsonrpc":"2.0","id":41,"method":"tools/call","params":{"name":"deploy_release","arguments":{"service":"catalog","version":"2026.07.22"}}}
Pueden ser la misma solicitud enviada dos veces después de perderse la conexión. También pueden proceder de dos procesos de cliente distintos que empiezan a numerar en 1 o 41. Incluso dentro de un solo proceso, un error de implementación puede reutilizar los IDs. El valor no te dice casi nada si no lo vinculas a un autor autenticado y a una sesión de protocolo concreta.
Ahora considera dos llamadas con IDs distintos:
{"jsonrpc":"2.0","id":41,"method":"tools/call","params":{"name":"deploy_release","arguments":{"service":"catalog","version":"2026.07.22"}}}
{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{"name":"deploy_release","arguments":{"service":"catalog","version":"2026.07.22"}}}
Podrían ser un reintento de transporte cuyo cliente asignó un ID nuevo. O el agente podría haber solicitado deliberadamente un segundo despliegue después de recibir un resultado incierto. Los IDs de mensaje son una señal, no la decisión.
No cometas el error contrario y deduplica para siempre todas las llamadas a herramientas que coincidan. Desplegar dos veces la misma versión puede ser inofensivo o incluso intencionado. Crear dos veces el mismo ticket externo puede ser incorrecto. Rotar dos veces una credencial puede bloquear un servicio. La clase de acción determina cuánto tiempo sigue siendo significativa una identidad de ejecución.
Un modelo práctico mantiene separados tres identificadores:
- ID de correlación del protocolo: el ID JSON-RPC y, cuando corresponda, el contexto de sesión o flujo MCP.
- ID de ejecución: un identificador creado por el servidor para un intento aceptado de realizar una acción de herramienta.
- Huella de intención: un resumen estable del efecto solicitado, usado para encontrar una ejecución anterior cuando cambia la correlación del protocolo.
Cuando los separas, los registros dejan de fingir que responden a una pregunta que no pueden contestar.
Una huella útil describe el efecto
Una huella de solicitud debe mantenerse igual cuando cambia la entrega y cambiar cuando cambia el efecto solicitado. No hagas un hash de los bytes JSON sin procesar y llames huella al resultado. El JSON sin procesar varía según el orden de las propiedades, los espacios, los valores predeterminados opcionales, los IDs de solicitud y los cambios de formato sin importancia.
Primero crea un registro de acción canónico. Para una acción HTTP, podría tener esta forma:
{
"actor": "signed-process:com.example.agent",
"tool": "deploy_release",
"channel": "http",
"target": "deploy-api.internal.example/releases",
"credential_ref": "deploy-service",
"method": "POST",
"arguments": {
"service": "catalog",
"version": "2026.07.22",
"region": "us-east-1"
},
"intent_scope": "run:5f8097"
}
Normaliza el orden de los campos, omite los que no tengan significado semántico y normaliza las equivalencias conocidas antes de calcular el resumen. Si region tiene como valor predeterminado us-east-1, materialízalo siempre u omítelo siempre cuando el objetivo vaya a proporcionar ese valor. Mezclar ambas opciones crea falsos negativos.
El campo actor importa. Dos procesos de agente autorizados distintos que envían argumentos idénticos pueden representar acciones previstas separadas. credential_ref también importa. Una solicitud hecha con una identidad de servicio no equivale necesariamente a la misma ruta y el mismo cuerpo enviados con otra identidad. Para SSH, incluye la identidad del host, la cuenta, el comando, el directorio de trabajo si afecta al comportamiento y una representación normalizada del comando cuando puedas producirla de forma segura.
Mantén los secretos fuera del registro canónico. Nunca incluyas tokens portador, claves privadas ni encabezados de autorización sin procesar en la entrada de una huella. Si un argumento contiene un secreto, sustitúyelo por una referencia interna protegida o calcula la huella con una construcción con clave, como HMAC. Un hash simple y sin sal de un secreto con poca entropía convierte tu almacén de auditoría en un oráculo para probar conjeturas.
La recomendación habitual de «haz un hash de la solicitud» es popular porque es breve. Es incorrecta para controlar acciones. Un hash solo demuestra que unos bytes se pasaron a una función. No indica si esos bytes representan al mismo actor, el mismo efecto sobre el objetivo o la misma ventana de reintento.
El registro de actividad necesita estados, no una sola línea
Un registro de actividad útil responde hasta dónde llegó la acción. Si solo registra éxito y fallo, una reconexión te dejará intentando adivinar justo cuando más necesitas una respuesta clara.
Registra al menos estas transiciones para cada ID de ejecución:
- Aceptada: la puerta de enlace validó la solicitud y asignó un ID de ejecución.
- Autorizada: la aprobación necesaria o la autorización de sesión permitió la acción.
- Enviada: la puerta de enlace entregó la acción al cliente HTTP o al asistente SSH.
- Resultado observado: llegó la respuesta del objetivo, el estado de salida o un fallo explícito de entrega.
- Resultado entregado: el agente recibió el resultado de la herramienta, si el transporte puede demostrarlo.
El cuarto y el quinto estado deben mantenerse separados. Un objetivo puede devolver HTTP 201 mientras la conexión con el cliente MCP se rompe antes de que este vea la respuesta. Marcar esa ejecución como fallida porque falló la entrega del resultado es mentir. Marcarla como completada da al código de recuperación algo útil: puede devolver o reconstruir el resultado conocido sin enviar otra solicitud.
Este es el formato de registro que quiero ver durante un incidente:
{
"execution_id": "act_01J4K8J7DX7V",
"fingerprint": "hmac-sha256:4a1e...d90c",
"tool": "deploy_release",
"actor": "signed-process:com.example.agent",
"target": "deploy-api.internal.example/releases",
"state": "completed_result_not_delivered",
"accepted_at": "2026-07-22T14:03:18Z",
"dispatched_at": "2026-07-22T14:03:19Z",
"completed_at": "2026-07-22T14:03:25Z",
"target_status": 201,
"result_reference": "result_01J4K8JFM2"
}
El registro no tiene que exponer la respuesta completa del objetivo a todos los operadores. Necesita suficientes detalles protegidos para que la puerta de enlace tome una decisión de recuperación y suficientes detalles legibles para que una persona entienda lo ocurrido.
El registro de actividad de Sallyport guarda llamadas individuales, mientras que su registro de sesiones guarda las ejecuciones de los agentes. Esa separación resulta útil en esta investigación: la ejecución indica qué proceso de agente existía y el registro de llamadas indica si una acción concreta hacia el mundo exterior cruzó el límite de envío. Su cadena de auditoría también puede verificarse sin conexión con sp audit verify, lo que ayuda a demostrar que el registro no se reescribió en silencio después de un incidente.
Trata los resultados desconocidos como un resultado separado
La mayoría de las acciones duplicadas empiezan con un sistema que solo tiene dos resultados: éxito y fallo. Las acciones en red necesitan un tercero: desconocido.
Desconocido no significa que el sistema no hiciera nada. Significa que el sistema no puede demostrar si el objetivo aceptó la acción. Un tiempo de espera agotado antes de que salgan bytes del proceso suele poder reintentarse sin peligro. Un tiempo de espera agotado después de que el cuerpo de una solicitud HTTP se haya entregado al sistema operativo no es el mismo evento. Una conexión SSH rota después de que el shell remoto haya iniciado un comando es aún peor, porque el comando remoto puede continuar después de que termine el proceso local.
Clasifica cada acción de herramienta antes de decidir cómo recuperarla:
| Tipo de acción | Ejemplo | Valor predeterminado después de un resultado desconocido |
|---|---|---|
| Solo lectura | Consultar el estado de una compilación | Reintentar con límites normales |
| Escritura idempotente | Establecer un recurso con nombre en un estado declarado | Reintentar usando la misma identidad de idempotencia |
| Escritura condicional | Actualizar solo si coincide la versión | Consultar el estado y reintentar solo si la condición sigue cumpliéndose |
| Acción irreversible | Enviar un pago, revocar acceso, rotar una credencial | Detenerse y solicitar revisión explícita |
| Comando remoto | Ejecutar una migración mediante SSH | Consultar una marca duradera o detenerse para revisión |
El verbo HTTP de una API no resuelve esta tabla. PUT suele describirse como idempotente, pero un endpoint mal diseñado puede enviar una notificación, activar una compilación o añadir un evento de auditoría cada vez que recibe la solicitud. POST puede repetirse sin peligro cuando la API respeta una clave de idempotencia. Revisa el contrato real del objetivo.
Para comandos remotos de larga duración, añade una marca duradera antes de ejecutar el trabajo. Un comando de migración puede crear un registro con un ID de ejecución, actualizarlo cuando empiece el trabajo y marcarlo como completado solo después de la validación. Al reconectarte, consulta esa marca antes de volver a enviar el comando. Sin una marca, «probablemente no se ejecutó» no es una estrategia de recuperación.
Relaciona los reintentos dentro de un ámbito de intención limitado
Una huella por sí sola relacionará demasiado trabajo legítimo. Limítala al periodo y al contexto en los que un reintento tenga sentido.
El ámbito más sencillo es una ejecución del agente. Si el mismo proceso firmado envía la misma acción mientras el primer resultado sigue sin resolverse, trata la segunda solicitud como posible reintento. Si otro proceso la envía horas después, considérala una intención nueva, salvo que la propia acción proporcione una clave de idempotencia duradera.
Una buena regla de coincidencia se parece a esta:
if prior.fingerprint == incoming.fingerprint
and prior.actor == incoming.actor
and prior.intent_scope == incoming.intent_scope
and prior.state in {accepted, authorized, dispatched, completed_result_not_delivered}:
recover_or_attach_to(prior.execution_id)
else:
create_new_execution()
recover_or_attach_to no debe devolver éxito a ciegas. Su comportamiento depende del estado anterior.
Si la ejecución anterior fue aceptada pero aún no se envió, la puerta de enlace puede continuarla. Si se envió y el resultado es desconocido, la puerta de enlace debe consultar el endpoint de estado, el mecanismo de idempotencia o la marca duradera del objetivo. Si se completó pero falló la entrega del resultado, debe devolver la referencia al resultado almacenado. Si la autorización la rechazó, debe devolver ese rechazo en lugar de crear una nueva ruta de aprobación a partir del mismo reintento ambiguo.
El ámbito debe corresponder a la acción. Una ventana de cinco minutos puede ser razonable para una solicitud API que agota el tiempo de espera. No basta para un despliegue de software que dura una hora. Una rotación de credenciales puede requerir una huella duradera hasta que puedas verificar qué credencial está activa. No uses un TTL global solo porque sea fácil de configurar. Aplica reglas de retención y recuperación específicas para cada acción.
La aprobación es una prueba, no un mecanismo de idempotencia
Una aprobación humana puede demostrar que un proceso tenía permiso para intentar una acción. No puede demostrar si un intento anterior ya ocurrió.
Esto importa en sistemas que solicitan aprobación para cada llamada sensible. Supón que un agente pide rotar una credencial de producción. Una persona lo aprueba. La puerta de enlace envía la solicitud y el cliente se desconecta. El agente se reconecta y genera la misma llamada a la herramienta. Volver a pedir aprobación presenta una elección engañosa. El operador ve una solicitud conocida y puede aprobarla, pero la pregunta que necesita responder es si la primera rotación terminó.
La aprobación por llamada sigue teniendo su lugar. Controla la autorización en el momento de uso. Mantenla separada de la gestión de duplicados:
- La autorización decide si el autor de la llamada actual puede iniciar una ejecución.
- La huella decide si una solicitud entrante corresponde a una ejecución existente.
- Los registros de actividad deciden si esa ejecución existente puede reanudarse, recuperarse o debe revisarse.
Cuando un reintento corresponde a una ejecución pendiente, muestra el registro de actividad original en lugar de presentar una aprobación nueva como si nada hubiera ocurrido. El revisor debe ver el objetivo, la primera hora de envío, el resultado conocido y el motivo por el que la puerta de enlace no volvió a enviar la acción.
Sallyport usa una secuencia fija de decisiones: un almacén bloqueado rechaza las acciones, un proceso de agente nuevo recibe autorización por sesión de forma predeterminada y las credenciales seleccionadas pueden exigir aprobación en cada uso. Esos controles responden a quién puede actuar. El registro de ejecución todavía debe responder si la acción ya cruzó el límite.
Las claves de idempotencia HTTP solo resuelven una parte del problema
Si una API ascendente acepta claves de idempotencia, úsalas. Envía un valor estable durante toda la vida de una ejecución, conserva la respuesta del objetivo y reutiliza ese valor solo al recuperar la misma ejecución.
Por ejemplo, la puerta de enlace puede crear un ID de ejecución antes del envío y asociarlo con el encabezado que espera la API:
POST /v1/releases HTTP/1.1
Host: deploy-api.internal.example
Idempotency-Key: act_01J4K8J7DX7V
Content-Type: application/json
{"service":"catalog","version":"2026.07.22","region":"us-east-1"}
La API debe definir qué hace cuando se repite ese encabezado. Lo mejor es devolver el resultado original para la misma solicitud semántica y rechazar una solicitud diferente que intente reutilizar el mismo valor. Si acepta en silencio un cuerpo cambiado con la misma clave, la puerta de enlace no puede deducir nada de forma segura a partir de una repetición.
No uses la propia huella como clave de idempotencia externa si puede persistir entre acciones intencionadas. Un ID de ejecución es único para un intento aceptado. La huella localiza un intento potencialmente relacionado. Cumplen funciones distintas.
La idempotencia HTTP tampoco sirve por sí sola para SSH. Necesitas un protocolo remoto. Un patrón seguro consiste en pasar un ID de ejecución generado a un script que escriba un registro de estado duradero en el host o en un almacén compartido y que se niegue a iniciar dos veces la misma operación. Si no puedes modificar el comando ni consultar una marca externa, clasifica el comando como irreversible y exige una revisión después de una desconexión incierta.
Investiga la secuencia, no el recuento final
Dos filas de actividad con argumentos coincidentes no demuestran que haya un duplicado. Empieza por la secuencia de eventos y sigue la primera llamada hasta su límite de envío.
Una investigación real debe responder estas preguntas en orden:
- ¿Uno o dos procesos distintos de agente enviaron las llamadas?
- ¿La primera llamada recibió autorización y entró en la fase de envío?
- ¿La puerta de enlace recibió una respuesta o un estado de salida del objetivo?
- ¿Falló la entrega del resultado después de que el objetivo terminara?
- ¿La segunda llamada reutilizó el ID de ejecución original, llevaba una clave de idempotencia o creó un intento nuevo?
Este orden evita una conclusión errónea habitual: «Los registros muestran dos llamadas, así que el agente actuó dos veces». Puedes descubrir que la puerta de enlace registró una ejecución completada y un reintento del cliente que se vinculó a ella. O puedes encontrar dos procesos autorizados distintos, cada uno con un contexto de planificación diferente, que emitieron la acción. Cada caso necesita una solución distinta.
Mantén el almacén de actividad como un registro de solo anexado o hazlo resistente a manipulaciones de otra forma. Las investigaciones de duplicados suelen ocurrir después de un incidente costoso, cuando alguien quiere una historia más limpia de la que el sistema puede respaldar. Un registro encadenado mediante hashes no vuelve correcta la decisión original, pero dificulta manipular la reconstrucción posterior.
Tampoco ocultes la ambigüedad al agente. Devuelve un resultado que indique que la ejecución anterior está pendiente de verificación o que se completó pero la entrega del resultado se interrumpió. Un modelo que ve un fallo inventado intentará de nuevo. Un modelo que ve un estado incierto claro puede consultar el estado, pedir una revisión o elegir una ruta más segura.
Haz que el comportamiento ante repeticiones forme parte del contrato de cada herramienta
Cada herramienta con efectos secundarios necesita una respuesta explícita a una pregunta: ¿qué ocurre cuando el autor de la llamada pierde la respuesta después del envío?
Escribe la respuesta junto a la definición de la herramienta. Indica si la acción es de solo lectura, repetible con una identidad de idempotencia, recuperable mediante una consulta de estado o bloqueada después de un resultado desconocido. Especifica qué debe incluir su huella y durante cuánto tiempo una ejecución sin terminar puede seguir aceptando solicitudes vinculadas. Si nadie puede escribirlo, la herramienta no está lista para un uso autónomo.
El trabajo de ingeniería suele ser modesto comparado con la limpieza posterior a un despliegue duplicado, una cuenta duplicada, un pago duplicado o una segunda rotación de credenciales. Añade el ID de ejecución antes de llamar al objetivo. Conserva las transiciones de estado antes y después del envío. Guarda una referencia al resultado. Después haz que el código de reconexión consulte ese registro antes de volver a tocar el mundo exterior.
Ese es el estándar que debes mantener: un transporte interrumpido puede cortar una conversación, pero no debe convertir silenciosamente la incertidumbre en una segunda acción.
FAQ
¿Qué se considera una llamada duplicada a una herramienta MCP?
Es un efecto secundario repetido causado por dos ejecuciones que llegan al objetivo, no simplemente dos mensajes en un registro. Repetir una solicitud HTTP que solo lee datos puede resultar molesto; repetir una solicitud que envía dinero, elimina una rama, rota una credencial o crea una cuenta requiere otra respuesta. Empieza por identificar el efecto posterior antes de discutir si el cliente «pretendía» reintentar.
¿Una reconexión de MCP puede hacer que la misma llamada a una herramienta se ejecute dos veces?
A veces. El cliente puede perder la respuesta después de que el servidor ya haya completado la llamada y, al reconectarse, enviar de nuevo la misma solicitud. Pero dos llamadas de aspecto idéntico también pueden proceder de un agente que reconsideró su plan, de un supervisor que reinició un proceso de trabajo o de una persona que emitió la instrucción dos veces.
¿Basta el ID de solicitud JSON-RPC para deduplicar?
No. Un ID de solicitud JSON-RPC identifica un mensaje dentro de una conversación de protocolo, pero no es una identidad de ejecución duradera entre reinicios del cliente o conexiones nuevas. Trátalo como una señal útil y combínalo con una huella de solicitud y un registro de actividad.
¿Qué debe incluir una huella de solicitud MCP?
Incluye el proceso o principal autorizado, el nombre de la herramienta, los argumentos normalizados, la identidad del objetivo, la identidad de la credencial y un ámbito temporal o de intención limitado. No incluyas ruido específico del transporte, como el número de socket, un ID temporal de evento SSE o un ID de solicitud generado al azar. La huella debe describir el efecto solicitado, no la ruta usada para entregarlo.
¿Debo reintentar automáticamente una llamada fallida a una herramienta?
Evita volver a ejecutar automáticamente cualquier acción con un efecto externo, salvo que el objetivo admita una clave de idempotencia o puedas demostrar que el primer intento no comenzó. Reconectar un flujo de respuestas no es lo mismo que reenviar una invocación de herramienta. Lo más seguro suele ser recuperar el resultado o consultar primero el registro de actividad.
¿Qué debe mostrar un registro de actividad durante una investigación de reintentos?
Debe registrar la huella de la solicitud, el intento de ejecución, el contexto de autorización, las marcas de tiempo, el objetivo y el estado del resultado. Tiene que distinguir entre «recibida», «iniciada», «completada» y «falló la entrega del resultado». Si esos estados se reducen a una sola línea de registro, el operador no puede saber si ocurrió una segunda acción.
¿Cuándo debe enviar un agente una clave de idempotencia?
Usa una clave de idempotencia cuando la API receptora la admita y consérvala durante un reintento de transporte de la misma acción prevista. No la reutilices para una acción posterior intencionada, aunque los argumentos coincidan. Si la API no tiene un mecanismo de idempotencia, tu puerta de enlace necesita sus propios registros de ejecuciones pendientes y completadas.
¿Cómo puedo distinguir un reintento de una segunda acción intencionada?
Una reconexión suele cambiar las señales de conexión: la hora de inicio del proceso, la sesión de transporte, el ID de solicitud o el estado del flujo. Una repetición intencionada suele tener un contexto de planificación nuevo, una decisión de autorización nueva, argumentos modificados o un intervalo significativo después del resultado anterior. Ninguna señal por sí sola resuelve el caso, por eso el sistema necesita una regla de decisión registrada.
¿Puedo guardar solo un hash de cada solicitud a una herramienta?
No. Un resumen criptográfico solo sirve como índice. Conserva una representación canónica protegida o suficientes campos estructurados para explicar por qué dos llamadas coincidieron, limitando los valores sensibles que aparecen para los operadores. Si las huellas abarcan secretos, créalas con una clave secreta para que quien lea el registro no pueda probar conjeturas contra el resumen.
¿Qué debe ocurrir cuando se desconoce el resultado de la primera llamada?
Trátalo como una ejecución ambigua y detén la repetición automática para esa clase de acción. Muestra el registro de actividad anterior, el resultado conocido y el efecto exacto sobre el objetivo a la persona que debe aprobarlo. Una duplicación rápida puede costar menos que una duplicación irreversible, pero sigue siendo un defecto de diseño que conviene corregir.