En MCP, un error de ejecución de una tool y un error de protocolo usan canales diferentes. Si una tool recibió una llamada válida, pero no pudo encontrar un registro, consultar una API o aplicar una regla de negocio, devuelve un resultado con isError: true y un mensaje que el modelo pueda usar. Si la solicitud JSON-RPC está mal formada o el estado del protocolo no puede atenderla, devuelve un error de protocolo al host.

La decisión se reduce a una pregunta: ¿quién puede corregir el problema? El modelo puede ajustar argumentos, elegir otra tool o explicar que un registro no existe. El host debe reparar un método, una versión del protocolo, un mensaje JSON-RPC o la configuración de la conexión. Mezclar los canales hace que el agente repita algo que el modelo no puede arreglar, o convierte un fallo recuperable de una tool en un error genérico del host.

Este artículo amplía el servidor MCP en TypeScript para agentes de código. Ese tutorial explica cómo registrar y conectar tools. Esta página se concentra en el contrato de fallo después de que la llamada llega al servidor.

Diagrama de una llamada de tool MCP que se divide entre un resultado isError visible para el modelo y un error JSON-RPC visible para el host.

Ideas principales

  • Usa isError: true para fallos de ejecución que el modelo puede entender y corregir.
  • Usa un error JSON-RPC cuando el problema está en la solicitud o en el estado del protocolo.
  • Escribe el mensaje de isError como una observación breve: qué falló y cuál es el siguiente paso válido.
  • Mantén fuera del texto para el modelo los stack traces, tokens, argumentos sensibles e identificadores internos.
  • Prueba la visibilidad: un fallo de una tool debe ser un resultado y un fallo de protocolo debe llegar al host como error.

isError y los errores de protocolo llegan a consumidores distintos

La especificación de tratamiento de errores de Model Context Protocol separa dos familias. Los errores de protocolo usan el campo error de una respuesta JSON-RPC para problemas como solicitudes mal formadas, métodos desconocidos y fallos del servidor. Los errores de ejecución aparecen dentro del resultado de la tool con isError: true; el mensaje puede ayudar al modelo a corregir sus parámetros o cambiar de estrategia.

Fallo Canal Quién debe actuar Siguiente paso habitual
Una fecha tiene un formato incorrecto result.isError: true Modelo y runtime Corregir el argumento y reintentar dentro del presupuesto
No existe un registro result.isError: true Modelo Elegir otro identificador o explicar la ausencia
Una API externa rechazó la operación result.isError: true Runtime y modelo Clasificar el retry, usar una alternativa o detenerse
JSON-RPC inválido error en JSON-RPC Host MCP Reparar el mensaje o el cliente
Método o versión no compatible error en JSON-RPC Host MCP Ajustar la negociación o la configuración
El servidor no puede atender solicitudes Error JSON-RPC o política del host Host y operación Recuperar el servidor antes de continuar

Esta tabla describe el contrato en el wire. Los SDK pueden normalizar algunos caminos de otra manera. La guía de errores del SDK TypeScript de MCP muestra, por ejemplo, que una excepción dentro de un handler de tool se convierte en un resultado isError: true, mientras que callbacks de recursos y prompts pueden lanzar ProtocolError para producir una respuesta JSON-RPC. Lee el comportamiento de la versión instalada y prueba la forma que realmente recibe tu cliente.

El detalle importante es la visibilidad. Un resultado con isError: true sigue siendo una respuesta válida a una llamada de tool. El modelo recibe el texto y puede tomar la siguiente decisión. Un error JSON-RPC no tiene result; el fallo lo recibe el cliente MCP o el host que coordina la sesión.

¿Cómo escribir un mensaje de error que el agente pueda usar?

Un mensaje dirigido al modelo debe contener el hecho observable y la corrección permitida. No necesita toda la excepción. La documentación del SDK usa un registro inexistente e incluye identificadores conocidos para que el modelo elija un valor válido. El mensaje funciona porque describe un cambio que la siguiente llamada puede hacer.

Prefiere esta forma:

No note with id "drafts". Use one of: welcome, release-notes.

Evita estas dos:

Error: ENOTFOUND db.internal:5432 at QueryRunner.execute (query-runner.ts:81)
The operation failed. Please try again.

El primero expone infraestructura sin decir si un retry puede ayudar. El segundo no ofrece una observación nueva al modelo. Un mensaje útil responde en pocas palabras a cuatro preguntas: qué tool falló, qué condición se observó, qué puede cambiar en la siguiente llamada y cuándo debe detenerse el agente.

El texto del error sigue siendo contenido no estructurado. Una issue abierta del repositorio de MCP sobre errores de tools gobernados por schema analiza el riesgo de que stack traces, identificadores internos y datos personales crucen esta frontera. La issue es una discusión de implementadores, no una prueba de que todos los servidores filtren datos. La respuesta práctica sigue siendo clara: redacta el texto antes de devolverlo y trátalo como contexto del modelo.

¿Cómo devolver un error de tool en TypeScript?

La forma pequeña de abajo adapta el ejemplo actual del SDK TypeScript. El tipo evita deliberadamente un import del SDK para que el contrato de respuesta quede visible. Los nombres de los métodos de registro cambian entre versiones.

type TextContent = {
  type: "text";
  text: string;
};

type ToolResult = {
  content: TextContent[];
  isError?: true;
};

const notes = new Map([
  ["welcome", "Read tools.md first."],
  ["release-notes", "The latest release is staged."],
]);

export async function readNote(id: string): Promise<ToolResult> {
  const note = notes.get(id);

  if (!note) {
    return {
      content: [{
        type: "text",
        text: `No note with id "${id}". Choose another id.`,
      }],
      isError: true,
    };
  }

  return { content: [{ type: "text", text: note }] };
}

En el servidor real, conecta este resultado con el método de registro de tools del SDK instalado. Un fallo conocido de una tool también puede lanzar la excepción de tool que el SDK convierte en isError: true, pero devolver el resultado de forma explícita te da control sobre el texto y deja visible la política en el código. No supongas que lanzar cualquier ProtocolError dentro de un handler de tool producirá un error JSON-RPC: la página de errores del SDK TypeScript indica que los handlers de tools usan el canal de resultados.

Valida los argumentos antes del efecto externo. Si id debe ser una cadena, el schema de entrada debe rechazar otros tipos. Si la cadena es válida pero ningún registro coincide, la ejecución llegó a la tool y puede devolver una observación con isError: true. Esta diferencia separa un problema de validación del transporte de una ausencia de dominio.

¿Cuándo es correcto usar un error JSON-RPC?

Usa el canal de protocolo cuando el mensaje no representa una solicitud válida de ejecución de una tool o cuando el host debe reparar la sesión. Algunos ejemplos son JSON inválido, un método no compatible, un mensaje que no cumple el schema de tools/call y una versión de protocolo que el servidor no admite.

Una respuesta de protocolo tiene esta forma conceptual:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32602,
    "message": "Invalid tool call request"
  }
}

El código de error debe seguir el contrato de la versión y de la biblioteca. No uses -32602 como etiqueta universal para cada fallo de la aplicación. Una regla de negocio como “una orden no se puede cancelar después de enviarse” pertenece al resultado de la tool, aunque el runtime use internamente una excepción para producirlo.

La guía de errores del SDK Python de MCP muestra la misma distinción en otro lenguaje: una excepción de tool aparece como is_error=True y el modelo lee el mensaje, mientras que una excepción de MCP produce un error JSON-RPC que maneja el cliente. La implementación cambia, pero la pregunta es la misma: ¿falló la tool elegida por el modelo o el host envió una solicitud de protocolo inválida?

¿Cómo probar que el agente recibe el fallo correcto?

Una prueba de errores no debe comprobar solo que ocurrió alguna excepción. Debe demostrar la forma que observa el consumidor. La fixture siguiente no abre una conexión MCP; fija el contrato local para que la capa de integración pueda probarse contra el SDK real.

import assert from "node:assert/strict";

const result = await readNote("missing");

assert.equal(result.isError, true);
assert.match(result.content[0].text, /Choose another id/);
assert.equal("error" in result, false);

const protocolFailure = {
  jsonrpc: "2.0",
  id: 7,
  error: { code: -32602, message: "Invalid tool call request" },
};

assert.equal("result" in protocolFailure, false);
assert.equal(protocolFailure.error.code, -32602);

En una suite que llame al servidor MCP real, añade cuatro comprobaciones:

  1. Llama a una tool con un argumento de dominio válido que no exista y confirma isError: true.
  2. Confirma que el texto no contiene stack trace, ruta absoluta, token ni payload secreto.
  3. Envía una solicitud JSON-RPC mal formada y confirma que el cliente recibe un error, no un CallToolResult.
  4. Provoca un fallo conocido del upstream y confirma que no ocurrió ninguna mutación antes del resultado de error.

Si el cliente convierte un caso en isError: true cuando esperabas un rechazo JSON-RPC, no cambies la prueba para ocultar la diferencia. Comprueba la versión del SDK, el camino del handler y el wire log. La guía de pruebas de contrato para servidores MCP en TypeScript cubre conexión y schemas; añade este caso como una extensión enfocada en la semántica del fallo.

¿Cómo evitar un retry infinito después de isError?

isError: true hace visible el fallo para el modelo, pero no decide si debe intentarlo otra vez. El runtime aún necesita distinguir entre corregir argumentos, reintentar una dependencia, cambiar de tool y detenerse. Un error de dominio puede corregirse una vez. El mismo fingerprint repetido varias veces es una razón para detenerse o pedir intervención.

No devuelvas un mensaje que anime a reintentar una operación que quizá ya produjo un efecto. Si se perdió la respuesta de una escritura, consulta el estado externo o usa una clave de idempotencia antes de presentar la acción como repetible. El resultado de error de MCP debe describir lo observado, no inventar que la mutación no ocurrió.

Tampoco uses isError como control de acceso. La autorización, las allowlists, la confirmación humana y el alcance del servidor deben bloquear el efecto antes o dentro de la política del servicio. El canal de error informa de la decisión, pero no convierte una tool peligrosa en una tool segura.

Checklist para tratar errores de MCP

  • ¿La llamada llegó como una solicitud válida de tools/call?
  • ¿El fallo ocurrió durante la ejecución de la tool o antes?
  • ¿El modelo puede corregir la entrada, elegir otra tool o usar la observación?
  • ¿El fallo dirigido al modelo usa isError: true?
  • ¿El error JSON-RPC queda reservado para que el host repare la solicitud o el protocolo?
  • ¿El mensaje es corto, accionable y no contiene datos sensibles?
  • ¿El runtime limita retries, registra el motivo y evita repetir una mutación incierta?
  • ¿La prueba comprueba la forma que recibe el modelo y la que recibe el host?
  • ¿El comportamiento observado coincide con la versión instalada del SDK?

El objetivo no es enviar todos los errores al modelo. Cada consumidor debe recibir la información que puede usar. Un resultado isError: true es una observación de la ejecución de una tool. Un error JSON-RPC es un fallo del contrato entre cliente y servidor. Cuando esa frontera aparece en el código y en las pruebas, el agente tiene menos probabilidades de tratar una indisponibilidad estructural como una invitación a reintentar.

Cómo se produjo este artículo

Samuel Fajreldines es responsable de este artículo. La investigación comparó la especificación de MCP, la documentación actual de errores del SDK TypeScript, la guía de errores del SDK Python y el comportamiento documentado de errores visibles para el modelo en un SDK de agentes. Las discusiones públicas se usaron solo para vocabulario, preocupaciones e ideas de prueba. El diagrama es una SVG original, y la fixture de formas de respuesta se comprobó localmente con Node.js sin iniciar un servidor MCP, llamar a un modelo ni acceder a una API externa. La asistencia de IA apoyó el descubrimiento, la organización, la redacción, la localización y la revisión de consistencia. El autor usa RemoteCode como herramienta de trabajo, pero no ejecutó pruebas de producción ni aportó experiencia operativa.

Fuentes consultadas