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.