Einen MCP-Server mit Skript-Clients testen
Ein Skript-Client ist ein kurzes Programm, das eine feste Liste von Nachrichten an einen Server sendet und jede Antwort prüft. Er wird nie müde und lässt nie einen Schritt aus.
Das ist wichtig, weil sich ein Server leicht aus Versehen kaputtmachen lässt. Eine Änderung von einem Zeichen kann eine Antwort verändern, und niemand merkt es, bis ein echter Client scheitert.
Am Ende dieser Lektion
Notiz
Ich kann einen Server mit Skript-Clients testen.
Schlüsselwörter
| Begriff | Was er bedeutet | Beispiel |
|---|---|---|
| Drehbuch | eine Liste von Nachrichten, jede mit der erwarteten Antwort | |
| Erwartet | die Antwort, die der Test will | |
| Erhalten | die Antwort, die der Server wirklich gesendet hat | |
| Unterschied | die erste Stelle, an der die beiden nicht übereinstimmen | |
| Platzhalter | eine Marke, die zu jedem Wert passt | <beliebig> |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Ein Drehbuch ordnet jeder Nachricht ihre Antwort zu
Ein Drehbuch ist eine Liste von Schritten. Jeder Schritt hat eine Nachricht zum Senden und die erwartete Antwort. Ein Schritt mit einer Benachrichtigung erwartet keine Antwort.
Manche Werte ändern sich, etwa eine Versionsnummer. Der Test kennzeichnet sie mit einem Platzhalter, damit eine Änderung dort kein Fehler ist. Alles andere muss genau passen.
Beispiel: ein Drehbuch für ein Rechenwerkzeug
| Schritt | Gesendet | Erwartete Antwort |
|---|---|---|
| 1 | initialize | Die verlangte Version, tools gemeldet, beliebige Serverversion |
| 2 | Die Bereit-Benachrichtigung | Keine Antwort |
| 3 | tools/list | Ein Werkzeug namens addieren |
| 4 | addieren 2 und 3 | Der Text 5, isError false |
| 5 | addieren "2" und 3 | Ein Werkzeugfehler, isError true |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Den ersten Unterschied melden, mit seiner Stelle
Ein Test, der nur "gescheitert" sagt, verschwendet Zeit. Ein guter sagt, wo. Der Pfad result.content[0].text zeigt auf einen Wert in der Antwort.
Der Prüfer geht die erwartete Antwort in fester Reihenfolge durch und hält beim ersten Unterschied an. Er sagt, was er erwartet hat und was er bekam.
Ein häufiger Fehler
Warnung
Viele lassen einen Test nur gegen einen Server laufen, der funktioniert. Ein Test, der nie gescheitert ist, hat nie gezeigt, dass er etwas fängt. Machen Sie den Server absichtlich kaputt und prüfen Sie, dass der Test scheitert. Machen Sie die Änderung dann rückgängig.
Modell-Helfer schreiben
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const basis = process.env.AI_URL || \"http://ai:4000\";' 'async function fragen(nachrichten, probe, max) {' ' try {' ' const res = await fetch(basis + \"/v1/chat/completions\", { method: \"POST\", headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" }, body: JSON.stringify({ model: \"lab\", messages: nachrichten, max_tokens: max || 80 }), signal: AbortSignal.timeout(30000) });' ' if (res.ok === false) throw new Error(\"Status \" + res.status);' ' const daten = await res.json();' ' return { text: daten.choices[0].message.content, aufgezeichnet: false };' ' } catch (fehler) {' ' console.log(\"Aufgezeichnete Probe: Kein Modell hat geantwortet (\" + fehler.message + \").\");' ' return { text: probe, aufgezeichnet: true };' ' }' '}' 'module.exports = { fragen };' > model.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Server schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function senden(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'const SCHEMA = { type: \"object\", properties: { a: { type: \"integer\" }, b: { type: \"integer\" } }, required: [\"a\", \"b\"] };' 'function worte(text, schlecht) { return { content: [{ type: \"text\", text: text }], isError: schlecht }; }' 'function verarbeiten(nachricht) {' ' if (nachricht.id === undefined) return;' ' const p = nachricht.params || {};' ' const argumente = p.arguments || {};' ' if (nachricht.method === \"initialize\") return senden({ id: nachricht.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"rechner\", version: \"1.0\" } } });' > server6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Server schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (nachricht.method === \"tools/list\") return senden({ id: nachricht.id, result: { tools: [{ name: \"addieren\", description: \"Zwei ganze Zahlen addieren\", inputSchema: SCHEMA }] } });' ' if (nachricht.method === \"tools/call\" && p.name === \"addieren\") {' ' if (Number.isInteger(argumente.a) && Number.isInteger(argumente.b)) return senden({ id: nachricht.id, result: worte(String(argumente.a + argumente.b), false) });' ' return senden({ id: nachricht.id, result: worte(\"a und b müssen ganze Zahlen sein\", true) });' ' }' ' if (nachricht.method === \"tools/call\") return senden({ id: nachricht.id, error: { code: -32602, message: \"Unbekanntes Werkzeug: \" + p.name } });' ' senden({ id: nachricht.id, error: { code: -32601, message: \"Method not found: \" + nachricht.method } });' '}' >> server6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Server schreiben, Teil 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (zeile) { verarbeiten(JSON.parse(zeile)); });' >> server6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer schreiben, Teil 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { fragen } = require(\"./model\");' 'const server = require(\"child_process\").spawn(\"node\", [process.argv[2] || \"server6.js\"]);' 'const BELIEBIG = \"<beliebig>\";' 'const eins = function (id, methode, params) { return { jsonrpc: \"2.0\", id: id, method: methode, params: params }; };' 'const addieren = function (id, argumente) { return eins(id, \"tools/call\", { name: \"addieren\", arguments: argumente }); };' 'const ok = function (id, ergebnis) { return { jsonrpc: \"2.0\", id: id, result: ergebnis }; };' 'const text = function (t, schlecht) { return { content: [{ type: \"text\", text: t }], isError: schlecht }; };' 'const schritte = [' ' [eins(1, \"initialize\", { protocolVersion: \"2025-03-26\" }), ok(1, { protocolVersion: \"2025-03-26\", capabilities: { tools: {} }, serverInfo: { name: \"rechner\", version: BELIEBIG } })],' > pruefen6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' [{ jsonrpc: \"2.0\", method: \"notifications/initialized\" }, null],' ' [eins(2, \"tools/list\"), ok(2, { tools: [{ name: \"addieren\", description: BELIEBIG, inputSchema: BELIEBIG }] })],' ' [addieren(3, { a: 2, b: 3 }), ok(3, text(\"5\", false))],' ' [addieren(4, { a: \"2\", b: 3 }), ok(4, text(\"a und b müssen ganze Zahlen sein\", true))],' ' [eins(5, \"tools/call\", { name: \"nichts\" }), { jsonrpc: \"2.0\", id: 5, error: { code: -32602, message: \"Unbekanntes Werkzeug: nichts\" } }],' '];' 'function istObjekt(x) { return Boolean(x) && typeof x === \"object\"; }' 'function pfadMit(pfad, k, liste) { return liste ? pfad + \"[\" + k + \"]\" : (pfad ? pfad + \".\" + k : k); }' 'function unterschied(erwartet, erhalten, pfad) {' ' if (erwartet === BELIEBIG) return null;' ' const beide = istObjekt(erwartet) && istObjekt(erhalten) && Array.isArray(erwartet) === Array.isArray(erhalten);' >> pruefen6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer schreiben, Teil 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const gesagt = \" erwartet \" + JSON.stringify(erwartet) + \", erhalten \" + JSON.stringify(erhalten);' ' if (beide === false) return JSON.stringify(erwartet) === JSON.stringify(erhalten) ? null : (pfad || \"(Wurzel)\") + \":\" + gesagt;' ' const liste = Array.isArray(erwartet);' ' if (liste && (erwartet.length === erhalten.length) === false) return (pfad || \"(Wurzel)\") + \": erwartet \" + erwartet.length + \" Elemente, erhalten \" + erhalten.length;' ' const schluessel = Object.keys(erwartet).sort();' ' for (const k of schluessel) {' ' if ((k in erhalten) === false) return pfadMit(pfad, k, liste) + \": fehlt\";' ' const d = unterschied(erwartet[k], erhalten[k], pfadMit(pfad, k, liste));' ' if (d) return d;' ' }' ' const zuviel = Object.keys(erhalten).sort().filter(function (k) { return schluessel.indexOf(k) < 0; });' >> pruefen6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer schreiben, Teil 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' return zuviel.length > 0 ? pfadMit(pfad, zuviel[0], liste) + \": unerwartet\" : null;' '}' 'const zeilen = [];' 'const wartend = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (l) {' ' if (wartend.length > 0) wartend.shift()(JSON.parse(l)); else zeilen.push(JSON.parse(l));' '});' 'function naechste() { return zeilen.length > 0 ? Promise.resolve(zeilen.shift()) : new Promise(function (r) { wartend.push(r); }); }' 'async function main() {' ' let bestanden = 0, gesamt = 0, erster = null;' ' for (let i = 0; i < schritte.length; i++) {' ' server.stdin.write(JSON.stringify(schritte[i][0]) + \"\n\");' ' if (schritte[i][1] === null) continue;' ' gesamt += 1;' ' const d = unterschied(schritte[i][1], await naechste(), \"\");' ' console.log((d ? \"FEHLER Schritt \" + (i + 1) + \" bei \" + d : \"BESTANDEN Schritt \" + (i + 1)));' >> pruefen6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer schreiben, Teil 5
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (d === null) bestanden += 1; else if (erster === null) erster = d;' ' }' ' server.stdin.end();' ' console.log(\"bestanden \" + bestanden + \" von \" + gesamt);' ' if (erster === null) return;' ' const frage = \"Ein Test meines Servers ist mit diesem Unterschied gescheitert. Was ist die wahrscheinliche Ursache? Antworten Sie in einem Satz. \" + erster;' ' const antwort = await fragen([{ role: \"user\", content: frage }], \"Der Server hat einen anderen Text geliefert als der Test erwartet hat.\", 60);' ' console.log(\"Modell: \" + antwort.text);' '}' 'main();' >> pruefen6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer auf dem guten Server ausführen
docker exec -w /home/node/project agent node pruefen6.jsHier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Eine Kopie mit schlechter Änderung machen
docker exec -w /home/node/project agent sh -c "sed 's/argumente.a + /argumente.a - /' server6.js > schlecht6.js"Hier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Prüfer auf der schlechten Kopie ausführen
docker exec -w /home/node/project agent node pruefen6.js schlecht6.jsHier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Die zwei Runs nebeneinander
| Run | Geprüfte Schritte | Ergebnis |
|---|---|---|
| Guter Server | 1, 3, 4, 5 und 6 | bestanden 5 von 5 |
| Schlechte Kopie | 1, 3, 4, 5 und 6 | bestanden 4 von 5 |
| Schlechte Kopie, Schritt 4 | addieren 2 und 3 | result.content[0].text: erwartet "5", erhalten "-1" |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.