Wie ein MCP-Server Nachrichten liest
Ein MCP-Server ist ein Programm, das JSON-Nachrichten aus seiner Eingabe liest und JSON-Nachrichten in seine Ausgabe schreibt, eine Nachricht pro Zeile. Ein Server, der das schlecht macht, läuft allein und scheitert an jedem echten Client.
Modul 67, Lektion 5, zeigte die Seite des Clients und die Arten von Nachricht. Hier schreiben Sie die Leseschleife eines Servers.
Am Ende dieser Lektion
Notiz
Ich kann die JSON-Nachrichten lesen und schreiben, die ein Server und ein Client austauschen.
Schlüsselwörter
| Begriff | Was er bedeutet | Beispiel |
|---|---|---|
| Puffer | Text, der aufbewahrt wird, bis eine ganze Zeile da ist | |
| Rahmen | eine ganze Nachricht, am Zeilenumbruch aus dem Strom geschnitten | |
| stdout | der Ausgabestrom: Hier gehen nur Nachrichten hin | |
| stderr | der Fehlerstrom: Hier gehen Protokolle hin | |
| Parse error | die Zeile war gar kein JSON | -32700 |
| Invalid Request | JSON, aber keine Anfrage | -32600 |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Ein Strom wird in Zeilen geschnitten
Die Eingabe kommt nicht als ganze Nachrichten an. Sie kommt in Stücken. Ein Stück kann eine halbe Nachricht enthalten, eine ganze oder zweieinhalb.
Der Server führt einen Puffer. Er hängt jedes Stück an, schneidet jede vollständige Zeile ab und behält den Rest. Eine Nachricht enthält nie einen rohen Zeilenumbruch, also beendet ein Zeilenumbruch immer eine.
Beispiel: drei Stücke
| Stück trifft ein | Puffer danach | Fertige Nachrichten |
|---|---|---|
| {"id":1} {"id | {"id | 1 Nachricht: {"id":1} |
| ":2} | (leer) | 1 Nachricht: {"id":2} |
| {"i | {"i | Noch keine |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Auch eine schlechte Zeile bekommt eine Antwort
Eine Zeile, die kein JSON ist, bekommt einen Parse error. Der Server kann keine id lesen, die id ist also null.
Eine Zeile, die JSON, aber keine Anfrage ist, bekommt Invalid Request. Lässt sich eine id lesen, wird sie übernommen. Eine Anfrage ohne id ist eine Benachrichtigung, und selbst eine Benachrichtigung für eine unbekannte Methode bekommt nichts.
Was der Server zurückschickt
| Zeile | Antwort |
|---|---|
| kein json | Fehler -32700, id null |
| {"jsonrpc":"2.0","id":5,"method":5} | Fehler -32600, id 5 |
| [1,2] | Fehler -32600, id null |
| {"jsonrpc":"2.0","method":"unsinn"} | Nichts: eine Benachrichtigung |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Antworten können in beliebiger Reihenfolge kommen
Eine langsame Anfrage darf eine schnelle nicht aufhalten. Der Server beantwortet jede Anfrage, sobald ihre Antwort fertig ist.
Ein Client kann Antworten also nicht nach ihrer Stelle zuordnen. Er ordnet sie nach der id zu. Darum übernimmt der Server die id genau so, wie sie kam, als Zahl oder als Text.
Ein häufiger Fehler
Warnung
Viele suchen Fehler mit einer Druckzeile. Auf stdout ist diese Zeile keine Nachricht, und der Client scheitert an ihr. Nur Nachrichten gehen nach stdout. Schreiben Sie Protokolle nach stderr, das der Client zeigen oder ignorieren kann.
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' 'let puffer = \"\";' 'function senden(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function fehler(id, code, text) { senden({ id: id, error: { code: code, message: text } }); }' 'function protokoll(text) { process.stderr.write(\"Protokoll: \" + text + \"\n\"); }' 'function verarbeiten(zeile) {' ' let nachricht;' ' try { nachricht = JSON.parse(zeile); } catch (problem) { return fehler(null, -32700, \"Parse error\"); }' ' const gesehen = nachricht && (typeof nachricht.id === \"number\" || typeof nachricht.id === \"string\") ? nachricht.id : null;' ' const gut = nachricht && typeof nachricht === \"object\" && nachricht.jsonrpc === \"2.0\" && typeof nachricht.method === \"string\";' ' if (Boolean(gut) === false) return fehler(gesehen, -32600, \"Invalid Request\");' ' protokoll(\"erhalten \" + nachricht.method);' > server1.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.id === undefined) return;' ' const ms = (nachricht.params || {}).ms || 0;' ' if (nachricht.method === \"ping\") return senden({ id: nachricht.id, result: {} });' ' if (nachricht.method === \"warten\") return setTimeout(function () { senden({ id: nachricht.id, result: { gewartet: ms } }); }, ms);' ' fehler(nachricht.id, -32601, \"Method not found: \" + nachricht.method);' '}' 'process.stdin.on(\"data\", function (stueck) {' ' puffer += stueck.toString();' ' let stelle = puffer.indexOf(\"\n\");' ' while (stelle >= 0) {' ' const zeile = puffer.slice(0, stelle);' ' puffer = puffer.slice(stelle + 1);' ' if (zeile.trim().length > 0) verarbeiten(zeile);' ' stelle = puffer.indexOf(\"\n\");' ' }' '});' 'protokoll(\"bereit\");' >> server1.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\", [\"server1.js\"]);' 'const nachricht = function (obj) { return JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, obj)); };' 'const warten = function (id, ms) { return nachricht({ id: id, method: \"warten\", params: { ms: ms } }) + \"\n\"; };' 'const ping = nachricht({ id: 3, method: \"ping\" }) + \"\n\";' 'const schlecht = \"kein json\n\" + nachricht({ id: 5, method: 5 }) + \"\n\" + nachricht({ method: \"notifications/x\" }) + \"\n\";' 'const schritte = [[0, warten(1, 80)], [10, warten(2, 10)], [20, ping.slice(0, 12)], [40, ping.slice(12) + schlecht]];' 'function starten() {' ' schritte.forEach(function (s) {' ' setTimeout(function () { console.log(\"-> \" + s[1].trim().split(\"\n\").join(\" | \")); server.stdin.write(s[1]); }, s[0]);' ' });' > client1.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' ' setTimeout(function () { server.stdin.end(); }, 200);' '}' 'require(\"readline\").createInterface({ input: server.stderr }).on(\"line\", function (zeile) {' ' console.log(\"stderr \" + zeile);' ' if (zeile === \"Protokoll: bereit\") starten();' '});' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (zeile) { console.log(\"<- \" + zeile); });' 'server.on(\"close\", async function () {' ' const frage = \"Warum muss eine Antwort die id ihrer Anfrage tragen? Antworten Sie in einem Satz.\";' ' const antwort = await fragen([{ role: \"user\", content: frage }], \"Damit der Client erkennt, zu welcher Anfrage jede Antwort gehört, denn Antworten können in beliebiger Reihenfolge eintreffen.\", 60);' ' console.log(\"Modell: \" + antwort.text);' '});' >> client1.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 client1.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 der Client tut
Der Client startet den Server und wartet auf dessen Bereit-Zeile auf stderr. Er sendet zwei langsame Anfragen, ein halbes ping und dann den Rest des ping mit drei weiteren Zeilen.
Ein Aufruf am Ende bittet ein Modell um einen Satz. Ohne Modell druckt der Run dafür eine Aufgezeichnete Probe.
Die Antworten, in der Reihenfolge ihres Eintreffens
| Mit <- gedruckte Zeile | Warum sie dann kam |
|---|---|
| id 2, gewartet 10 | Das kurze Warten endete zuerst |
| id 3, ein leeres Ergebnis | Das ping war ganz, als seine zweite Hälfte kam |
| id null, Fehler -32700 | Die Zeile "kein json" |
| id 5, Fehler -32600 | Eine Methode, die eine Zahl ist |
| id 1, gewartet 80 | Das lange Warten endete zuletzt |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.