Wie ein MCP-Server begrenzt, wer was aufruft
Ein Werkzeug, das eine Bestellung aufgibt, und ein Werkzeug, das jede Bestellung storniert, sollten nicht demselben Aufrufer offenstehen. Ein Server begrenzt das mit Scopes: einer kurzen Bezeichnung für jede Sache, die ein Aufrufer tun darf.
Ein Server über Standardein- und -ausgabe hat keine Anmeldeseite. Wer ihn startet, sagt, welche Scopes der Aufrufer hält, und der Server prüft sie bei jedem Aufruf.
Am Ende dieser Lektion
Notiz
Ich kann die Werkzeuge eines Servers danach begrenzen, wer fragt.
Schlüsselwörter
| Begriff | Was er bedeutet | Beispiel |
|---|---|---|
| Scope | eine Bezeichnung für eine Sache, die ein Aufrufer tun darf | bestellungen:schreiben |
| Berechtigung | ein Scope, den der Aufrufer hält | |
| Ablehnen | einen Aufruf verweigern, weil ein Scope fehlt | |
| Platzhalter | eine Berechtigung, die jeden Scope mit demselben Anfang abdeckt | bestellungen:* |
| Prüfprotokoll | eine Aufzeichnung jedes Aufrufs und was entschieden wurde |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Jedes Werkzeug nennt den Scope, den es braucht
Der Server führt eine Tabelle: jedes Werkzeug und der eine Scope, den es braucht. Der Aufrufer hält eine Liste von Berechtigungen. Ein Aufruf ist erlaubt, wenn eine Berechtigung den Scope abdeckt.
Ein Platzhalter wie bestellungen: deckt jeden Scope ab, der mit bestellungen: beginnt, und sonst nichts. Die Berechtigung deckt alles ab.
Beispiel: ein Aufrufer hält zwei Berechtigungen
| Werkzeug | Braucht | Aufrufer hält speisekarte:lesen, bestellungen:schreiben | Entscheidung |
|---|---|---|---|
| speisekarte_lesen | speisekarte:lesen | Abgedeckt | Erlauben |
| bestellung_aufgeben | bestellungen:schreiben | Abgedeckt | Erlauben |
| alle_stornieren | bestellungen:verwalten | Nicht abgedeckt | Ablehnen |
| kasse_leeren | Kein solches Werkzeug | Entfällt | Unbekannt |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.
Ein Werkzeug zu verstecken heißt nicht, es abzulehnen
Der Server darf Werkzeuge aus tools/list weglassen, die der Aufrufer nicht nutzen kann. Das erspart dem Modell das Probieren. Aber ein Aufrufer kann trotzdem einen Aufruf für ein Werkzeug senden, das ihm nie gezeigt wurde.
Darum findet die Prüfung bei jedem tools/call statt, immer. Ein fehlender Scope ergibt einen Werkzeugfehler, der den Scope nennt. Ein unbekanntes Werkzeug bleibt ein Protokollfehler.
Ein häufiger Fehler
Warnung
Viele verstecken das gefährliche Werkzeug in der Liste und nennen das Sicherheit. Ein verstecktes Werkzeug ist immer noch da. Wer seinen Namen kennt, kann es aufrufen. Prüfen Sie den Scope bei jedem Aufruf und protokollieren Sie die abgelehnten Aufrufe ebenso wie die ausgeführten.
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 BRAUCHT = { speisekarte_lesen: \"speisekarte:lesen\", bestellung_aufgeben: \"bestellungen:schreiben\", alle_stornieren: \"bestellungen:verwalten\" };' 'const HABEN = (process.env.RECHTE || \"\").split(\",\");' 'let zaehler = 0;' 'function senden(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function worte(text, schlecht) { return { content: [{ type: \"text\", text: text }], isError: schlecht }; }' 'function vermerken(werkzeug, entscheidung) {' ' zaehler += 1;' ' process.stderr.write(JSON.stringify({ nr: zaehler, werkzeug: werkzeug, entscheidung: entscheidung, braucht: BRAUCHT[werkzeug] || null }) + \"\n\");' '}' 'function aufrufen(nachricht, p) {' ' if (Object.keys(BRAUCHT).indexOf(p.name) < 0) {' ' vermerken(p.name, \"unbekannt\");' > server5.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' ' return senden({ id: nachricht.id, error: { code: -32602, message: \"Unbekanntes Werkzeug: \" + p.name } });' ' }' ' if (HABEN.indexOf(BRAUCHT[p.name]) < 0) {' ' vermerken(p.name, \"ablehnen\");' ' return senden({ id: nachricht.id, result: worte(\"Nicht erlaubt: \" + p.name + \" braucht \" + BRAUCHT[p.name], true) });' ' }' ' vermerken(p.name, \"erlauben\");' ' senden({ id: nachricht.id, result: worte(\"erledigt: \" + p.name, false) });' '}' 'function sichtbar() {' ' return Object.keys(BRAUCHT).filter(function (n) { return HABEN.indexOf(BRAUCHT[n]) >= 0; }).map(function (n) {' ' return { name: n, description: \"Mensa-Werkzeug \" + n, inputSchema: { type: \"object\" } };' ' });' '}' 'function verarbeiten(nachricht) {' ' if (nachricht.id === undefined) return;' ' const p = nachricht.params || {};' >> server5.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.5\" } } });' ' if (nachricht.method === \"tools/list\") return senden({ id: nachricht.id, result: { tools: sichtbar() } });' ' if (nachricht.method === \"tools/call\") return aufrufen(nachricht, p);' ' senden({ id: nachricht.id, error: { code: -32601, message: \"Method not found: \" + nachricht.method } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (zeile) { verarbeiten(JSON.parse(zeile)); });' >> server5.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 umgebung = Object.assign({}, process.env, { RECHTE: \"speisekarte:lesen,bestellungen:schreiben\" });' 'const server = require(\"child_process\").spawn(\"node\", [\"server5.js\"], { env: umgebung });' 'const aufruf = function (id, name) { return { id: id, method: \"tools/call\", params: { name: name, arguments: {} } }; };' 'const skript = [' ' { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' ' { id: 2, method: \"tools/list\" },' ' aufruf(3, \"speisekarte_lesen\"), aufruf(4, \"bestellung_aufgeben\"), aufruf(5, \"alle_stornieren\"), aufruf(6, \"kasse_leeren\"),' '];' 'const gesehen = [];' 'const protokoll = [];' 'require(\"readline\").createInterface({ input: server.stderr }).on(\"line\", function (zeile) { protokoll.push(zeile); });' > client5.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' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (zeile) {' ' const m = JSON.parse(zeile);' ' const namen = m.result && m.result.tools ? \"tools: \" + m.result.tools.map(function (t) { return t.name; }).join(\", \") : null;' ' console.log(\"<- \" + (namen || (m.result && m.result.content ? m.result.content[0].text : JSON.stringify(m.error || m.result))));' ' 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 () {' ' protokoll.forEach(function (l) { console.log(\"Protokoll \" + l); });' ' const ablehnung = gesehen[4].result.content[0].text;' ' const frage = \"Erklären Sie diese Ablehnung einem Studierenden in einem kurzen Satz: \" + ablehnung;' >> client5.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 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' const antwort = await fragen([{ role: \"user\", content: frage }], \"Ihr Konto hat keine Berechtigung, alle Bestellungen zu stornieren.\", 60);' ' console.log(\"Modell: \" + antwort.text);' '});' >> client5.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 client5.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 tut
Der Client startet den Server mit den Scopes speisekarte:lesen und bestellungen:schreiben in seiner Umgebung. Er listet die Werkzeuge auf und ruft dann drei auf, die es gibt, und eines, das es nicht gibt.
Zuletzt druckt er das Protokoll des Servers. Ein kurzer Modellaufruf erklärt eine Ablehnung. Ohne Modell druckt er eine Aufgezeichnete Probe.
Was jeder Aufruf zurückgibt und protokolliert
| Aufruf | Antwort | Entscheidung im Protokoll |
|---|---|---|
| tools/list | Nur speisekarte_lesen und bestellung_aufgeben | Kein Werkzeugaufruf, also nicht protokolliert |
| speisekarte_lesen | erledigt: speisekarte_lesen | erlauben |
| bestellung_aufgeben | erledigt: bestellung_aufgeben | erlauben |
| alle_stornieren | Nicht erlaubt: alle_stornieren braucht bestellungen:verwalten | ablehnen |
| kasse_leeren | Protokollfehler -32602 | unbekannt |
Die Zellen, die Lernende ausfüllen, bleiben in dieser Vorschau leer.