
Contratos para MCP en Claude Code: integraciones que no revientan
TL;DR: Antes de conectar otro servidor MCP o CLI a Claude Code, define un contrato mínimo con cinco piezas: esquema de entrada validado, esquema de salida estable, errores tipados, idempotencia y límites explícitos. Sin ese contrato, el agente improvisa, los flujos largos se rompen a mitad de tarea y depurar se vuelve adivinar.
Por qué tus integraciones MCP fallan a mitad de flujo
El patrón se repite: añades un servidor MCP nuevo, las primeras tres llamadas funcionan y a la cuarta el agente recibe un JSON con un campo opcional cambiado, no sabe qué hacer y empieza a alucinar parámetros. Lo mismo pasa con CLIs envueltos en Bash: un día devuelven la tabla en stdout, al siguiente lo mezclan con stderr y la salida ya no es parseable.
El problema rara vez es el modelo. Es que la integración no tiene un contrato. Una herramienta sin contrato es como una API REST sin documentación: puede funcionar en happy path, pero cualquier desviación rompe el flujo. Y en sesiones largas con Claude Code, donde una sola tarea encadena 20 o 30 llamadas a tools, la probabilidad de desviación se acumula.
Aquí el contrato no es un documento Markdown decorativo. Es código ejecutable que el servidor MCP impone y que el agente puede leer en su tool definition. Cuando existe, el agente sabe qué pedir, qué esperar y cómo recuperarse. Cuando no existe, improvisa.
¿Qué es un contrato en una integración MCP o CLI?
Un contrato de integración es la especificación verificable de cómo una tool acepta entradas, devuelve salidas y comunica errores. Tiene tres propiedades: es estable entre versiones, es validable automáticamente y es autodescriptivo en la definición de la tool.
En MCP esto se traduce en un inputSchema JSON Schema en la declaración del tool, una estructura de respuesta documentada y códigos de error consistentes. En CLIs externos invocados desde Claude Code, se traduce en flags estables, formato de salida fijo (idealmente --json) y exit codes con significado.
La diferencia entre una integración con contrato y una sin contrato no se nota en la demo. Se nota en la sesión número 30 cuando el agente lleva 40 minutos en una tarea y de repente no sabe interpretar una salida que ha cambiado de formato. Este principio entronca con la separación de responsabilidades clásica: el contrato es la frontera donde la responsabilidad de validar pasa de un lado a otro.
Los 5 elementos del contrato mínimo
| Elemento | Qué define | Cómo se verifica |
|---|---|---|
| Esquema de entrada | Tipos, campos obligatorios y rangos | JSON Schema validado antes de ejecutar |
| Esquema de salida | Estructura estable que el modelo puede parsear | Tipado en código, tests de regresión |
| Errores tipados | Códigos discretos con causa accionable | Enum de errores documentado |
| Idempotencia | Qué llamadas se pueden repetir sin efectos | Marcado explícito en la descripción |
| Límites | Timeouts, tamaño máximo, rate limits | Enforced en el servidor, no solo documentado |
Cualquiera de los cinco que falte se convierte en el punto por donde el flujo se rompe. Y normalmente se rompe al sexto turno, no al primero, lo que dificulta atribuir la causa.
Implementación paso a paso de un MCP con contrato
Voy a mostrarlo con un servidor MCP en Python usando el SDK oficial. La tool busca tickets en un sistema interno y devuelve metadatos. Caso simple, pero suficiente para ver los cinco elementos en acción.
1. Define el esquema de entrada con validación estricta
Lo primero es declarar exactamente qué acepta la tool. Sin campos abiertos tipo extra: dict que invitan a alucinar.
# Esquema de entrada validado: el agente solo puede pasar query y status, nada mas.
from pydantic import BaseModel, Field
from typing import Literal
class SearchTicketsInput(BaseModel):
query: str = Field(min_length=2, max_length=200, description="Texto a buscar en titulo o descripcion")
status: Literal["open", "closed", "any"] = Field(default="any")
limit: int = Field(default=10, ge=1, le=50)
Los Literal y los rangos no son cosméticos. Convierten errores silenciosos del agente (pasar status="pending" porque le sonó bien) en errores de validación con mensaje claro.
2. Define el esquema de salida estable
El agente va a leer la respuesta turno a turno. Si los campos cambian o aparecen nulls inesperados, empieza a improvisar.
# Salida tipada: el agente sabe que campos esperar siempre, sin opcionales sorpresa.
class Ticket(BaseModel):
id: str
title: str
status: Literal["open", "closed"]
updated_at: str # ISO 8601, no datetime crudo
class SearchTicketsOutput(BaseModel):
results: list[Ticket]
total: int
truncated: bool
El campo truncated es importante: comunica al agente que hay más resultados sin mentir con un total. Esto evita que pida "todos" cuando ya devolviste el máximo.
3. Errores tipados, no excepciones genéricas
Si la búsqueda falla, el agente necesita saber por qué para decidir si reintentar, cambiar la query o abandonar.
# Errores discretos: cada codigo le dice al agente que hacer despues.
class TicketError(BaseModel):
code: Literal["AUTH_EXPIRED", "RATE_LIMITED", "INVALID_QUERY", "BACKEND_DOWN"]
message: str
retry_after_seconds: int | None = None
Un AUTH_EXPIRED le dice al agente que pida credenciales. Un RATE_LIMITED con retry_after_seconds le permite esperar y reintentar sin spammear. Un Exception: connection refused sin estructura, en cambio, lo deja a oscuras.
4. Marca idempotencia explícitamente en la descripción
En la tool definition del MCP, la descripción no es decorativa: el agente la usa para razonar.
# La descripcion comunica al agente que esta llamada es segura de reintentar.
TOOL_DESCRIPTION = """Busca tickets por texto. Idempotente: repetir la misma query devuelve el mismo resultado.
Usala libremente para refinar busquedas. NO modifica estado."""
Cuando una tool sí muta estado, marcarlo igual de explícito: "No idempotente: cada llamada crea un nuevo registro". El agente ajusta su estrategia de reintentos en consecuencia.
5. Aplica límites en el servidor, no solo en el prompt
Decirle al agente "no pidas más de 50 resultados" en CLAUDE.md es una sugerencia. Forzarlo en el esquema y en la lógica es un contrato. Si el agente envía limit=200, el servidor lo rechaza con un error tipado y el agente aprende en una iteración.
Wrapper para CLIs externos con el mismo contrato
Si tu integración es un CLI invocado vía Bash, el contrato vive en un script wrapper. El patrón es el mismo: validar entrada, normalizar salida a JSON, mapear exit codes a errores tipados.
# Wrapper que da contrato a un CLI legacy con salida inconsistente.
import subprocess
def run_legacy_tool(query: str) -> dict:
if len(query) < 2:
return {"data": None, "error": {"code": "INVALID_QUERY", "message": "query too short"}}
result = subprocess.run(["legacy-cli", "--query", query], capture_output=True, text=True, timeout=30)
if result.returncode == 0:
return {"data": parse_legacy_output(result.stdout), "error": None}
return {"data": None, "error": map_exit_code(result.returncode, result.stderr)}
Este patrón es lo que evita que Claude Code lea stdout mezclado con warnings y empiece a inventar campos. La salida siempre tiene la misma forma: {data, error}. El agente nunca tiene que adivinar.
En Producción
Coste por turno: cada tool definida en MCP consume tokens del contexto del agente, en algunos casos varios miles por turno. Definir contratos compactos (descripciones concisas, esquemas planos, sin campos opcionales innecesarios) reduce el impacto. Es un equilibrio: el contrato debe ser preciso pero no verboso.
Versionado: trata el contrato como API pública. Cambios incompatibles requieren versión nueva (search_tickets_v2) y migración planificada. Romper un esquema sin avisar deja agentes en producción haciendo llamadas que ya no funcionan.
Observabilidad: loguea cada llamada con el esquema validado, el resultado y el tiempo. Sin trazas, depurar por qué el agente eligió mal una tool en el turno 23 es imposible.
Defensa frente a alucinación: aunque la validación rechace entradas malformadas, el agente puede alucinar nombres de tools que no existen o flags inventadas. Esta defensa complementa los contratos cerrando ese vector.
Secretos en el contrato: nunca incluyas credenciales en los esquemas de entrada. El agente puede loguearlas o reenviarlas. Inyecta secretos en el servidor desde variables de entorno, fuera del flujo del modelo. La misma lógica del secret scanning en GitHub MCP aplica aquí.
Errores comunes y depuración
- Error: el agente pasa parámetros que no existen. Causa: el esquema acepta
additionalProperties: true. Solución: configurar el esquema en modo estricto, rechazando campos extra. - Error: la tool funciona aislada pero falla en flujos largos. Causa: salida no idempotente sin marcar como tal. Solución: documentar idempotencia y, si no lo es, exigir un
request_iden la entrada. - Error: el agente reintenta una llamada que falló por auth y agota el rate limit. Causa: error genérico tipo
Exception. Solución: devolverAUTH_EXPIREDtipado para que el agente no reintente. - Error: respuestas masivas saturan el contexto. Causa: sin límite de salida. Solución: paginar con
limitobligatorio y campotruncateden la respuesta. - Error: cambias el esquema en producción y los agentes activos rompen. Causa: contrato no versionado. Solución: tool con sufijo
_v2, deprecación gradual.
Preguntas Frecuentes
¿Necesito un contrato si solo uso MCPs oficiales?
Los MCPs oficiales (GitHub, Linear, Notion) ya traen contratos razonables. El problema aparece con servidores propios, wrappers de CLIs internos o forks rápidos. Ahí es donde el contrato te salva la sesión.
¿Cómo verifico que mi MCP cumple su propio contrato?
Tests con casos límite: entradas vacías, tamaños máximos, errores forzados. Pydantic o JSON Schema validators se integran bien en CI. La misma lógica de hooks en Claude Code sirve para validar contratos en cada cambio.
¿Pasar a JSON Schema estricto rompe agentes existentes?
Puede pasar si tu agente venía pasando campos extra. Despliega primero en modo permisivo logueando rechazos, ajusta los prompts del agente y después activa modo estricto. Migración en dos pasos, sin sorpresas.
Cierre
Un contrato bien definido convierte una integración frágil en infraestructura. Los cinco elementos (entrada validada, salida estable, errores tipados, idempotencia explícita, límites en el servidor) no son opcionales si quieres que Claude Code complete tareas largas sin improvisar. El esfuerzo inicial se paga en la primera sesión de cuatro horas que no se rompe.
La regla práctica que me ha funcionado: antes de añadir una tool nueva a CLAUDE.md, escribe primero el contrato y los tests. Si no puedes especificarlo, probablemente el agente tampoco podrá usarlo bien. La próxima entrega del blog cubrirá cómo combinar estos contratos con slash commands para crear flujos verificables de extremo a extremo.
¿Has tenido integraciones MCP que fallaban a mitad de flujo y resolviste con contratos más estrictos? Cuéntamelo en los comentarios o en Twitter @sergiomarquezp_.


