El agente pidió customerId 42, pero la herramienta esperaba una cadena. El modelo recibió un error genérico, volvió a intentar el mismo payload y gastó otro turno sin obtener evidencia nueva. Esto no se resuelve con un prompt más ingenioso. Hace falta un contrato ejecutable en la frontera entre el agente y el código.
Trata los tool calls de agentes de IA como input no confiable. En TypeScript, el camino corto es declarar un schema, validar el input antes de ejecutar, validar el output antes de devolverlo y convertir los fallos previsibles en datos que el loop pueda entender. El modelo puede proponer la llamada. El runtime decide si existe.

Resumen práctico
- Los tipos de TypeScript ayudan durante la compilación, pero no validan el JSON recibido en runtime.
- safeParse puede devolver campos de error sin lanzar una excepción que termine el loop.
- El output de la herramienta también necesita un schema, porque el estado del transporte no prueba una regla de negocio.
- Un retry solo es seguro cuando el fallo es transitorio y la operación puede repetirse sin duplicar un efecto.
¿Dónde suele fallar la validación de un tool call?
El fallo empieza cuando el código confía en un tipo antes de comprobar el valor. La especificación de tools de Model Context Protocol define inputSchema para los parámetros esperados y outputSchema para la estructura devuelta, pero esos campos no sustituyen la validación dentro del servidor. El runtime sigue recibiendo datos producidos por un modelo, un cliente o una integración.
Hay cuatro fronteras distintas. Un proveedor puede rechazar JSON inválido antes de que llegue a la aplicación. Un cliente puede serializar campos adicionales. El servidor puede aceptar el input y recibir un resultado que rompe una regla de negocio. Por último, el agente puede leer un texto de error como si fuera una respuesta válida. Cada frontera necesita una decisión explícita.
El ejemplo clásico es un objeto construido desde datos unknown:
type GetOrder = {
customerId: string;
includeHistory?: boolean;
};
function execute(input: GetOrder) {
return orders.find(input.customerId);
}
const received: unknown = JSON.parse(payload);
execute(received as GetOrder);
La aserción de tipo solo silencia al compilador. No convierte 42 en un input válido ni evita que falte customerId. Si la herramienta llama una API, consulta una base de datos o envía un mensaje, el fallo aparece lejos de su origen.
Por eso HTTP 200 tampoco cierra la investigación. Una API puede devolver una respuesta formalmente válida con accepted en false, una lista vacía inesperada o un estado pendiente. La herramienta debe definir qué significa éxito para el agente.
Este artículo sigue el servidor MCP en TypeScript con herramientas de solo lectura. Allí el foco es exponer una superficie estrecha. Aquí la pregunta es qué ocurre cuando una llamada llega incompleta, devuelve una forma equivocada o falla después de un cambio parcial.
Cápsula citable: Un tipo de TypeScript no protege un tool call recibido como JSON. El contrato debe existir en runtime antes de ejecutar y cubrir input, output y semántica de errores. MCP ofrece schemas para esas fronteras, pero el servidor aún debe validar los valores y detener una respuesta aparentemente plausible cuando no existe evidencia.
¿Cómo convertir el schema en un contrato de runtime?
El schema debe ser la fuente usada para inferir el tipo y validar datos reales. Los fundamentos de Zod presentan safeParse como un resultado discriminado que contiene datos válidos o un ZodError, sin obligar al flujo a depender de try/catch. Esa forma encaja bien en un ejecutor de herramientas.
Empieza por la operación más pequeña que el agente necesita. No aceptes una ruta de archivo arbitraria, una consulta SQL o un objeto de configuración completo cuando la tarea solo requiere un identificador y una opción. Los campos estrechos reducen la ambigüedad y hacen más fáciles los casos inválidos.
import { z } from "zod";
const GetOrderInput = z.object({
customerId: z.string().min(1),
includeHistory: z.boolean().default(false),
});
const GetOrderOutput = z.object({
found: z.boolean(),
customerId: z.string(),
orders: z.array(
z.object({
id: z.string(),
status: z.enum(["pending", "paid", "cancelled"]),
}),
),
});
type GetOrderInput = z.infer<typeof GetOrderInput>;
type GetOrderOutput = z.infer<typeof GetOrderOutput>;
El tipo y el schema tienen ahora trabajos distintos. GetOrderInput sirve para llamadas internas que ya cruzaron la frontera. GetOrderInput.safeParse sirve para valores de una API, un cliente MCP o un modelo. El tipo no sustituye al segundo paso.
En un servidor MCP, el mismo contrato puede alimentar el inputSchema expuesto al cliente y la validación dentro del handler. Si el SDK elegido ofrece un helper para convertir Zod en JSON Schema, prueba el resultado generado. No des por hecho que optional, default, union y las claves desconocidas se tradujeron como espera el cliente.
Idea de implementación: El valor de un solo schema no es eliminar todos los errores del modelo. Evita que el compilador, el protocolo y el handler adopten en silencio tres definiciones distintas. Cuando divergen, el retry solo repite una contradicción.
¿Cómo devolver un error que el agente pueda corregir?
Un error útil identifica la frontera que falló, señala los campos que deben corregirse e informa si la operación llegó a ejecutarse. Las buenas prácticas para clientes MCP recomiendan mostrar los errores no capturados como resultado del script para que el modelo pueda autocorregirse y registrar los efectos parciales ya realizados. Eso es distinto de ocultar una excepción o devolver una página HTML.
Usa un sobre estable. El ejemplo mantiene corto el mensaje, conserva las rutas de los problemas y separa los fallos de validación de los fallos transitorios. No devuelvas un ZodError completo si contiene secretos o detalles internos.
type ToolErrorCode =
| "INVALID_INPUT"
| "INVALID_OUTPUT"
| "RETRYABLE"
| "FAILED";
type ToolFailure = {
ok: false;
code: ToolErrorCode;
message: string;
issues?: Array<{ path: string; message: string }>;
retryable: boolean;
};
function invalidInput(error: z.ZodError): ToolFailure {
return {
ok: false,
code: "INVALID_INPUT",
message: "Corrige los argumentos y vuelve a intentarlo.",
issues: error.issues.map((issue) => ({
path: issue.path.join("."),
message: issue.message,
})),
retryable: false,
};
}
El campo retryable no debe ser una opinión del modelo. El ejecutor sabe si el fallo viene de una validación, un timeout, un límite de solicitudes, un permiso o una regla de negocio. El texto puede ayudar al agente, pero la política de retry pertenece al código.
Un input inválido puede corregirse. Un token vencido quizá requiera renovar credenciales fuera del loop. Si una API aplicó una escritura y falló antes de responder, reintentar puede duplicar el efecto. El sobre debe hacer visible esa diferencia.
Este diseño se conecta con la observabilidad de agentes de código en CI. Registra nombre de la herramienta, versión del schema, código de error, duración, intento y si hubo un efecto parcial. No registres secretos ni el payload completo por defecto. El objetivo es reconstruir la decisión sin crear otra base de datos sensible.
Cápsula citable: Un error de tool call debe ser un dato estructurado, no solo texto para que el modelo lo interprete. El sobre mínimo informa código, campos inválidos, posibilidad de retry y estado de ejecución. Así el agente puede corregir argumentos o escalar sin confundir un timeout con un rechazo de negocio o un efecto parcial.
¿Cómo validar el output antes del siguiente paso?
Valida el resultado antes de devolverlo al agente. La especificación de tools de MCP admite outputSchema y recomienda contenido estructurado cuando una herramienta ofrece ese contrato. La aplicación debe aplicar la misma regla aunque el cliente no valide la respuesta.
async function getOrder(rawInput: unknown): Promise<GetOrderOutput | ToolFailure> {
const input = GetOrderInput.safeParse(rawInput);
if (!input.success) return invalidInput(input.error);
try {
const rawOutput = await orders.find(input.data.customerId, {
includeHistory: input.data.includeHistory,
});
const output = GetOrderOutput.safeParse(rawOutput);
if (!output.success) {
return {
ok: false,
code: "INVALID_OUTPUT",
message: "El servicio devolvió una forma de herramienta desconocida.",
retryable: false,
};
}
return output.data;
} catch (error) {
if (isTemporary(error)) {
return {
ok: false,
code: "RETRYABLE",
message: "El servicio no está disponible temporalmente.",
retryable: true,
};
}
return {
ok: false,
code: "FAILED",
message: "La herramienta no completó la operación.",
retryable: false,
};
}
}
El tipo de retorno puede refinarse con una unión discriminada que incluya ok true en el éxito. Decide esto antes de conectar el ejecutor al agente. Un revisor debe distinguir una lista vacía de un pedido inexistente, y ambos de una llamada fallida.
No uses texto libre como única salida cuando el siguiente paso depende de una decisión. El texto sigue siendo útil para explicar algo al usuario, pero el sistema debe recibir campos que pueda probar. Para una lista, define tamaño y orden. Para un estado, incluye un identificador y la versión observada.
Cuando el flujo crezca, conecta esta frontera con evals de regresión para agentes de código. Prueba input inválido, output roto, un error transitorio, una respuesta vacía válida y una regla de negocio que rechace una solicitud aunque el transporte haya funcionado.
¿Cuándo un retry es autocorrección y cuándo es duplicación?
Reintenta solo cuando el ejecutor pueda justificar la repetición. La orientación actual de OpenAI sobre modelos y tool calling recomienda documentar campos, tipos, comportamiento de errores, concurrencia, límites de intentos y condiciones de parada. Ese contrato debe vivir en código y tests, no solo en el prompt.
| Fallo | ¿Reintentar? | Siguiente acción |
|---|---|---|
| Input inválido | Sí, una vez después de corregir los campos. | Devolver los problemas al agente. |
| Timeout antes de conocer el efecto | No automáticamente en una escritura. | Comprobar idempotencia o escalar. |
| Límite temporal de lectura | Sí, con un límite y espera. | Repetir sin ampliar el alcance. |
| Rechazo de regla de negocio | No con la misma llamada. | Pedir una decisión nueva o informar el motivo. |
Una herramienta de solo lectura puede tener una política más sencilla, pero aún puede sobrecargar una API. Una herramienta que crea un cobro, envía un mensaje o cambia estado necesita una clave de idempotencia, una consulta de estado o confirmación humana. El agente no debe deducir que un retry es seguro solo porque la llamada falló.
Los loops largos también hacen caro transportar el historial de errores. Cuando una ejecución atraviesa muchas rondas de Claude Code o Codex, uso RemoteCode para reducir el contexto repetido en flujos agentic. Es una herramienta propia, mencionada aquí porque los handoffs cortos y la evidencia estructurada ayudan tanto al presupuesto de tokens como a la recuperación de fallos.
Cápsula citable: Retry es una decisión semántica, no una reacción automática ante una excepción. Las lecturas idempotentes pueden repetir un fallo transitorio dentro de un límite. Las escrituras que quizá ya se aplicaron necesitan consultar el estado o usar una clave de idempotencia antes de otro intento.
¿Cómo comprobar que el contrato funciona?
El contrato funciona cuando las pruebas demuestran tres cosas: el input inválido nunca llega al servicio, el output inválido nunca llega al siguiente paso y un fallo temporal no puede crear un loop sin límite. Haz esta comprobación con un ejecutor falso antes de conectar la herramienta a un modelo real.
Los requisitos son Node.js, TypeScript con strict, Zod y el runner de tests que ya use el proyecto. La documentación de Zod recomienda strict en tsconfig.json y documenta safeParse como un resultado tipado de éxito o error. No fijes versiones sin revisar el árbol de dependencias del repositorio.
it("no llama al servicio con input inválido", async () => {
const find = vi.fn();
const result = await executeTool({ customerId: 42 }, { find });
expect(result).toMatchObject({
ok: false,
code: "INVALID_INPUT",
retryable: false,
});
expect(find).not.toHaveBeenCalled();
});
it("rechaza un output que rompe el contrato", async () => {
const find = vi.fn().mockResolvedValue({
found: true,
customerId: "cus_1",
orders: [{ id: "ord_1", status: "unknown" }],
});
const result = await executeTool(
{ customerId: "cus_1" },
{ find },
);
expect(result).toMatchObject({
ok: false,
code: "INVALID_OUTPUT",
});
});
Añade casos para un campo adicional, un campo obligatorio ausente, null, un enum desconocido y un timeout. Después ejecuta una prueba de integración mediante el transporte MCP usado en producción. Una prueba unitaria que llama directamente al handler no detectará incompatibilidad de serialización, negociación u outputSchema.
Si el agente recibe INVALID_INPUT, debe corregir solo los campos indicados. Si recibe INVALID_OUTPUT, el flujo debe detenerse y crear evidencia para un operador. Si recibe RETRYABLE, el ejecutor aplica la política local. Esa separación es el pequeño harness que impide que el modelo invente el siguiente paso.
¿Qué no puede resolver el schema?
Un schema no autentica una llamada, no autoriza una herramienta ni demuestra que una respuesta cumple la política del producto. Controla forma y tipos. Los permisos, el alcance, los secretos, los límites de solicitudes, la auditoría y la aprobación siguen siendo responsabilidades del runtime y del servicio.
Tampoco trates las descripciones y anotaciones de una herramienta como autoridad absoluta. La especificación de MCP pide que los clientes consideren no confiables las anotaciones que no procedan de servidores confiables. El diseño de allowlist para MCP en CI explica por qué los contratos de schema y el control de acceso deben vivir en capas distintas.
La migración es otro límite. Cuando cambies un schema, registra su versión en el trace y prueba clientes antiguos. Un nuevo default puede ocultar que el modelo sigue enviando una forma anterior. Un campo opcional puede pasar el parser y fallar una regla de negocio. La compatibilidad tiene que ser una decisión documentada.
El límite útil es sencillo: el agente propone, el schema puede rechazar, la herramienta puede ejecutar y la verificación puede detener el flujo. Cuando esas funciones se mezclan dentro de un prompt, el sistema pierde la capacidad de decir qué parte falló.
Preguntas frecuentes
¿TypeScript valida por sí solo los argumentos de un tool call?
No. TypeScript comprueba relaciones entre valores durante la compilación, pero un payload de runtime sigue siendo un dato unknown no confiable. Coloca un schema de Zod en la frontera y deriva el tipo desde él. La documentación de Zod presenta safeParse para separar datos válidos de ZodError sin confundir los dos caminos.
¿Debo aceptar campos adicionales enviados por el agente?
Solo si pertenecen a un sobre separado con su propia política. Los campos adicionales mezclados con el input de la herramienta pueden ocultar un error de contrato o metadatos que el handler no debería consumir. Si el cliente añade información de auditoría, extrae primero ese sobre y valida después los argumentos funcionales con un schema estrecho.
¿Todo error de herramienta debe devolver isError?
El protocolo y el SDK definen sus propias señales de fallo, así que sigue el contrato del cliente MCP que uses. Además, devuelve un sobre de aplicación que diferencie input inválido, output inválido, fallo transitorio y fallo permanente. Un booleano no informa si el retry es seguro ni si ocurrió un efecto parcial.
¿Un agente debe corregir su propio error?
Puede corregir errores pequeños de input cuando el ejecutor devuelva campos y límites claros. No permitas reintentos infinitos ni que reabra toda la tarea durante la reparación. Para output inválido, efectos parciales, permisos denegados o rechazo de negocio, lo más seguro suele ser detenerse, registrar evidencia y pedir una decisión nueva.
¿Necesito validar el output si la API ya devuelve JSON?
Sí. JSON garantiza solo la sintaxis del transporte. Un output schema comprueba campos, tipos, enums e invariantes que espera el siguiente paso. Una respuesta JSON con un estado desconocido puede ser correcta sintácticamente y peligrosa operativamente. Valídala antes de devolverla al agente y prueba cada estado reconocido.
Fuentes consultadas
- Especificación de tools de Model Context Protocol, consultada el 27/07/2026.
- Buenas prácticas para clientes MCP, consultadas el 27/07/2026.
- Fundamentos de Zod, consultado el 27/07/2026.
- OpenAI Developers, orientación sobre modelos y tool calling, consultada el 27/07/2026.
- Documentación de Zod, consultada el 27/07/2026.