Wie ein MCP-Werkzeug seine Eingabe prüft
Ein MCP-Werkzeug hat einen Namen, eine Beschreibung und ein Eingabeschema. Das Schema sagt, wie seine Argumente aussehen müssen. Der Server prüft die Argumente dagegen, bevor irgendetwas läuft.
Das ist wichtig, weil ein Modell die Argumente füllt und manchmal falsch liegt. Eine gute Ablehnung sagt ihm, was es ändern soll.
Am Ende dieser Lektion
Notiz
Ich kann ein Werkzeug anbieten, dessen Eingabe vor dem Lauf geprüft wird.
Schlüsselwörter
| Begriff | Was er bedeutet | Beispiel |
|---|---|---|
| Schema | eine Beschreibung, wie die Eingabe aussehen muss | |
| Argument | ein benannter Wert in einem Werkzeugaufruf | |
| Prüfen | die Eingabe kontrollieren, bevor man sie nutzt | |
| Aufzählung | eine feste Liste erlaubter Werte | nudeln, wrap, salat |
| Werkzeugfehler | ein Ergebnis mit isError true, das sagt, was schiefging |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Ein Schema nennt die Regeln für jedes Argument
Jedes Argument hat einen Typ. Es kann auch eine Aufzählung, einen Zahlenbereich oder eine größte Länge haben. Das Schema nennt, welche Argumente nötig sind, und kann sagen, dass keine anderen erlaubt sind.
Der Server prüft jede Regel in fester Reihenfolge und sammelt alle Probleme, nicht nur das erste.
Beispiel: das Werkzeug essen_bestellen
| Argument | Regel | Besteht | Scheitert |
|---|---|---|---|
| gericht | Nötig. Eines von nudeln, wrap, salat | wrap | suppe |
| menge | Nötig. Eine ganze Zahl, 1 bis 5 | 2 | 9, oder "2" als Text |
| notiz | Optionaler Text, höchstens 20 Zeichen | ohne Zwiebeln | Eine Notiz mit 21 Zeichen |
| jeder andere Name | Nicht erlaubt | Keiner | farbe |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Eine Ablehnung sagt, was zu ändern ist
Ein Aufruf mit schlechten Argumenten ist keine kaputte Anfrage. Die Anfrage war in Ordnung. Der Server sendet also ein normales Ergebnis mit isError true und einem Satz, der die Probleme nennt.
Ein Aufruf für ein Werkzeug, das es nicht gibt, ist etwas anderes. Das ist ein Protokollfehler mit dem Code -32602.
Ein häufiger Fehler
Warnung
Viele glauben, das Modell halte sich immer an das Schema, der Server müsse also nicht prüfen. Ein Modell kann einen Namen weglassen, Text für eine Zahl senden oder einen erfundenen Namen hinzufügen. Auch der Client prüft vielleicht nicht. Der Server ist die letzte Stelle zum Prüfen, also prüft er immer.
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' 'const SCHEMA = { type: \"object\", properties: { gericht: { type: \"string\", enum: [\"nudeln\", \"wrap\", \"salat\"] }, menge: { type: \"integer\", minimum: 1, maximum: 5 }, notiz: { type: \"string\", maxLength: 20 } }, required: [\"gericht\", \"menge\"], additionalProperties: false };' 'function senden(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function typPasst(regel, wert) { return regel.type === \"string\" ? typeof wert === \"string\" : Number.isInteger(wert); }' 'function probleme(argumente) {' ' const aus = [];' ' SCHEMA.required.forEach(function (n) { if ((n in argumente) === false) aus.push(\"fehlt: \" + n); });' ' Object.keys(SCHEMA.properties).forEach(function (n) {' ' if ((n in argumente) === false) return;' ' const r = SCHEMA.properties[n], w = argumente[n];' > server3.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 (typPasst(r, w) === false) return aus.push(n + \": erwartet \" + r.type);' ' if (r.enum && r.enum.indexOf(w) < 0) aus.push(n + \": muss eines von \" + r.enum.join(\", \") + \" sein\");' ' if (typeof r.minimum === \"number\" && (w < r.minimum || w > r.maximum)) aus.push(n + \": muss zwischen \" + r.minimum + \" und \" + r.maximum + \" liegen\");' ' if (typeof r.maxLength === \"number\" && w.length > r.maxLength) aus.push(n + \": höchstens \" + r.maxLength + \" Zeichen\");' ' });' ' Object.keys(argumente).sort().forEach(function (n) { if ((n in SCHEMA.properties) === false) aus.push(\"zusätzlich: \" + n); });' ' return aus;' '}' 'function worte(text, schlecht) { return { content: [{ type: \"text\", text: text }], isError: schlecht }; }' 'function verarbeiten(nachricht) {' ' if (nachricht.id === undefined) return;' ' const p = nachricht.params || {};' >> server3.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' ' if (nachricht.method === \"initialize\") return senden({ id: nachricht.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"mensa\", version: \"0.3\" } } });' ' if (nachricht.method === \"tools/list\") return senden({ id: nachricht.id, result: { tools: [{ name: \"essen_bestellen\", description: \"Essen in der Mensa bestellen\", inputSchema: SCHEMA }] } });' ' if (p.name === \"essen_bestellen\") {' ' const argumente = p.arguments || {};' ' const gefunden = probleme(argumente);' ' if (gefunden.length > 0) return senden({ id: nachricht.id, result: worte(\"Aufruf von essen_bestellen nicht möglich: \" + gefunden.join(\"; \"), true) });' ' return senden({ id: nachricht.id, result: worte(\"Bestellt: \" + argumente.menge + \" \" + argumente.gericht, false) });' ' }' >> server3.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 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' senden({ id: nachricht.id, error: { code: -32602, message: \"Unbekanntes Werkzeug: \" + p.name } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (zeile) { verarbeiten(JSON.parse(zeile)); });' >> server3.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.
Skript-Client 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\", [\"server3.js\"]);' 'const bestellen = function (id, argumente) { return { id: id, method: \"tools/call\", params: { name: \"essen_bestellen\", arguments: argumente } }; };' 'const skript = [' ' { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' ' { method: \"notifications/initialized\" },' ' bestellen(2, { gericht: \"wrap\", menge: 2 }),' ' bestellen(3, { gericht: \"suppe\", menge: 9, farbe: \"rot\" }),' ' bestellen(4, {}),' ' bestellen(5, { gericht: \"nudeln\", menge: \"2\" }),' ' { id: 6, method: \"tools/call\", params: { name: \"speisekarte_loeschen\", arguments: {} } },' '];' 'const gesehen = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (zeile) {' ' const m = JSON.parse(zeile);' > client3.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.
Skript-Client schreiben, Teil 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' console.log(\"<- \" + (m.result && m.result.content ? m.result.content[0].text + \" (isError \" + m.result.isError + \")\" : zeile));' ' gesehen.push(m);' ' if (gesehen.length === 6) server.stdin.end();' '});' 'skript.forEach(function (m) { server.stdin.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); });' 'server.on(\"close\", async function () {' ' const gezeigt = gesehen[2].result.content[0].text;' ' const frage = \"Ein Werkzeug hat einen Aufruf abgelehnt. Was soll der Aufrufer ändern? Antworten Sie in einem Satz. \" + gezeigt;' ' const antwort = await fragen([{ role: \"user\", content: frage }], \"Wählen Sie nudeln, wrap oder salat, eine Menge von 1 bis 5 und keine Farbe.\", 60);' ' console.log(\"Modell: \" + antwort.text);' '});' >> client3.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.
Client gegen den Server ausführen
docker exec -w /home/node/project agent node client3.jsHier angezeigt, nicht ausgeführt. Erstellen Sie ein kostenloses Konto, um „Ausführen“ zu drücken und ihn im Terminal Ihres eigenen Labs einzugeben.
Was das Skript aufruft
Das Skript startet eine Sitzung und ruft dann fünfmal essen_bestellen auf und einmal ein Werkzeug, das es nicht gibt. Ein Aufruf ist gut. Die anderen verletzen eine Regel, mehrere Regeln oder lassen alle nötigen Namen weg.
Ein kurzer Modellaufruf am Ende liest eine Ablehnung und sagt, was zu ändern ist. Ohne Modell druckt er eine Aufgezeichnete Probe.
Was jeder Aufruf zurückbekommt
| Aufruf | Antwort |
|---|---|
| wrap, menge 2 | Bestellt: 2 wrap, isError false |
| suppe, menge 9, farbe rot | Drei Probleme in einem Satz, isError true |
| keine Argumente | fehlt: gericht; fehlt: menge |
| menge als Text "2" | menge: erwartet integer |
| speisekarte_loeschen | Protokollfehler -32602 |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.