O agente pediu customerId 42, mas a ferramenta esperava uma string. O modelo recebeu uma mensagem de erro genérica, tentou outra vez com os mesmos dados e gastou mais uma rodada sem produzir evidência nova. Esse fluxo não precisa de um prompt mais esperto. Precisa de um contrato executável na fronteira entre o agente e o código.
Tool calls de agentes de IA devem ser tratados como entrada não confiável. Em TypeScript, o caminho curto é declarar o schema uma vez, validar o input antes de executar, validar o output antes de devolver e transformar falhas previsíveis em dados que o loop consegue interpretar. O modelo pode sugerir a chamada, mas o runtime decide se ela existe.

Resumo prático
- O tipo do TypeScript ajuda durante a compilação, mas não valida JSON que chega em runtime.
- safeParse permite devolver campos de erro sem lançar uma exceção que encerra o loop.
- O resultado da ferramenta também precisa de schema, porque status de transporte não prova regra de negócio.
- Retry só é seguro quando a falha é transitória e a operação pode ser repetida sem duplicar efeito.
Onde a validação de uma tool call costuma falhar?
O problema aparece quando o código confia no tipo antes de conferir o valor real. A especificação de tools do Model Context Protocol define inputSchema para os parâmetros esperados e outputSchema para a estrutura de saída, mas a existência desses campos não substitui a validação no servidor. O runtime ainda recebe dados produzidos por um modelo, cliente ou integração.
Há quatro fronteiras diferentes. O provedor pode recusar JSON inválido antes da aplicação. O cliente pode serializar campos extras. O servidor pode aceitar o input e receber um resultado que não respeita a regra de negócio. Por fim, o agente pode ler um texto de erro como se fosse uma resposta válida. Cada fronteira precisa de uma decisão explícita.
O caso clássico é um objeto que compila no TypeScript, mas foi montado a partir de unknown:
type BuscarPedido = {
customerId: string;
includeHistory?: boolean;
};
function executar(input: BuscarPedido) {
return pedidos.buscar(input.customerId);
}
const recebido: unknown = JSON.parse(payload);
executar(recebido as BuscarPedido);
O as BuscarPedido apenas silencia o compilador. Ele não converte 42 em uma entrada válida nem impede customerId ausente. Se a ferramenta chama uma API, consulta um banco ou envia uma mensagem, o erro aparece tarde demais, longe do ponto em que nasceu.
Isso também explica por que uma mensagem como HTTP 200 não encerra a investigação. Uma API pode devolver uma resposta formalmente válida, mas com accepted: false, lista vazia inesperada ou estado ainda pendente. A ferramenta precisa declarar o que significa sucesso para o agente.
Este artigo complementa o servidor MCP em TypeScript com ferramentas somente leitura. O servidor mostra como expor uma superfície estreita. Aqui, o foco é decidir o que acontece quando a chamada chega incompleta, retorna um formato errado ou falha depois de uma mudança parcial.
Cápsula citável: Um tipo TypeScript não protege uma tool call recebida como JSON. O contrato precisa existir em runtime, antes da execução, e deve cobrir input, output e semântica de erro. O MCP fornece schemas para essas fronteiras, mas o servidor continua responsável por validar os valores e impedir que uma resposta plausível avance sem prova.
Como transformar o schema em contrato de runtime?
O schema deve ser a fonte usada para inferir o tipo e para validar dados reais. O uso básico do Zod mostra que safeParse devolve uma união discriminada com dados válidos ou um ZodError, sem exigir que o fluxo dependa de try/catch. Esse formato encaixa bem em um executor de ferramentas.
Comece pela menor operação que o agente precisa realizar. Não aceite um caminho de arquivo, uma consulta SQL ou um objeto de configuração inteiro quando a tarefa pede somente um identificador e uma opção. Campos estreitos reduzem ambiguidade e facilitam escrever casos inválidos.
import { z } from "zod";
const BuscarPedidoInput = z.object({
customerId: z.string().min(1),
includeHistory: z.boolean().default(false),
});
const BuscarPedidoOutput = z.object({
found: z.boolean(),
customerId: z.string(),
orders: z.array(
z.object({
id: z.string(),
status: z.enum(["pending", "paid", "cancelled"]),
}),
),
});
type BuscarPedidoInput = z.infer<typeof BuscarPedidoInput>;
type BuscarPedidoOutput = z.infer<typeof BuscarPedidoOutput>;
Agora há uma diferença importante entre o tipo e o schema. BuscarPedidoInput serve para chamadas internas que já passaram pela fronteira. BuscarPedidoInput.safeParse serve para qualquer valor vindo de uma API, de um MCP client ou de um modelo. Não use o tipo como substituto do segundo passo.
Em um servidor MCP, o mesmo contrato pode alimentar o inputSchema exposto ao cliente e a validação feita dentro do handler. Se o SDK escolhido tiver um helper próprio para converter Zod em JSON Schema, valide o resultado gerado em teste. Não assuma que optional, default, union e campos desconhecidos foram traduzidos do jeito que o cliente espera.
Insight de implementação: O ganho do schema único não é eliminar todos os erros do modelo. É impedir que a aplicação tenha uma definição para o compilador, outra para o protocolo e uma terceira escondida no handler. Quando essas três versões divergem, o retry apenas repete uma contradição.
Como devolver um erro que o agente consegue corrigir?
Um erro útil identifica a fronteira que falhou, aponta os campos que precisam de correção e informa se a operação chegou a executar. As melhores práticas de clientes MCP recomendam que erros não capturados apareçam como resultado do script para que o modelo possa se autocorrigir, além de registrar efeitos parciais já cometidos. Isso é diferente de esconder a exceção ou devolver uma página HTML.
Use um envelope estável. O exemplo abaixo mantém a mensagem curta, preserva o caminho dos problemas e separa erro de validação de falha transitória. Não devolva o ZodError inteiro se ele contiver valores sensíveis ou detalhes 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: "Corrija os argumentos e tente novamente.",
issues: error.issues.map((issue) => ({
path: issue.path.join("."),
message: issue.message,
})),
retryable: false,
};
}
O campo retryable não deve ser uma opinião do modelo. O executor sabe se o erro veio de validação, timeout, limite de requisição, permissão ou regra de negócio. Uma descrição textual pode ajudar o agente, mas a política de repetição precisa ficar no código.
Quando o input é inválido, o agente pode corrigir a chamada. Quando um token expirou, talvez seja necessário renovar credencial fora do loop. Quando a API aplicou uma alteração e caiu antes de responder, repetir pode duplicar o efeito. O envelope deve tornar essa diferença visível.
Esse desenho se liga à observabilidade de agentes de código no CI. Registre nome da ferramenta, versão do schema, código de erro, duração, tentativa e se houve efeito parcial. Não registre segredos nem o payload completo por padrão. O objetivo é reconstruir a decisão sem transformar o trace em uma segunda base de dados sensível.
Cápsula citável: Um erro de tool call deve ser dado estruturado, não apenas texto para o modelo interpretar. O envelope mínimo informa código, campos inválidos, possibilidade de retry e estado de execução. Assim, o agente pode corrigir argumentos ou escalar sem confundir timeout, rejeição de negócio e efeito parcial.
Como validar o output antes de alimentar o próximo passo?
Valide a saída antes de devolvê-la ao agente. O MCP permite um outputSchema e recomenda conteúdo estruturado quando a ferramenta fornece esse contrato. A aplicação deve usar a mesma ideia mesmo quando o cliente não valida o resultado.
async function buscarPedido(rawInput: unknown): Promise<BuscarPedidoOutput | ToolFailure> {
const input = BuscarPedidoInput.safeParse(rawInput);
if (!input.success) return invalidInput(input.error);
try {
const rawOutput = await pedidos.buscar(input.data.customerId, {
includeHistory: input.data.includeHistory,
});
const output = BuscarPedidoOutput.safeParse(rawOutput);
if (!output.success) {
return {
ok: false,
code: "INVALID_OUTPUT",
message: "O serviço retornou um formato que a ferramenta não reconhece.",
retryable: false,
};
}
return output.data;
} catch (error) {
if (isTemporary(error)) {
return {
ok: false,
code: "RETRYABLE",
message: "O serviço está temporariamente indisponível.",
retryable: true,
};
}
return {
ok: false,
code: "FAILED",
message: "A ferramenta não concluiu a operação.",
retryable: false,
};
}
}
O retorno tipado acima ainda pode ser refinado com uma união discriminada que inclua ok true no sucesso. Em um projeto real, faça essa escolha antes de conectar o executor ao agente. O revisor precisa conseguir diferenciar uma lista vazia de um pedido inexistente, e ambos de uma chamada que falhou.
Não use texto livre como única saída quando o próximo passo depende de uma decisão. Texto pode continuar útil para uma explicação ao usuário, mas o sistema deve receber campos que possa testar. Se a ferramenta devolve uma lista, limite tamanho e ordenação. Se devolve estado, inclua um identificador e a versão que foi observada.
Quando o fluxo crescer, ligue essa fronteira aos evals de regressão para agentes de código. Um caso de teste deve cobrir input inválido, output quebrado, erro transitório, resposta vazia válida e uma regra de negócio que retorna status de transporte bem-sucedido.
Quando um retry é autocorreção e quando é duplicação?
Retry deve existir somente quando o executor consegue justificar a repetição. A orientação atual da OpenAI para fluxos com tool calling recomenda documentar campos, tipos, comportamento de erro, concorrência, limites de tentativas e condição de parada. Esse contrato precisa ser testado como código, não deixado no prompt.
Uma política simples pode separar quatro casos:
| Falha | Repetir? | Próxima ação |
|---|---|---|
| Input inválido | Sim, uma vez após corrigir os campos. | Devolver os problemas ao agente. |
| Timeout antes de saber o efeito | Não automaticamente em operação de escrita. | Consultar idempotência ou escalar. |
| Limite temporário de leitura | Sim, com limite e espera. | Repetir sem ampliar o escopo. |
| Regra de negócio rejeitada | Não como a mesma chamada. | Pedir nova decisão ou informar o motivo. |
Uma ferramenta somente leitura costuma ter uma política mais simples, mas ainda pode sobrecarregar a API. Uma ferramenta que cria cobrança, envia mensagem ou altera estado precisa de chave de idempotência, consulta de estado ou confirmação humana. O agente não deve inferir segurança de retry pelo fato de a chamada ter falhado.
Em loops longos, o histórico de erros pode consumir a janela de contexto. Quando uma execução atravessa muitas rodadas de Claude Code ou Codex, uso RemoteCode para reduzir a repetição de contexto em fluxos agentic. É uma ferramenta minha, citada aqui porque handoffs curtos e evidências estruturadas ajudam tanto o orçamento de tokens quanto a recuperação de falhas.
Cápsula citável: Retry é uma decisão de semântica, não uma reação automática a exceção. Leituras idempotentes podem repetir uma falha transitória com limite. Escritas que talvez tenham sido aplicadas precisam consultar estado ou usar chave de idempotência antes de tentar de novo.
Como verificar que o contrato realmente funciona?
O contrato está funcionando quando o teste consegue provar três coisas: entrada inválida não alcança o serviço, saída inválida não chega ao próximo passo e uma falha temporária não cria um loop sem limite. Faça essa verificação com um executor falso antes de conectar a ferramenta a um modelo real.
Os pré-requisitos são Node.js, TypeScript em modo strict, Zod e um runner de testes já usado pelo projeto. A própria documentação do Zod recomenda strict no tsconfig.json e mostra safeParse como retorno tipado para sucesso ou erro. Não fixe versões aqui sem a árvore de dependências do seu repositório.
it("não chama o serviço com input inválido", async () => {
const buscar = vi.fn();
const result = await executarTool({ customerId: 42 }, { buscar });
expect(result).toMatchObject({
ok: false,
code: "INVALID_INPUT",
retryable: false,
});
expect(buscar).not.toHaveBeenCalled();
});
it("recusa output que quebra o contrato", async () => {
const buscar = vi.fn().mockResolvedValue({
found: true,
customerId: "cus_1",
orders: [{ id: "ord_1", status: "unknown" }],
});
const result = await executarTool(
{ customerId: "cus_1" },
{ buscar },
);
expect(result).toMatchObject({
ok: false,
code: "INVALID_OUTPUT",
});
});
Inclua um caso para campo extra, um para ausência de campo obrigatório, um para null, um para enum desconhecido e um para timeout. Depois faça um teste de integração que chama o servidor MCP pelo transporte usado em produção. Um unit test que chama o handler diretamente não revela erro de serialização, negociação ou outputSchema incompatível.
Se o agente receber INVALID_INPUT, ele deve corrigir apenas os campos indicados. Se receber INVALID_OUTPUT, o fluxo deve parar e abrir uma evidência para o operador. Se receber RETRYABLE, o executor aplica a política local. Essa separação é o pequeno harness que impede o modelo de inventar a próxima etapa.
Limites que o schema não resolve
Schema não autentica uma chamada, não autoriza o agente a usar uma ferramenta e não prova que uma resposta atende à política do produto. Ele controla forma e tipos. Permissão, escopo, segredo, rate limit, auditoria e aprovação continuam sendo responsabilidades do runtime e do serviço.
Também não trate descrições e anotações da ferramenta como autoridade absoluta. A especificação do MCP orienta clientes a tratar anotações como não confiáveis quando não vierem de servidores confiáveis. O desenho de allowlist para MCP no CI mostra por que contrato de schema e controle de acesso precisam ficar em camadas diferentes.
Outra limitação é a migração. Se o schema mudar, registre a versão no trace e teste clientes antigos. Um default novo pode esconder que o modelo ainda envia um formato anterior. Um campo opcional pode ser aceito pelo parser e rejeitado pela regra de negócio. Compatibilidade precisa ser uma decisão documentada.
O resultado é um limite simples e útil: o agente pode propor, o schema pode recusar, a ferramenta pode executar e a verificação pode interromper. Quando essas funções ficam misturadas num prompt, o sistema perde a capacidade de dizer qual parte falhou.
Perguntas frequentes
TypeScript sozinho valida argumentos de uma tool call?
Não. TypeScript verifica relações entre valores durante a compilação, mas o payload recebido em runtime ainda é unknown do ponto de vista de confiança. Use um schema como Zod na fronteira e derive o tipo a partir dele. A documentação do Zod mostra safeParse para separar dados válidos de ZodError sem confundir os dois caminhos.
Devo aceitar campos extras enviados pelo agente?
Só quando eles pertencem a um envelope separado e têm política própria. Campos extras misturados ao input da ferramenta podem esconder erro de contrato ou metadados que o handler não deveria consumir. Se o cliente adiciona informações de auditoria, extraia esse envelope antes da validação e valide os argumentos funcionais com um schema estreito.
Todo erro de ferramenta deve voltar como isError?
O protocolo e o SDK definem formas próprias de sinalizar falhas, então siga o contrato do cliente MCP escolhido. Além disso, devolva um envelope de aplicação que diferencie input inválido, output inválido, erro temporário e falha permanente. Um único booleano não informa se repetir é seguro nem se a operação teve efeito parcial.
Um agente deve tentar corrigir o próprio erro?
Pode corrigir erros de input pequenos quando o executor devolver campos e limites claros. Não deixe o agente repetir indefinidamente nem reabrir toda a tarefa durante o reparo. Para output inválido, efeito parcial, permissão negada ou regra de negócio, o caminho mais seguro costuma ser parar, registrar evidência e pedir uma nova decisão.
Preciso validar output se a API já responde JSON?
Sim. JSON garante apenas a sintaxe de transporte. O output schema verifica campos, tipos, enums e invariantes que o próximo passo espera. Uma resposta JSON com estado desconhecido pode ser sintaticamente correta e operacionalmente perigosa. Valide antes de devolver ao agente e escreva um teste para cada estado que o fluxo reconhece.
Fontes consultadas
- Model Context Protocol, especificação de tools, consultada em 27/07/2026.
- Model Context Protocol, melhores práticas de clientes, consultadas em 27/07/2026.
- Zod, uso básico, consultado em 27/07/2026.
- OpenAI Developers, orientação de modelos e tool calling, consultada em 27/07/2026.
- Zod, documentação principal, consultada em 27/07/2026.