Il codice di errore di un tool non finisce in un log: finisce nel contesto del modello, e da lì determina la mossa successiva. Un tool MCP che fallisce male costa più di uno che non esiste, perché consuma turni e porta l’agente a conclusioni sbagliate.
L’errore non interrompe il loop
La prima cosa da correggere è l’intuizione che un’eccezione faccia cadere la query. La documentazione dell’Agent SDK è netta: «A handler error doesn’t stop the agent loop». Il server MCP in-process cattura le eccezioni non gestite e le trasforma comunque in un risultato di errore. La conseguenza è quella che conta in fase di progetto: «how you report an error determines what Claude reads, not whether the query fails».
La scelta quindi non è fra fallire e non fallire, ma fra due messaggi. Se lasci propagare l’eccezione, il modello legge la stringa grezza, senza contesto su quale chiamata sia saltata. Se la catturi e restituisci un errore composto da te, legge la frase che hai scritto. In entrambi i casi può riprovare, cambiare tool o spiegare il fallimento all’utente: cambia solo con quanta informazione lo fa.
Il flag che cambia la semantica del risultato
A livello di Messages API un risultato di tool ha tre campi: l’id della chiamata, il contenuto opzionale e un flag opzionale da mettere a vero «if the tool execution resulted in an error».
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "ConnectionError: the weather service API is not available (HTTP 500)",
"is_error": true
}
]
}
Con il connettore MCP dell’API il blocco che torna si chiama mcp_tool_result e porta lo stesso campo is_error. Nell’SDK il nome segue il linguaggio: isError in TypeScript, is_error in Python. Il flag però è solo l’etichetta. La regola sul testo sta in una riga della documentazione: «Write instructive error messages». E l’esempio di che cosa scrivere al posto di un generico "failed" è «Rate limit exceeded. Retry after 60 seconds.», cioè una causa e un’azione con un tempo.
Comporre il messaggio invece di rilanciare l’eccezione
Il pattern operativo è catturare due famiglie di guasto dentro l’handler: lo stato HTTP non riuscito, letto dalla risposta, e l’errore di rete o il JSON invalido, presi dal blocco try.
if response.status_code != 200:
return {
"content": [
{"type": "text",
"text": f"API error: {response.status_code} {response.reason_phrase}"}
],
"is_error": True,
}
La guida «Writing tools for agents» generalizza il principio: «you can prompt-engineer your error responses to clearly communicate specific and actionable improvements, rather than opaque error codes or tracebacks». In pratica aggiungi quello che l’eccezione non sa: quale risorsa, quale parametro era fuori dominio, quale tool alternativo esiste.
Il ritorno è misurabile sul comportamento del modello. Per una chiamata malformata, «Claude will retry 2-3 times with corrections before apologizing to the user»: un errore che nomina il parametro mancante consuma un tentativo e chiude, uno opaco li brucia tutti e finisce in una scusa. Sono tre round trip di latenza e token che paghi ogni volta.
structuredContent e la trappola dei blocchi di testo
structuredContent è un oggetto JSON opzionale, separato dall’array dei contenuti, pensato per «raw values that Claude can read as exact fields instead of parsing them out of a text string or image». Su un errore è la forma giusta: codice, risorsa e secondi di attesa come campi, non come prosa da interpretare.
La trappola è che, quando è valorizzato, il modello riceve il JSON più eventuali blocchi immagine o risorsa, ma «Text blocks in content are not forwarded, since they are assumed to duplicate the structured data». Se la spiegazione leggibile vive solo nel testo, sparisce. Seconda trappola, specifica di Python: il decoratore @tool inoltra soltanto contenuto e flag di errore, quindi per restituire structuredContent serve un server MCP standalone al posto di quello in-process.
Il risultato di un tool è contenuto non fidato
Ultimo vincolo, quello che rompe le architetture che infilano istruzioni dentro l’errore. I risultati di tool arrivano spesso da fonti fuori dal tuo controllo, e «Claude is trained to treat instructions inside tool results as potentially untrusted third-party content». Se nell’errore scrivi un ordine operativo, il modello può rifiutarlo o chiedere conferma all’utente. La guida di troubleshooting indica dove spostarlo: in un turno utente successivo al blocco di risultato, oppure in un system message a metà conversazione sui modelli che lo supportano. «Keep the tool result to just the data.»
C’è anche un tetto di dimensione da rispettare: in Claude Code un risultato MCP oltre i 25.000 token viene salvato su file e sostituito da un messaggio di errore che ne indica il percorso. Un errore prolisso, con lo stack trace completo allegato, può quindi costare più del successo che stava sostituendo.