Anschließen und scheitern lassen
Die drei typischen Fehlerbilder ausgelöst, nicht behauptet - und eines davon sieht wie ein Erfolg aus.
Meilenstein Unbekanntes Werkzeug, fehlende Pflichtangabe und ein Ergebnis mit 130 152 Zeichen stehen als echte Ausgaben da.
Der Server läuft. Jetzt kommt der Teil, den du nicht überspringen solltest: Du schließt ihn an und bringst ihn absichtlich zum Scheitern.
Fehlerbilder, die du kennst, kosten Minuten. Fehlerbilder, die du das erste Mal im Betrieb siehst, kosten einen Nachmittag. Und das dritte in diesem Kapitel sieht nicht einmal wie ein Fehler aus.
An einen Harness hängen
Was braucht ein Client für stdio? Zwei Angaben: womit der Prozess startet und wo. Bei Claude Code und Claude Desktop steht das in einer JSON-Datei:
{
"mcpServers": {
"simhaven-helpdesk": {
"command": "node",
"args": ["--experimental-strip-types", "/pfad/zu/mcp-kurs/helpdesk/stdio.ts"]
}
}
}
Für Streamable HTTP tritt die Adresse an die Stelle des Befehls, und das Token kommt als Kopfzeile dazu. Welcher Schlüssel dafür gilt, unterscheidet sich von Client zu Client. Hier gilt die Dokumentation deines Harness und nicht diese Seite.
Der Prüfstand bleibt trotzdem
Ein angeschlossener Harness sagt „funktioniert“ oder „funktioniert nicht“. Warum, sagt er nicht. Deshalb bleibt der Client aus Kapitel 1 daneben stehen, jetzt mit Mitschrift:
// helpdesk/lauf.ts
const protokoll: { werkzeug: string; args: unknown; zeichen: number; ms: number }[] = [];
async function ruf(name: string, args: Record<string, unknown>) {
const start = Date.now();
const r = await client.callTool({ name, arguments: args });
const text = (r as { content: { text: string }[] }).content[0]!.text;
protokoll.push({ werkzeug: name, args, zeichen: text.length, ms: Date.now() - start });
return text;
}
Vier Spalten, mehr brauchst du nicht: welches Werkzeug, mit welchen Argumenten, wie groß die Antwort, wie lange sie gedauert hat. Die dritte ist die, die du vermisst, sobald sie fehlt.
Fehlerbild 1: das Werkzeug gibt es nicht
Ein Modell erfindet Werkzeugnamen. Nicht oft, aber regelmäßig, und besonders gern dann, wenn ein Name naheliegt, den es nicht gibt.
await client.callTool({ name: "delete_all_tickets", arguments: {} });
{"content":[{"type":"text",
"text":"MCP error -32602: Tool delete_all_tickets not found"}],
"isError":true}
Zwei Dinge daran solltest du dir merken. Erstens wirft das nicht. Das SDK
gibt ein Ergebnis mit isError: true zurück, und der Text landet beim Modell.
Wer im eigenen Code auf eine Ausnahme wartet, wartet vergebens.
Zweitens taugt die Meldung etwas. Sie nennt den Namen, den es nicht gibt, und ein Modell schlägt danach meistens in seiner Werkzeugliste nach, statt es noch einmal zu versuchen. Geholfen hat dabei nicht dein Server. Es war das Protokoll.
Fehlerbild 2: die Pflichtangabe fehlt
await client.callTool({ name: "answer_ticket", arguments: { key: "T-1041" } });
{"content":[{"type":"text",
"text":"MCP error -32602: Input validation error: Invalid arguments for tool
answer_ticket: Required at antwort"}],
"isError":true}
Der Rumpf ist nie angelaufen. Das Schema hat abgefangen, und die Meldung nennt Werkzeug und Feld.
Dasselbe mit einem falschen Enum-Wert:
MCP error -32602: Input validation error: Invalid arguments for tool
categorize_ticket: Invalid enum value.
Expected 'frage' | 'stoerung' | 'rechnung' | 'recht' | 'spam', received 'dringend'
at kategorie
Und zum Vergleich der Fall, den das Schema nicht kennt - ein Ticket, das es nicht gibt:
{"content":[{"type":"text","text":"{\"ok\":false,\"error\":\"unbekanntes Ticket: T-9999\"}"}],
"isError":true}
Die beiden unterscheiden sich darin, wer zuständig ist. Das Schema prüft die Form, dein Rumpf prüft den Bestand. Was du ins Schema schiebst, musst du nicht selbst formulieren, und was den Bestand angeht, kann das Schema nicht wissen. Beide Meldungen sollten dasselbe leisten: sagen, was gilt, und nicht nur, was nicht gilt.
Fehlerbild 3: das Ergebnis, das wie ein Erfolg aussieht
Acht Tickets sind ein Lehrbeispiel. Ein Helpdesk nach einem langen Wochenende hat vierhundert. Dafür steht der Parameter aus Kapitel 3:
// helpdesk/gross.ts
const bestand = neuerBestand(50); // 50 Runden zu acht Tickets
// Das Werkzeug, das jeder einmal baut: alles auf einmal, Volltext inklusive.
server.registerTool(
"export_inbox",
{ description: "Gibt den kompletten Posteingang mit allen Volltexten zurück.", inputSchema: {} },
async () => ({
content: [{ type: "text", text: JSON.stringify([...bestand.values()], null, 2) }],
}),
);
Gemessen, nicht geschätzt:
| Aufruf | Zeichen |
|---|---|
tools/list (die Werkzeugliste selbst) | 4 206 |
export_inbox | 130 152 |
list_tickets ohne Deckel | 81 970 |
list_tickets mit limit: 25 | 5 223 |
helpdesk_stats | 115 |
get_ticket | 365 |
export_inbox gibt 130 152 Zeichen zurück, grob 33 000 Token. Es antwortet mit
ok, es dauert Millisekunden, und im Protokoll steht kein Fehler. Verbraucht ist
trotzdem der Kontext des Agenten, für den Rest der Sitzung.
Unangenehmer ist die zweite Zeile. list_tickets ohne Volltexte, nur
Betreffzeilen und Status, kommt bei vierhundert Tickets immer noch auf 81 970
Zeichen. Den Volltext wegzulassen reicht also nicht. Eine Liste braucht einen
Deckel, sonst wächst sie mit dem Bestand.
Der Deckel aus Kapitel 3 macht daraus 5 223 Zeichen, ein Sechzehntel. Er kostet vier Zeilen:
limit: z.number().int().min(1).max(100).optional(),
offset: z.number().int().min(0).optional(),
const seite = treffer.slice(offset, offset + limit);
rest: Math.max(0, treffer.length - offset - seite.length),
Der Höchstwert gehört ins Schema und nicht in die Beschreibung. Ein
max(100) wird durchgesetzt, während ein „bitte nicht mehr als 100 anfordern“ im
Beschreibungstext eine Bitte bleibt, die ein Modell unter Druck überliest.
Und helpdesk_stats mit 115 Zeichen zeigt es andersherum. Ein Werkzeug, das
eine Frage beantwortet statt Daten auszuliefern, kostet drei Größenordnungen
weniger. Wo eine Zahl reicht, sollte keine Liste zurückkommen.
Was in die Mitschrift gehört
Nach dem Lauf steht das Protokoll da, und drei Spalten sagen genug:
- Zeichen je Aufruf. Alles über zehntausend ist erklärungsbedürftig.
- Aufrufe je Werkzeug. Dasselbe Werkzeug dreimal hintereinander mit denselben Argumenten heißt, dass du nicht beschrieben hast, was fehlt.
- Fehlerquote je Werkzeug. Ein Werkzeug, das jedes zweite Mal abgelehnt wird, hat kein Modellproblem, sondern ein Schemaproblem.
Der Baustein Benchmarks misst so etwas im großen Maßstab. Für einen einzelnen Server reicht die kleine Fassung: vier Spalten in einem Array.