# Errori nei client MCP: distinguere HTTP 503, JSON-RPC error e isError

> Canonical: https://vxi.me/it/posts/mcp-client-http-jsonrpc-iserror/
> Locale: it-it
> Published: 2026-10-18
> Updated: 2026-10-18
> Markdown: https://vxi.me/it/posts/mcp-client-http-jsonrpc-iserror.md

Un client MCP chiama uno strumento, riceve HTTP 200 e l'agente segnala comunque un errore. Non c'è contraddizione: uno stato HTTP riuscito non garantisce che nella risposta JSON-RPC manchi `error` né che il risultato dello strumento abbia `isError: false`.

In un gateway MCP, in un'integrazione per Coding Agent o in un client sviluppato internamente, **controllare soltanto `response.ok` nasconde due importanti classi di errore**. Conviene esaminare prima il trasporto HTTP, poi la busta JSON-RPC e infine il risultato dello strumento. La decisione di riprovare è separata.

Ho letto il sorgente di [Cakewalk MCP Test Kitchen](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen) al [commit fissato `cf2f689`](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/tree/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b) e ho eseguito un classificatore indipendente su loopback locale con Node.js: **tutti gli undici casi sintetici sono passati**. Questo verifica la logica che ho scritto, non l'interoperabilità con il server .NET di Cakewalk o con client MCP reali come Cursor e Codex.

## Tre livelli di errore in tools/call: HTTP, JSON-RPC e risultato dello strumento

La sezione Error Handling della [specifica MCP Tools del 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) distingue gli errori di protocollo da quelli causati dall'esecuzione dello strumento. Considerando anche HTTP, i livelli diventano tre:

| Livello | Esempio di risposta | Cosa può stabilire il client |
| --- | --- | --- |
| Trasporto HTTP | `503 Service Unavailable` | La richiesta HTTP è fallita; effetti collaterali e retry automatici restano sconosciuti |
| Protocollo JSON-RPC | HTTP 200 con `error.code` e `error.message` al livello superiore | Si è verificato un errore JSON-RPC, senza risultato utile dello strumento |
| Esecuzione dello strumento | HTTP 200 con `result.isError: true` | Lo scambio di protocollo può terminare, ma lo strumento è fallito |

La prima risposta HTTP 200 è una busta di errore JSON-RPC:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": { "code": -32602, "message": "Invalid params" }
}
```

La seconda è un fallimento applicativo segnalato dallo strumento:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "content": [{ "type": "text", "text": "Tool-side failure" }],
    "isError": true
  }
}
```

Entrambi i JSON rappresentano **corpi della risposta server**; una richiesta completa deve rispettare anche `_meta` e le intestazioni HTTP previste.

Nel [`CallToolResult` Schema del 2026-07-28](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2026-07-28/schema.json), `isError` è un booleano opzionale: se manca, vale implicitamente `false`. Interpretare `resultType` assente come `complete` segue un'altra regola di retrocompatibilità. Il risultato può inoltre contenere `structuredContent`, non necessariamente un oggetto, ma questa versione dello schema **continua a richiedere `content` come array**. Il nostro classificatore convalida questo ambito limitato, non tutti gli schemi di tutte le versioni e SDK.

Il testo di un errore dello strumento può aiutare il modello a correggere gli argomenti. La [sezione Error Handling](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#error-handling) recita “Clients SHOULD provide tool execution errors to language models”; per gli errori di protocollo scrive “Clients MAY provide protocol errors to language models”. **SHOULD e MAY riguardano cosa mostrare al modello, non un obbligo di ripetere incondizionatamente la richiesta.** È cruciale con strumenti che producono effetti collaterali.

`resultType: "input_required"` significa che la chiamata non è terminata e richiede altri dati. In [MRTR (Multi Round-Trip Requests, richieste a più scambi)](https://modelcontextprotocol.io/specification/2026-07-28/basic#message-patterns), la risposta include `inputRequests`; il client raccoglie i dati e invia un'altra richiesta con `inputResponses`, eventualmente `requestState`, e un nuovo `id` JSON-RPC. Non è un errore dello strumento né un semplice retry per timeout.

Streamable HTTP presenta un'insidia meno evidente. La [specifica di trasporto 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) permette come risposta `application/json` oppure `text/event-stream`. **HTTP 200 con SSE indica solo che lo stream di risposta è iniziato.** Può interrompersi prima del risultato JSON-RPC finale, oppure il risultato finale può contenere `error`. Il client deve distinguere in base a `Content-Type`, associare l'ID della richiesta e verificare l'arrivo della risposta conclusiva. È un problema di completezza dello stream, non una quarta categoria artificiale di errore dello strumento.

Le notifiche JSON-RPC non hanno `id`. Per i POST di notifica accettati, la [specifica descrive `202 Accepted` senza corpo](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#sending-messages). La versione di base Streamable HTTP 2026-07-28 non definisce il consueto flusso di notifiche HTTP avviate dal client; chiudere lo stream esprime la cancellazione. **Non bisogna passare una risposta 202 vuota al classificatore che si aspetta il corpo di `tools/call`.**

## Analisi statica di Cakewalk: 503 → 200 non prova un retry automatico

Cakewalk permette al server di provocare deliberatamente errori secondo lo scenario scelto. Il sorgente fissato mostra dove vengono iniettati; i test sintetici locali della prossima sezione restano un esperimento diverso.

In [`HttpStatusSequenceScenario.cs`, righe 12–35](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/src/McpTestServer.API/Scenarios/Errors/HttpStatusSequenceScenario.cs#L12-L35), `ScenarioWireInvocationKeys.HttpRequest` conta i POST MCP della sessione, non soltanto le chiamate a `tools/call`. La configurazione `{"statusCodes":[503,200]}` restituisce 503 al primo POST contato e permette a un secondo POST effettivo di proseguire con 200. **Un altro POST può consumare il primo stato.** Ad esempio `tools/list` → primo POST con 503 → il client si ferma → `tools/call` non viene nemmeno inviato. È una sequenza dedotta dal codice, non una cattura di pacchetti; `[503,200]` da solo non dimostra che ci sia stato un retry.

Diversamente, [`JsonRpcErrorScenario.cs`](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/src/McpTestServer.API/Scenarios/Errors/JsonRpcErrorScenario.cs#L20-L36) produce HTTP 200 con `error` JSON-RPC per `tools/call`, mentre [`ToolErrorScenario.cs`](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/src/McpTestServer.API/Scenarios/Errors/ToolErrorScenario.cs) percorre la via `CallToolResult.isError`. I punti di iniezione sono diversi.

Ho verificato anche [le registrazioni DI alle righe 308–315](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/src/McpTestServer.API/Extensions/ServiceCollectionExtensions.cs#L308-L315) e [il README fissato alle righe 197–220](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/README.md#L197-L220). I sei scenari predefiniti sono `baseline`, `errors.http_status_sequence`, `errors.jsonrpc_error`, `errors.tool_error`, `elicitation.approval` e `compat.sdk_v2`. Altre implementazioni di timeout, autenticazione e catalogo si trovano nel repository, ma non risultano abilitate di default.

Il cambio di scenario ha anche una dimensione temporale: secondo il README la selezione salvata vale per la **sessione MCP successiva**, non modifica quelle già attive. Se dopo aver scelto 503 compare ancora una vecchia risposta positiva, controllerei sessione e sequenza POST prima di attribuirla alla cache del client.

La [matrice di `SdkV2CompatScenario.cs`, righe 74–106](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/src/McpTestServer.API/Scenarios/Compat/SdkV2CompatScenario.cs#L74-L106) riguarda l'host stateless di esempio: supporta MRTR nativo stateless del 2026-07-28, ma non abilita il percorso stateful né il bridge stateful del 2025-11-25. I vecchi client stateless possono ricevere istruzioni per ripetere la chiamata aggiungendo `closeReason`. **È il perimetro di quel server dimostrativo, non il comportamento universale dei client MCP.**

Uno sguardo statico a un SDK reale offre un confronto concreto. La [documentazione di `Client.callTool()` nell'SDK TypeScript](https://github.com/modelcontextprotocol/typescript-sdk/blob/9bc40e903fde15534ec06ff4c1347d47000dbe16/packages/client/src/client/client.ts#L2286-L2306) distingue l'errore dello strumento **restituito come `result.isError`** dall'errore di protocollo **sollevato come `ProtocolError`**. Il gestore sottostante [`_onresponse`](https://github.com/modelcontextprotocol/typescript-sdk/blob/9bc40e903fde15534ec06ff4c1347d47000dbe16/packages/core-internal/src/shared/protocol.ts#L1213-L1230) conserva `error.code/message/data`. Al contrario, la [funzione dimostrativa del client TypeScript di Cakewalk](https://github.com/cakewalk-security/cakewalk-mcp-ts-test-client/blob/4a0ff892bca6557ea97d427408cf888e2644000e/src/index.ts#L95-L108) estrae e stampa principalmente testo e, in quella funzione, non gestisce autonomamente una diramazione `isError`. Sono osservazioni del sorgente, non test di ripristino effettuati sul client.

## Classificare gli errori MCP con undici test sintetici locali in Node.js

Con Linux e Node.js `v22.16.0` la prima versione ha superato **9/9** casi. Correggendo la compatibilità con il vecchio `resultType` è arrivata a **10/10**; aggiungendo un risultato con struttura non valida, a **11/11**. Ho conservato sorgenti e registri TAP separati. I test usano `node:http`, `node:events`, `node:test`, `node:assert/strict` e `fetch` integrato, su una porta casuale di `127.0.0.1`, senza pacchetti npm esterni.

La prima versione giudicava `INVALID_TOOL_RESULT` una risposta valida priva di `resultType`. La [sezione ResultType del protocollo base 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic#resulttype) richiede, per server precedenti: “clients MUST treat an absent `resultType` as `"complete"`”. Gli [esempi del 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) non hanno quel campo. Un validatore rigido senza fallback rifiuta dunque risultati legittimi.

Ho aggiunto un caso con `content` scritto come stringa `"not-an-array"` anziché array. Il classificatore ha restituito `INVALID_TOOL_RESULT`. L'ultima esecuzione ha prodotto **11 test superati, zero errori, codice di uscita 0**, osservando tutti gli otto valori `outcome` previsti.

| Input sintetico | Risultato osservato | Significato |
| --- | --- | --- |
| Normale risultato strumento con HTTP 200 | `TOOL_SUCCESS` | Caso di controllo positivo |
| Primo POST 503, il test invia esplicitamente un secondo POST con 200 | `HTTP_ERROR` → `TOOL_SUCCESS` | Il secondo invio è del test, non un retry automatico |
| HTTP 200 con `error: -32602` JSON-RPC | `JSONRPC_ERROR` | La sola verifica HTTP segnerebbe un falso successo |
| HTTP 200 con `isError: true` | `TOOL_ERROR` | Successo del protocollo non significa successo dello strumento |
| HTTP 500 con corpo apparentemente valido | `HTTP_ERROR` | Il trasporto ha precedenza |
| JSON malformato | `MALFORMED_JSON` | Errore di parsing non equivale a completamento |
| `result` ed `error` insieme | `INVALID_ENVELOPE` | Busta JSON-RPC contraddittoria |
| `id` della risposta diverso | `INVALID_ENVELOPE` | Associare sempre la risposta alla richiesta |
| `input_required` | `INPUT_REQUIRED` | Serve altro input, non è un successo concluso |
| Vecchio risultato valido senza `resultType` | **`TOOL_SUCCESS`** | Fallback di compatibilità obbligatorio |
| `content: "not-an-array"` | **`INVALID_TOOL_RESULT`** | Struttura del risultato non valida |

Gli otto valori sono `TOOL_SUCCESS`, `HTTP_ERROR`, `JSONRPC_ERROR`, `TOOL_ERROR`, `INPUT_REQUIRED`, `MALFORMED_JSON`, `INVALID_ENVELOPE` e `INVALID_TOOL_RESULT`. **Tutti e otto sono comparsi nei test**, ma ciò non implica una copertura al 100% dei rami del codice né una certificazione ufficiale di conformità MCP.

Il fix di retrocompatibilità è concentrato in due righe di `classifyToolCall`:

```js
const result = response.result;
const resultType = has(result, 'resultType') ? result.resultType : 'complete';
```

Salvare il programma seguente come `mcp-error-layers-v3.test.mjs` ed eseguirlo con Node.js 22:

```sh
node --test --test-reporter=tap mcp-error-layers-v3.test.mjs
```

<details>
<summary>Espandi: codice Node.js completo ed eseguibile degli undici casi sintetici</summary>

```js
/**
 * First-party synthetic MCP-like response classifier. Offline loopback only.
 * Node.js 22; no MCP SDK, Cakewalk runtime, credentials or external network.
 * 2026-07-28 base-protocol compatibility: missing resultType is "complete".
 * This deliberately omits real MCP negotiation, _meta, authorization and MRTR.
 * Run: node --test --test-reporter=tap mcp-error-layers-v3.test.mjs
 */
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { createServer } from 'node:http';
import { test } from 'node:test';

const has = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
const good = { jsonrpc: '2.0', id: 1, result: {
  resultType: 'complete', content: [{ type: 'text', text: 'ok' }], isError: false,
} };
const rpcError = { jsonrpc: '2.0', id: 1, error: {
  code: -32602, message: 'synthetic invalid params',
} };
const toolError = { jsonrpc: '2.0', id: 1, result: {
  resultType: 'complete', content: [{ type: 'text', text: 'failed' }], isError: true,
} };

export function classifyToolCall({ status, body, expectedId = 1 }) {
  if (status < 200 || status >= 300) {
    return { layer: 'transport', outcome: 'HTTP_ERROR', status };
  }
  let response;
  try { response = JSON.parse(body); }
  catch { return { layer: 'protocol', outcome: 'MALFORMED_JSON' }; }
  if (!response || Array.isArray(response) || typeof response !== 'object' ||
      response.jsonrpc !== '2.0' || response.id !== expectedId ||
      has(response, 'result') === has(response, 'error')) {
    return { layer: 'protocol', outcome: 'INVALID_ENVELOPE' };
  }
  if (has(response, 'error')) {
    if (!response.error || !Number.isInteger(response.error.code) ||
        typeof response.error.message !== 'string') {
      return { layer: 'protocol', outcome: 'INVALID_ENVELOPE' };
    }
    return { layer: 'protocol', outcome: 'JSONRPC_ERROR', code: response.error.code };
  }
  const result = response.result;
  if (!result || Array.isArray(result) || typeof result !== 'object') {
    return { layer: 'protocol', outcome: 'INVALID_TOOL_RESULT' };
  }
  // Normative 2026-07-28 fallback: absent resultType means "complete".
  const resultType = has(result, 'resultType') ? result.resultType : 'complete';
  if (resultType === 'input_required') {
    return { layer: 'interaction', outcome: 'INPUT_REQUIRED' };
  }
  if (resultType !== 'complete' || !Array.isArray(result.content) ||
      (has(result, 'isError') && typeof result.isError !== 'boolean')) {
    return { layer: 'protocol', outcome: 'INVALID_TOOL_RESULT' };
  }
  return result.isError === true
    ? { layer: 'application', outcome: 'TOOL_ERROR' }
    : { layer: 'application', outcome: 'TOOL_SUCCESS' };
}

async function withPeer(t, reply) {
  const calls = [];
  const server = createServer(async (req, res) => {
    let raw = '';
    for await (const part of req) raw += part.toString('utf8');
    calls.push({ method: req.method, path: req.url, body: raw });
    const next = reply(calls.length);
    res.writeHead(next.status, { 'content-type': 'application/json' });
    res.end(typeof next.body === 'string' ? next.body : JSON.stringify(next.body));
  });
  server.listen(0, '127.0.0.1');
  await once(server, 'listening');
  t.after(() => new Promise((resolve, reject) =>
    server.close(error => error ? reject(error) : resolve())));
  return { url: `http://127.0.0.1:${server.address().port}/mcp`, calls };
}
async function onePost(url) {
  const response = await fetch(url, { method: 'POST',
    headers: { 'content-type': 'application/json', accept: 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call',
      params: { name: 'synthetic', arguments: {} } }),
  });
  const body = await response.text();
  return classifyToolCall({ status: response.status, body });
}
function record(t, name, outcome, calls) {
  t.diagnostic(JSON.stringify({ synthetic: true, name, outcome, postCount: calls.length }));
}
async function testCase(t, name, wire, expected) {
  const peer = await withPeer(t, () => wire);
  const actual = await onePost(peer.url);
  assert.deepEqual(actual, expected);
  assert.equal(peer.calls.length, 1);
  record(t, name, actual, peer.calls);
}

test('baseline complete -> TOOL_SUCCESS', t => testCase(t, 'baseline',
  { status: 200, body: good }, { layer: 'application', outcome: 'TOOL_SUCCESS' }));
test('503 then 200 requires explicit second POST', async t => {
  const peer = await withPeer(t, n => n === 1
    ? { status: 503, body: { note: 'synthetic outage' } }
    : { status: 200, body: good });
  const first = await onePost(peer.url);
  assert.deepEqual(first, { layer: 'transport', outcome: 'HTTP_ERROR', status: 503 });
  assert.equal(peer.calls.length, 1, 'no automatic retry by toy client');
  const second = await onePost(peer.url); // Only this explicit call creates POST #2.
  assert.deepEqual(second, { layer: 'application', outcome: 'TOOL_SUCCESS' });
  assert.equal(peer.calls.length, 2);
  record(t, 'explicit_http_503_then_200', first, peer.calls);
});
test('HTTP 200 + JSON-RPC error -> JSONRPC_ERROR', t => testCase(t, 'jsonrpc_error',
  { status: 200, body: rpcError },
  { layer: 'protocol', outcome: 'JSONRPC_ERROR', code: -32602 }));
test('HTTP 200 + tool isError -> TOOL_ERROR', t => testCase(t, 'tool_isError',
  { status: 200, body: toolError }, { layer: 'application', outcome: 'TOOL_ERROR' }));
test('HTTP 500 with success-looking body -> HTTP_ERROR', t => testCase(t, 'transport_precedence',
  { status: 500, body: good }, { layer: 'transport', outcome: 'HTTP_ERROR', status: 500 }));
test('malformed JSON -> MALFORMED_JSON', t => testCase(t, 'malformed_json',
  { status: 200, body: '{unclosed' }, { layer: 'protocol', outcome: 'MALFORMED_JSON' }));
test('result and error together -> INVALID_ENVELOPE', t => testCase(t, 'conflicting_envelope',
  { status: 200, body: { ...good, error: rpcError.error } },
  { layer: 'protocol', outcome: 'INVALID_ENVELOPE' }));
test('wrong JSON-RPC id -> INVALID_ENVELOPE', t => testCase(t, 'wrong_response_id',
  { status: 200, body: { ...good, id: 99 } },
  { layer: 'protocol', outcome: 'INVALID_ENVELOPE' }));
test('input_required is not successful completion', t => testCase(t, 'input_required',
  { status: 200, body: { jsonrpc: '2.0', id: 1, result: {
    resultType: 'input_required', inputRequests: { approval: {
      method: 'elicitation/create', params: { mode: 'form', message: 'synthetic input' },
    } },
  } } }, { layer: 'interaction', outcome: 'INPUT_REQUIRED' }));
test('legacy result without resultType -> TOOL_SUCCESS', t => testCase(t, 'legacy_resultType_absent',
  { status: 200, body: { jsonrpc: '2.0', id: 1,
    result: { content: [{ type: 'text', text: 'legacy ok' }], isError: false } } },
  { layer: 'application', outcome: 'TOOL_SUCCESS' }));
test('invalid tool content shape -> INVALID_TOOL_RESULT', t => testCase(t, 'invalid_tool_result',
  { status: 200, body: { jsonrpc: '2.0', id: 1, result: {
    resultType: 'complete', content: 'not-an-array', isError: false,
  } } }, { layer: 'protocol', outcome: 'INVALID_TOOL_RESULT' }));
```

</details>

L'area espandibile contiene **tutto il codice eseguibile**. Può essere copiato nel file e avviato con il comando precedente. Questo è il riepilogo TAP archiviato:

```text
# tests 11
# pass 11
# fail 0
```

Il repository di ricerca è privato. I collegamenti al sorgente originale, al TAP completo e alla ricevuta d'esecuzione **sono disponibili solo ai collaboratori autorizzati** e non sono download pubblici. Lo SHA-256 del file originale è `0f4616594ea14f23081b3da60d91808363b3b5b24a0889c560332bb5977f1cb2` e ne verifica i byte originali; copiando dal browser possono cambiare i ritorni a capo. Anche i precedenti registri 9/9 e 10/10 restano conservati.

## Diagnostica dei client MCP reali: log e limiti di retry sicuro

Per analizzare un client reale bisogna associare ogni richiesta alla risposta, non guardare soltanto l'ultimo indicatore verde o rosso dell'interfaccia. Un possibile record con **valori illustrativi**, non derivati da una cattura, è il seguente:

```json
{
  "client": "your-client@version",
  "protocolVersion": "2026-07-28",
  "scenario": "errors.jsonrpc_error",
  "method": "tools/call",
  "postCount": 1,
  "httpStatuses": [200],
  "jsonrpc": { "id": 1, "errorCode": -32602 },
  "resultType": null,
  "outcome": "JSONRPC_ERROR",
  "retryOrigin": "none"
}
```

Versione del client, protocollo negoziato e `_meta` provengono dai log del client; configurazione dello scenario, conteggi POST e ordine dai log server. Registrare stato HTTP, `Content-Type`, `id` JSON-RPC e `error.code`, `error.message` e `error.data` dopo la rimozione dei dati sensibili. Il classificatore minimo restituisce soprattutto codici e non è un sistema di diagnostica per la produzione. `retryOrigin` distingue un retry dell'SDK, un reinvio della logica applicativa e una ripetizione manuale.

| Osservazione | Retry automatico? | Condizioni preliminari |
| --- | --- | --- |
| HTTP 503 o problema temporaneo di rete | **Condizionato** | Accertare sola lettura o idempotenza; limitare durata e tentativi; fermarsi con effetti collaterali incerti |
| HTTP 429 per rate limit | **Condizionato** | Rispettare `Retry-After` se presente; altrimenti backoff limitato e quote del servizio, senza eludere i limiti |
| JSON-RPC `-32602`, parametri errati | **Di norma non con gli stessi parametri** | Correggere gli argomenti e fare una nuova chiamata |
| Fallimento dello strumento `isError: true` | **Dipende dalla semantica** | Conservare i dettagli e controllare input e stato applicativo; non ripetere alla cieca |
| `MALFORMED_JSON` / `INVALID_ENVELOPE` / `INVALID_TOOL_RESULT` | **No retry indiscriminato** | Salvare risposta ripulita, ID richiesta e versione del protocollo, indagare la compatibilità |
| `INPUT_REQUIRED` | **Non è un retry per guasto** | Ottenere i dati MRTR e inviare una chiamata successiva con `inputResponses` |

Per letture certamente prive di effetti collaterali, si può iniziare con **non più di due retry, backoff esponenziale iniziale di 200 ms e jitter**, adattando la scelta a SLA e rate limit. In caso di 429, un effettivo `Retry-After` inviato dal server ha precedenza. Pagamenti, scritture su database e notifiche non vanno ripetuti solo perché è arrivato 503, se mancano chiavi d'idempotenza o deduplicazione lato server. Con SSE registrare anche `Content-Type`, numero di eventi, presenza della risposta JSON-RPC finale e causa dell'interruzione. **HTTP 200 senza risultato conclusivo non è successo dello strumento.**

La strategia di recupero deve dipendere da **quale livello ha fallito, se esiste un risultato finale e se la ripetizione è sicura**, non dal solo numero 200 o 503.

<details>
<summary>Approfondimento: strumenti, installazione locale e limiti del servizio ospitato</summary>

[MCP Inspector](https://github.com/modelcontextprotocol/inspector) consente l'ispezione manuale dei messaggi, [MCP Conformance ufficiale](https://github.com/modelcontextprotocol/conformance) verifica la conformità del protocollo e Cakewalk Test Kitchen inietta guasti per osservare il client. Sono strumenti con obiettivi diversi.

Il [README fissato di Cakewalk](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/README.md#L18-L39) richiede .NET 10 SDK e Docker/PostgreSQL in locale; lo sviluppo dell'interfaccia usa Node.js 22.17.1. Per `/mcp` locale si può usare un PAT di test; la configurazione Google OAuth dell'amministrazione è separata.

Le [condizioni d'uso del servizio ospitato Cakewalk (data indicata 2026-09-21)](https://www.cakewalk.security/mcp-test-kitchen/legal/terms) vietano di collegare l'endpoint ufficiale a CI o test di carico automatizzati (`5.2), impongono **120 richieste MCP al minuto per IP** con HTTP 429 oltre la soglia (`5.3) e non garantiscono che i guasti iniettati corrispondano a quelli reali (`8). Le condizioni descrivono ancora il server come non open source, mentre il repository pubblico fissato contiene una [MIT LICENSE](https://github.com/cakewalk-security/cakewalk-mcp-test-kitchen/blob/cf2f6897b9712ec0cb9a7e01abee5109385fcd8b/LICENSE). Licenza del codice e regole del servizio ospitato vanno valutate separatamente. **Tutti i test automatici dell'articolo sono avvenuti su loopback locale.**

</details>

Due articoli completano il contesto: [allarmi HTTP 503 e recupero con Gatus](/posts/gatus-docker-compose-http-503-alert-recovery/) tratta il rilevamento dei guasti; [compressione dei log Claude Code e conservazione degli errori](/posts/claude-code-context-diet-log-compression/) parla delle prove da mantenere. Questo testo copre il passaggio intermedio: **capire in quale livello sia avvenuto il guasto**.

*Test su loopback locale registrati il 2026-10-17 (UTC), undici casi sintetici. Sorgente Cakewalk fissato al commit `cf2f6897b9712ec0cb9a7e01abee5109385fcd8b`. Non sono stati eseguiti test di retry con client MCP reali, disconnessione SSE o interoperabilità MRTR.*
