MCP-Client-Fehler unterscheiden: HTTP 503, JSON-RPC error und isError
Ein MCP-Client ruft ein Tool auf, der Server antwortet mit HTTP 200, und der Agent meldet trotzdem einen Fehler. Das ist kein Widerspruch: Ein erfolgreicher HTTP-Status bedeutet weder, dass die JSON-RPC-Antwort kein error enthält, noch, dass isError im Tool-Ergebnis false ist.
In MCP-Gateways, Integrationen für Coding Agents und eigenen Clients übersieht eine reine Prüfung von response.ok zwei wichtige Fehlerarten. Zuerst muss der HTTP-Transport beurteilt werden, danach die JSON-RPC-Antworthülle, schließlich das Tool-Ergebnis. Ob ein Aufruf wiederholt werden darf, ist eine eigenständige Entscheidung.
Ich habe Cakewalk MCP Test Kitchen beim festen Commit cf2f689 untersucht und einen unabhängigen Node.js-Loopback-Klassifikator ausgeführt. Alle elf synthetischen Testfälle bestanden. Das bestätigt die selbst geschriebene Klassifizierung, nicht die Interoperabilität mit dem .NET-Server von Cakewalk oder realen MCP-Clients wie Cursor und Codex.
Drei Fehlerebenen eines tools/call: HTTP, JSON-RPC und Tool-Ergebnis
Der Abschnitt Error Handling der MCP-Tools-Spezifikation vom 28. Juli 2026 trennt Protokollfehler von Fehlern der Tool-Ausführung. Zusammen mit dem HTTP-Transport ergeben sich drei Ebenen:
| Ebene | Beispiel | Was der Client tatsächlich weiß |
|---|---|---|
| HTTP-Transport | 503 Service Unavailable | Die HTTP-Anfrage schlug fehl; Nebenwirkungen und automatisches Wiederholen sind unbekannt |
| JSON-RPC-Protokoll | HTTP 200, oberste Felder error.code und error.message | Ein JSON-RPC-Fehler liegt vor; es gibt kein erfolgreiches Tool-Ergebnis |
| Tool-Ausführung | HTTP 200, result.isError: true | Der Protokollaustausch kann abgeschlossen sein, obwohl das Tool gescheitert ist |
Auch bei HTTP 200 kann eine JSON-RPC-Fehlerantwort vorliegen:
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32602, "message": "Invalid params" }
}
Diese andere HTTP-200-Antwort steht dagegen für einen Fehler innerhalb des Tools:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"content": [{ "type": "text", "text": "Tool-side failure" }],
"isError": true
}
}
Beide JSON-Beispiele sind Antwortkörper des Servers. Vollständige Anfragen müssen zusätzlich _meta und die vorgeschriebenen HTTP-Header berücksichtigen.
Laut dem CallToolResult-Schema von 2026-07-28 ist isError ein optionales Boolean-Feld, das bei Abwesenheit als false gilt. Das Zurückfallen von fehlendem resultType auf complete ist eine andere Kompatibilitätsregel. structuredContent ist ebenfalls möglich und muss kein Objekt sein, doch content bleibt in dieser Schemaversion als Array verpflichtend. Unser Klassifikator prüft diesen begrenzten Fall, nicht sämtliche MCP-Versionen und SDK-Varianten.
Fehlermeldungen eines Tools können dem Sprachmodell helfen, Argumente zu korrigieren. Im Abschnitt Error Handling steht „Clients SHOULD provide tool execution errors to language models“; für Protokollfehler heißt es „Clients MAY provide protocol errors to language models“. SHOULD und MAY beziehen sich auf die Weitergabe an das Modell, nicht auf eine Pflicht zur unbedingten Wiederholung der Anfrage. Das ist bei Tools mit Nebenwirkungen besonders wichtig.
resultType: "input_required" zeigt an, dass ein Aufruf weitere Eingaben benötigt und noch nicht abgeschlossen ist. MRTR (Multi Round-Trip Requests, mehrstufige Anfragen) liefert dazu inputRequests. Der Client sammelt Antworten und sendet sie als inputResponses, gegebenenfalls mit requestState, in einer neuen Anfrage. Dabei muss die JSON-RPC-id wechseln. Es handelt sich weder um einen Tool-Fehler noch um einen gewöhnlichen Timeout-Retry.
Bei Streamable HTTP gibt es eine subtilere Grenze. Laut Transport-Spezifikation 2026-07-28 kann dieselbe Anfrage application/json oder text/event-stream erhalten. HTTP 200 zusammen mit SSE besagt lediglich, dass der Antwortstrom begonnen hat. Der Strom kann vor der finalen JSON-RPC-Antwort abbrechen, oder die finale Antwort enthält error. Clients müssen nach Content-Type unterscheiden und bis zum passenden terminalen Ergebnis warten. Das ist ein Problem der Vollständigkeit des Streams, keine mechanische vierte Art von Tool-Fehler.
JSON-RPC-Benachrichtigungen besitzen keine id. Für angenommene Notification-POSTs beschreibt die Transport-Spezifikation 202 Accepted mit leerem Körper. Die Kernfassung von Streamable HTTP 2026-07-28 definiert keine gewöhnlichen clientseitigen HTTP-Benachrichtigungen; ein Abbruch wird durch Schließen des Antwortstroms ausgedrückt. Eine leere 202-Antwort darf nicht in einen tools/call-Klassifikator geraten, der einen Ergebniskörper erwartet.
Cakewalk im Quellcode: 503 → 200 beweist keinen automatischen Retry
Cakewalk lässt den Server konfigurierte Störungen absichtlich erzeugen. Der feste Quellcode zeigt, an welcher Stelle sie entstehen; die eigenständigen lokalen Tests folgen im nächsten Abschnitt.
In HttpStatusSequenceScenario.cs, Zeilen 12–35 zählt ScenarioWireInvocationKeys.HttpRequest die MCP-POSTs einer Sitzung, nicht ausschließlich die Aufrufe von tools/call. Die Einstellung {"statusCodes":[503,200]} gibt beim ersten gezählten POST 503 zurück und lässt einen tatsächlich eintreffenden zweiten POST mit 200 weiterlaufen. Auch ein anderer POST kann den ersten Status verbrauchen. Beispiel: tools/list → erster POST mit 503 → Client beendet den Vorgang → tools/call findet gar nicht statt. Dies ist eine Ableitung aus dem Zähler, kein Paketmitschnitt. Die Folge [503,200] beweist für sich genommen keinen Retry.
Dagegen gibt JsonRpcErrorScenario.cs für tools/call HTTP 200 mit JSON-RPC-error zurück; ToolErrorScenario.cs verwendet die isError-Variante von CallToolResult. Es sind drei unterschiedliche Stellen der Fehler-Injektion.
Die DI-Registrierung in Zeilen 308–315 und die fixierte README in Zeilen 197–220 nennen sechs standardmäßig aktivierte Szenarien: baseline, errors.http_status_sequence, errors.jsonrpc_error, errors.tool_error, elicitation.approval und compat.sdk_v2. Zusätzliche Implementierungen für Timeout, Authentifizierung oder Kataloge liegen zwar im Repository, sind aber nicht standardmäßig registriert.
Auch der Zeitpunkt eines Szenariowechsels ist wichtig: Die gespeicherte Auswahl gilt laut README für die nächste MCP-Sitzung, nicht rückwirkend für bestehende Sitzungen. Wenn nach Auswahl von 503 immer noch ein alter normaler Status erscheint, sollte man zuerst Sitzung und POST-Reihenfolge prüfen statt sofort einen Client-Cache zu verdächtigen.
Die Kompatibilitätsmatrix in SdkV2CompatScenario.cs, Zeilen 74–106 beschreibt diesen stateless-Beispielhost. Das native MRTR von 2026-07-28 wird unterstützt, während ein stateful-Host-Pfad und die stateful-Brücke von 2025-11-25 hier nicht aktiviert sind. Ältere stateless-Clients können aufgefordert werden, closeReason mitzusenden und erneut aufzurufen. Diese Einschränkung betrifft das Beispielprojekt, nicht alle MCP-Clients.
Ein statischer Blick in echte SDK-Quellen liefert eine weitere Unterscheidung: Die offizielle TypeScript-SDK-Dokumentation zu Client.callTool() trennt einen als result.isError zurückgegebenen Tool-Fehler von einem geworfenen ProtocolError. Der darunterliegende Handler _onresponse bewahrt error.code/message/data auf. Im öffentlichen TypeScript-Beispielclient von Cakewalk extrahiert die Ausgabefunktion dagegen vor allem Text; sie hat in dieser Funktion keinen eigenen isError-Zweig. Das sind Befunde aus Quelltext, keine ausgeführten Wiederherstellungstests des Beispielclients.
MCP-Fehlerklassifikation mit elf lokalen synthetischen Node.js-Tests
Unter Linux mit Node.js v22.16.0 bestand die erste Fassung 9/9 Fälle. Nach Behebung der älteren resultType-Kompatibilität waren es 10/10. Ein zusätzlicher Fall mit ungültigem Tool-Inhalt brachte 11/11. Die Quellen und TAP-Protokolle aller Fassungen bleiben separat erhalten. Verwendet werden node:http, node:events, node:test, node:assert/strict und das eingebaute fetch auf einem zufälligen 127.0.0.1-Port, ohne npm-Abhängigkeiten.
Die frühere Version stufte ein legitimes Ergebnis ohne resultType als INVALID_TOOL_RESULT ein. Der ResultType-Abschnitt der Basisspezifikation 2026-07-28 fordert für alte Server: „clients MUST treat an absent resultType as "complete"“. Den Beispielen der Tools-Spezifikation 2025-11-25 fehlt dieses Feld. Ohne Fallback würde ein strenger Client gültige alte Antworten ablehnen.
Danach fügte ich den Fall content: "not-an-array" hinzu: ein String statt des erforderlichen Arrays. Das Ergebnis war INVALID_TOOL_RESULT. Die aktuelle Ausführung meldet elf bestandene Tests, null Fehler und Exit-Code 0. Alle acht vorgesehenen outcome-Werte wurden in synthetischen Eingaben beobachtet.
| Synthetische Eingabe | Beobachteter Wert | Bedeutung |
|---|---|---|
| Normales HTTP-200-Tool-Ergebnis | TOOL_SUCCESS | Erfolgreicher Kontrollfall |
| Erster POST 503, Test sendet explizit einen zweiten POST mit 200 | HTTP_ERROR → TOOL_SUCCESS | Kein automatischer Retry: der Test sendet selbst |
HTTP 200 mit JSON-RPC error: -32602 | JSONRPC_ERROR | Nur HTTP zu prüfen wäre falsch |
HTTP 200 mit isError: true | TOOL_ERROR | Protokollerfolg ist nicht Tool-Erfolg |
| HTTP 500 mit scheinbar erfolgreichem Körper | HTTP_ERROR | Transportstatus hat Vorrang |
| Ungültiges JSON | MALFORMED_JSON | Parsing-Fehler ist kein Abschluss |
result und error gleichzeitig | INVALID_ENVELOPE | Widersprüchliche Antwort-Hülle |
Falsche Antwort-id | INVALID_ENVELOPE | Antwort muss zur Anfrage passen |
input_required | INPUT_REQUIRED | Weitere Eingabe statt endgültigem Erfolg |
Legitime alte Antwort ohne resultType | TOOL_SUCCESS | Verbindlicher Kompatibilitäts-Fallback |
content: "not-an-array" | INVALID_TOOL_RESULT | Ungültige Tool-Ergebnisstruktur |
Die acht definierten Werte heißen TOOL_SUCCESS, HTTP_ERROR, JSONRPC_ERROR, TOOL_ERROR, INPUT_REQUIRED, MALFORMED_JSON, INVALID_ENVELOPE und INVALID_TOOL_RESULT. Alle acht Klassifikationsergebnisse wurden beobachtet. Daraus folgt weder eine 100-prozentige Abdeckung sämtlicher Codezweige noch eine offizielle MCP-Konformitätsprüfung.
Die entscheidende Abwärtskompatibilität steckt in diesen beiden Zeilen aus classifyToolCall:
const result = response.result;
const resultType = has(result, 'resultType') ? result.resultType : 'complete';
Das vollständige Programm unten als mcp-error-layers-v3.test.mjs speichern und mit Node.js 22 ausführen:
node --test --test-reporter=tap mcp-error-layers-v3.test.mjs
Aufklappen: vollständiger ausführbarer Node.js-Quelltext für elf synthetische Tests
/**
* 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' }));
Der Bereich enthält den vollständigen ausführbaren Quelltext. Nach dem Speichern lässt er sich mit dem angegebenen Befehl erneut testen. Die archivierte TAP-Zusammenfassung lautet:
# tests 11
# pass 11
# fail 0
Das Forschungs-Repository ist privat. Die Links zu Originalquelltext, vollständigem TAP und Ausführungsbeleg sind nur für berechtigte Repository-Mitarbeitende zugänglich; sie sind keine öffentlichen Downloads. Der SHA-256 der archivierten Quelldatei lautet 0f4616594ea14f23081b3da60d91808363b3b5b24a0889c560332bb5977f1cb2 und gilt für die Originalbytes. Beim Kopieren aus dem Browser können Zeilenumbrüche anders ausfallen. Die älteren 9/9- und 10/10-Daten wurden nicht überschrieben.
Diagnose realer MCP-Clients: Logfelder und sichere Retry-Grenzen
Bei echten Clients sollten Anfragen und Antworten miteinander verknüpft werden, statt nur auf den abschließenden Status in der Oberfläche zu schauen. Eine mögliche Logstruktur mit beispielhaften, nicht gemessenen Werten ist:
{
"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"
}
Clientversion, ausgehandelte Protokollversion und _meta stammen aus Clientlogs; Szenario-Konfiguration, POST-Zähler und zeitliche Reihenfolge sollten mit Serverlogs abgeglichen werden. HTTP-Status, Content-Type, JSON-RPC-id sowie bereinigte error.code, error.message und error.data gehören ins Protokoll. Der kleine Testklassifikator gibt überwiegend nur Codes zurück und ersetzt kein Diagnosesystem. retryOrigin trennt SDK-Retry, erneutes Senden durch Geschäftslogik und manuellen Klick.
| Beobachtung | Automatischer Retry? | Erforderliche Bedingungen |
|---|---|---|
| HTTP 503 oder vorübergehender Netzwerkfehler | Bedingt | Nur wenn Lesen oder Idempotenz bestätigt ist; Timeout und Versuche begrenzen, bei unklaren Nebenwirkungen stoppen |
| HTTP 429, Rate Limit | Bedingt | Retry-After respektieren, falls vorhanden; sonst Kontingent und begrenzten Backoff beachten, Limits nicht umgehen |
JSON-RPC -32602, falsche Parameter | Normalerweise nicht mit denselben Argumenten | Parameter korrigieren und neu aufrufen |
isError: true, Tool-Fehler | Je nach Tool-Semantik | Fehlermeldung erhalten, Eingabe und Geschäftsstatus prüfen, nicht blind wiederholen |
MALFORMED_JSON / INVALID_ENVELOPE / INVALID_TOOL_RESULT | Kein blinder Retry | Bereinigte Antwort, Anfrage-ID und Protokollversion sichern, Implementierung prüfen |
INPUT_REQUIRED | Kein Fehler-Retry | MRTR-Eingaben sammeln und Folgeanfrage mit inputResponses senden |
Für nachweislich nebenwirkungsfreie Lesezugriffe kann man mit maximal zwei Retries, anfänglich 200 ms exponentiellem Backoff plus Jitter beginnen und an SLA und Rate Limits anpassen. Falls bei 429 ein tatsächliches Retry-After vorliegt, hat es Vorrang. Zahlungen, Datenbank-Schreibvorgänge und Benachrichtigungen dürfen ohne Idempotenzschlüssel oder serverseitige Deduplizierung nicht allein wegen 503 wiederholt werden. Bei SSE sollten außerdem Content-Type, Ereignisanzahl, finaler JSON-RPC-Status und Abbruchgrund erfasst werden. HTTP 200 ohne finales Ergebnis ist kein Tool-Erfolg.
Die Wiederherstellungsstrategie hängt von Fehlerebene, Vorliegen des Endergebnisses und Sicherheit einer Wiederholung ab, nicht bloß von den Zahlen 200 und 503.
Zusatz: Testwerkzeuge, lokale Installation und Hosted-Service-Grenzen
MCP Inspector eignet sich zur manuellen Nachrichteninspektion, offizielle MCP Conformance zur Protokollprüfung und Cakewalk Test Kitchen zur Beobachtung von Störungen. Die Aufgaben unterscheiden sich.
Nach der fixierten Cakewalk-README braucht die lokale Umgebung .NET 10 SDK und Docker/PostgreSQL; für die Weboberfläche wird Node.js 22.17.1 verwendet. Ein Test-PAT kann lokal für /mcp genutzt werden. Die Google-OAuth-Konfiguration für die Administration ist eine separate Frage.
Die Cakewalk-Nutzungsbedingungen (Standangabe 2026-09-21) untersagen in 5.2 die Einbindung des gehosteten Endpunkts in CI oder automatisierte Lasttests. 5.3 nennt 120 MCP-Anfragen pro Minute und IP und HTTP 429 bei Überschreitung; `8 garantiert keine Übereinstimmung injizierter Störungen mit realen Fehlerbildern. Die Bedingungen bezeichnen den Server weiterhin als nicht quelloffen, obwohl das fixierte öffentliche Repository eine MIT LICENSE enthält. Quelllizenz und Regeln des Hosted-Dienstes sind getrennt zu prüfen. Alle automatisierten Tests hier liefen ausschließlich auf lokalem Loopback.
Zwei verwandte Beiträge ergänzen die Diagnose: Gatus: HTTP-503-Alarme und Wiederherstellung behandelt die Erkennung von Störungen; Claude Code: Fehlererhalt bei Logkomprimierung behandelt die Sicherung von Fehlerevidenz. Dazwischen liegt die Frage dieses Artikels: Auf welcher Ebene ist der Aufruf gescheitert?
Lokale Loopback-Tests: 2026-10-17 (UTC), elf synthetische Fälle. Cakewalk-Quellcode fixiert auf cf2f6897b9712ec0cb9a7e01abee5109385fcd8b. Keine realen Client-Recovery-, SSE-Abbruch- oder MRTR-Interoperabilitätstests durchgeführt.