Skip to content

English · Español

Break 00 — Schema de tool call malformado: campo required ausente

🇪🇸 Rompemos el JSON-Schema del tool conjugate quitando el array required. El modelo emite tool calls con argumentos parciales ({"verb": "eat"} sin tense); el servidor MCP cae al executor con KeyError; el cliente del agente recibe un error genérico que no diagnostica la causa. La lección es por qué la validación tiene que vivir en el cliente.

Este ejercicio /break apunta a la estructura defensa en profundidad de los contratos agente-tool. El bug es una línea eliminada en un JSON-Schema; la superficie de fallo cruza tres capas — modelo, servidor MCP (Model Context Protocol), parser del agente.

Anchors: theory/01-function-calling-formats.md, theory/02-mcp-architecture.md, theory/05-mcp-wire-and-100-line-server.md, .claude/commands/break.md.


Hipótesis

El learner predice: "Si quito el campo required del schema de la tool, el LLM (Mini-GPT) — sin restricciones de schema-mask en cada campo — ocasionalmente emitirá tool calls que omiten argumentos. El despacho del servidor MCP los aceptará (son JSON válido), los pasará a la función de conjugación, que lanzará KeyError. El error se propaga de vuelta como código de error JSON-RPC -32603 (internal error) en lugar del correcto -32602 (invalid params). El parser del agente no sabe si reintentar o si rendirse."

El break

En src/minimcp/server.py:

 CONJUGATE_SCHEMA = {
     "type": "object",
-    "required": ["verb", "tense", "person"],
+    # /break: removed `required` — schema no longer enforces presence
     "properties": {
         "verb":   {"enum": ["work","play", ..., "write"]},
         "tense":  {"enum": ["infinitive", ..., "simple future"]},
         "person": {"enum": ["1st singular", "2nd singular", "3rd singular"]},
     },
 }

Una clave eliminada. El schema sigue siendo JSON-Schema válidorequired es opcional. Pero para la tool de conjugación, la omisión significa que un tool call con solo {"verb": "eat"} pasa la validación del schema.

Predice, luego ejecuta

El bucle del agente de la Fase 32 genera tool calls muestreando de Mini-GPT con el schema como restricción suave. Sin required, el modelo ocasionalmente trunca temprano ("{\"verb\": \"eat\"}"), especialmente cuando el prompt de gramática es ambiguo (p. ej., "conjugate eat for me").

Predicciones

  • Tool calls sin los tres args: 15-25% de las llamadas del agente (cuando temperatura > 0).
  • Respuesta del servidor MCP:
  • Si la función de conjugación usa args["tense"], lanza KeyError.
  • El handler de excepciones por defecto devuelve código JSON-RPC -32603 ("Internal error") — no -32602 ("Invalid params") — porque la validación ocurre dentro de la función, no en la frontera del schema.
  • Parser del agente: recibe -32603 y reintenta (porque -32603 es a veces transitorio). El retry tiene el mismo problema. El agente entra en bucle 3-5 veces antes de rendirse.
  • Tasa de fallo end-to-end: ~20% de los tool calls dan un error irrecuperable. La precisión global del tutor de gramática §A13 cae ~15 pp en la eval de 30 ejemplos.

Escribe tus predicciones en learners/borja/phase-31/notes/breaks.md antes de ejecutar.

Observa

Corre el round-trip de la Fase 31 con el schema roto:

just exp 31-mcp --variant broken-schema

Diagnósticos a graficar:

  1. Histograma de códigos de error JSON-RPC vistos sobre 100 tool calls: en el estado roto, -32603 debería aparecer en ~20% de las respuestas. En el baseline, ~0%.
  2. Log del servidor: el KeyError debería aparecer en el stderr del servidor (no en el cuerpo de respuesta JSON-RPC — que es el problema de pérdida diagnóstica).
  3. Log del agente: cuenta los retries por sesión. Roto: media ~2.5 retries por llamada. Baseline: ~0.05.

Síntoma que Borja verá

  • El log del servidor muestra trazas KeyError: 'tense'.
  • El log del cliente muestra código de error JSON-RPC -32603 ("Internal error") — el código equivocado para "el cliente pasó args malos".
  • El agente reintenta porque -32603 es convencionalmente reintentable.
  • La precisión end-to-end del tutor de gramática §A13 cae ~15 pp.

Causa oculta (una frase)

Quitar required del JSON-Schema hace que el schema acepte argumentos parciales; la validación que lo habría pillado en el lado servidor se ha ido, así que el fallo aflora profundo en la implementación de la tool como un KeyError, mapeado al código de error JSON-RPC equivocado.

Cascada de pistas

  1. Imprime el JSON del tool call del agente para 5 sesiones de muestra. ¿Están siempre presentes los tres args?
  2. Corre el servidor en modo verbose. ¿Qué código de error se devuelve para llamadas con args parciales?
  3. Compara tu schema de tool con theory/05-mcp-wire-and-100-line-server.md §"Un servidor MCP de ~100 líneas" — ¿qué campo falta?

Diff del fix

 CONJUGATE_SCHEMA = {
     "type": "object",
+    "required": ["verb", "tense", "person"],
     "properties": {
         ...
     },
 }

Y — defensa en profundidad — añade validación explícita en _handle antes del despacho:

def _handle(req: dict) -> dict | None:
    ...
    if method == "tools/call":
        name = req["params"]["name"]
        args = req["params"].get("arguments", {})
        if name not in _TOOLS:
            return _error(rid, -32601, f"Tool '{name}' not found")
        fn, schema = _TOOLS[name]
        # Validate args against schema BEFORE dispatch.
        try:
            jsonschema.validate(args, schema)
        except jsonschema.ValidationError as e:
            return _error(rid, -32602, f"Invalid params: {e.message}")
        ...

El fix son dos capas: schema correcto y validación explícita. Cualquiera por sí sola es frágil; las dos juntas producen el código de error correcto en la frontera correcta.

Por qué esto enseña el concepto

La lección es dónde ocurre la validación determina qué error ves. Un schema con required permite al servidor MCP rechazar args parciales en la frontera del protocolo (-32602). Sin required, el servidor pasa input inválido a la implementación de la tool, que falla con una excepción Python, que se convierte en un genérico -32603. El parser del agente no puede distinguir "envié input malformado" de "la red está inestable" — ambos parecen errores internos.

Defensa en profundidad significa: validar en cada frontera. El modelo emite un candidato; el agente parsea y valida la forma JSON; el cliente MCP valida contra el schema de la tool antes de enviar; el servidor MCP re-valida al recibir; la implementación de la tool aún puede lanzar si se violan invariantes. Cinco capas, cada una con el código de error correcto para el fallo correcto. Saltarse cualquiera — como quitar required — empuja el fallo a una capa que no tiene el contexto para diagnosticarlo.

Para el tutor de gramática §A13, esta es la diferencia entre un agente que tiene éxito el 95% y uno que se atasca al 65%. El break es pequeño; la consecuencia es la reputación del agente.

Referencias


Siguiente: restaura required y corre lab/02-mcp-roundtrip.md para confirmar que la distribución de códigos de error vuelve al baseline.