Der Spickzettel zum Seminar MCP & Agentic Software Engineering, erster Teil: das Protokoll und der eigene Server — Rollen, Primitives, Fehlercodes, Tool-Schnitt, Robustheit, Autorisierung, Betrieb. Wie man einen Coding Agent beauftragt, prüft und absichert, steht auf dem zweiten Blatt, Agenten kontrollieren. Maßgeblich ist und bleibt die Spezifikation auf modelcontextprotocol.io — sie bewegt sich schnell, dieses Blatt steht auf dem Stand 2026-07-28.
Rollen und Verantwortung
| Rolle | Kernaufgabe | Dazu gehört |
|---|---|---|
| Host | koordinieren | Zustimmung, Richtlinien, Kontext |
| Client | verbinden | eine Verbindung, Grenze halten |
| Server | bereitstellen | Tools, Resources, Prompts |
Die 1:1-Beziehung zwischen Client und Server ist die technische Form der Isolation: Ein Server sieht weder den Gesprächsverlauf noch, was andere Server liefern. Wer sie aufweicht, verliert die Nachvollziehbarkeit.
| Rolle | Verantwortet | Typischer Fehler |
|---|---|---|
| Mensch | Anforderung, Testorakel | delegiert das Urteil mit |
| Host | Zustimmung, Sichtbarkeit | bestätigt im Stapel |
| Server | Autorisierung, Wirkung | vertraut dem Aufrufer |
| Zielsystem | Konsistenz, Audit | kennt den Agenten nicht |
Module: MCP-Architektur: Host, Client und Server · MCP als Verbindungsschicht einordnen
Abgrenzung zu den Nachbarn
| Konzept | Verhältnis zu MCP |
|---|---|
| REST-API | bleibt die fachliche Schnittstelle darunter |
| Function Calling | Modellseite, die MCP-Werkzeuge nutzt |
| Agenten-Framework | steuert Ablauf und Planung darüber |
| Hersteller-Plugin | das, was MCP ablösen soll |
MCP ersetzt keine dieser Schichten, sondern setzt sie in ein gemeinsames Format. Als Agenten-Framework eingeführt, enttäuscht es erwartungsgemäß.
Modul: MCP als Verbindungsschicht einordnen
Primitives: wer steuert was
| Primitive | Gesteuert von | Methoden |
|---|---|---|
| Tools | dem Modell | tools/list, tools/call |
| Resources | der Anwendung | resources/list, resources/read |
| Prompts | dem Nutzer | prompts/list, prompts/get |
Die Zuordnung hängt daran, wer entscheidet, wann etwas benutzt wird — nicht an der Technik. Eine Aktion als Resource wird nie ausgeführt, ein Dokument als Tool verbrennt Kontextbudget bei jedem Aufruf.
Felder einer Resource:
| Feld | Pflicht | Wofür |
|---|---|---|
| uri | ja | eindeutige Adresse |
| name | ja | technischer Bezeichner |
| title | nein | Anzeige in der Oberfläche |
| mimeType | nein | Auswertung beim Client |
| annotations | nein | Zielgruppe, Priorität, Änderungsdatum |
Eine nicht vorhandene Resource wird mit -32602 abgelehnt — nie mit einer
leeren Liste, die bleibt mehrdeutig.
Module: Primitives, Protokollmechanik und Spezifikationsstand · Einen lokalen MCP-Server entwickeln
Protokollmechanik und Pflichtmetadaten
Es gibt keine Sitzung mehr: Fähigkeiten werden je Anfrage in _meta
ausgehandelt, eine offene Verbindung ist kein Gespräch.
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "anliegen-client", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": { "elicitation": {} } }
protocolVersion und clientCapabilities sind Pflicht, clientInfo soll
gesetzt sein. Listen tragen ttlMs und cacheScope und dürfen
zwischengespeichert werden — cacheScope: private, sobald die Liste
nutzerabhängig ist.
| Code | Name | Bedeutet |
|---|---|---|
| -32602 | Invalid params | Pflichtfeld fehlt, Ressource unbekannt |
| -32020 | HeaderMismatch | Header und Rumpf widersprechen sich |
| -32021 | MissingRequiredClientCapability | Client kann nicht, was nötig wäre |
| -32022 | UnsupportedProtocolVersion | Version wird nicht unterstützt |
Der Bereich −32020 bis −32099 ist für die Spezifikation reserviert; eigene Codes gehören nicht hinein.
Rückfragen laufen als Multi Round-Trip Request — der Server sendet keine eigene Anfrage mehr:
{ "result": { "resultType": "input_required",
"inputRequests": { "freigabe": { "method": "elicitation/create" } },
"requestState": "eyJ..." } }
Die Wiederholung schickt inputResponses samt requestState — und eine neue
JSON-RPC-ID, sonst wird sie abgelehnt.
Module: MCP-Architektur: Host, Client und Server · Primitives, Protokollmechanik und Spezifikationsstand
Abgekündigt mit 2026-07-28
| Feature | Empfohlener Weg |
|---|---|
| Roots | Verzeichnisse über Parameter oder Konfiguration |
| Sampling | direkte Anbindung an die Modell-API |
| Logging über das Protokoll | stderr bei stdio, sonst OpenTelemetry |
| Dynamic Client Registration | Client ID Metadata Documents |
| HTTP+SSE-Transport | Streamable HTTP |
Abgekündigt heißt: weiterhin Teil der Spezifikation, aber zum Entfall vorgesehen. Frühester Entfall für die ersten vier ist die erste Revision ab dem 28.07.2027. Neue Implementierungen nehmen es nicht mehr auf.
Modul: Primitives, Protokollmechanik und Spezifikationsstand
Server-Grundgerüst (TypeScript)
// npm install @modelcontextprotocol/server zod
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
const server = new McpServer({ name: "anliegen-mcp", version: "1.0.0" });
server.registerTool("anforderung_suchen", {
description: "Sucht in den Anforderungen des Anliegenbuchs.",
inputSchema: z.object({
suchbegriff: z.string().min(2).describe("Fachbegriff, z. B. Kostenstelle"),
maxTreffer: z.number().int().max(50).default(10),
}),
}, async ({ suchbegriff, maxTreffer }) => { /* ... */ });
Werkzeugnamen: 1 bis 128 Zeichen, nur Buchstaben, Ziffern, Unterstrich,
Bindestrich und Punkt. Ohne Angabe gilt der JSON-Schema-Dialekt 2020-12; ein
Tool ohne Parameter bekommt ein leeres Objektschema, nicht null.
Die Werkzeugfunktion ruft eine normale Funktion der Anwendung auf — sonst ist die Fachlogik ohne Protokoll nicht mehr testbar.
Modul: Einen lokalen MCP-Server entwickeln
Tool-Schnitt und Zusicherungen
Parameter führen besser als Prosa: Aufzählungen statt freier Zeichenketten,
Muster statt Formathinweisen, Beispielwerte im describe.
inputSchema: z.object({
status: z.enum(["offen", "geprueft", "geschlossen"])
.describe("Zielstatus der Serviceanfrage"),
anliegenId: z.string().regex(/^AN-\d{6}$/)
.describe("Nummer im Format AN-000123"),
grund: z.string().max(280).optional().describe("Pflicht bei geschlossen"),
})
| Art | Aussage | Wo sie steht |
|---|---|---|
| Vorbedingung | Anfrage existiert und ist offen | Beschreibung |
| Nachbedingung | Status gesetzt, Historie ergänzt | Beschreibung |
| Invariante | geschlossene Anfrage bleibt geschlossen | Beschreibung |
Was der Server zusichert, muss er auch prüfen — die Beschreibung allein setzt nichts durch. Zustand über mehrere Aufrufe läuft über ein explizites Handle, dessen Berechtigung bei jedem Aufruf neu geprüft wird; der Besitz eines Handles ist keine Berechtigung, und fortlaufende Nummern sind erratbar.
Lesend, vorbereitend und verändernd gehören getrennt — in eigene Werkzeuge und in eigene Berechtigungen, nicht nur in die Dokumentation.
Modul: Tools aus Sicht des Sprachmodells entwerfen
Robuste Operationen
Wiederholbarkeit entsteht über einen fachlichen Schlüssel, den der Aufrufer bei der Wiederholung gleich lässt:
inputSchema: z.object({
anliegenId: z.string().regex(/^AN-\d{6}$/),
status: z.enum(["offen", "geprueft", "geschlossen"]),
vorgangsschluessel: z.string().uuid().describe("Gleicher Wert bei Wiederholung"),
})
| Operation | Pagination | Caching |
|---|---|---|
| tools/list | ja | ja |
| prompts/list | ja | ja |
| resources/list | ja | ja |
| resources/read | nein | ja |
| tools/call | nein | nein |
Cursor sind undurchsichtig und werden nicht ausgewertet; ein ungültiger Cursor
wird mit -32602 abgelehnt. Eine Kürzung des Ergebnisses muss gemeldet
werden, sonst hält das Modell sie für die Wahrheit. Bei Streamable HTTP gilt
das Schließen des Streams als Abbruch.
Modul: Robuste Operationen, Fehler und Kontextbudget
Start, Diagnose und Fehlermeldungen
{ "mcpServers": {
"anliegen": {
"command": "node",
"args": ["/opt/anliegen-mcp/build/index.js"],
"env": { "ANLIEGEN_DB": "postgres://db/anliegen" } } } }
Immer absolute Pfade — das Arbeitsverzeichnis eines vom Client gestarteten Servers ist undefiniert, und über stdio gestartete Server erben nur einen begrenzten, plattformabhängigen Teil der Umgebungsvariablen.
| Symptom | Ursache |
|---|---|
| Prozess startet nicht | relativer Pfad, fehlende Rechte |
| Startet und beendet sofort | Syntaxfehler, fehlende Umgebungsvariable |
| Verbindung bleibt leer | Ausgaben auf stdout statt stderr |
npx @modelcontextprotocol/inspector node build/index.js
npx @modelcontextprotocol/inspector --cli node build/index.js \
--method tools/call --tool-name anforderung_suchen \
--tool-arg suchbegriff=Kostenstelle
Der Inspector verlangt Node 22.19.0 oder neuer; die CLI-Variante läuft in der Pipeline. Ein Ausführungsfehler ist kein Protokollfehler — er geht ans Modell weiter und muss deshalb den nächsten Schritt nennen:
{ "result": { "resultType": "complete", "isError": true,
"content": [ { "type": "text",
"text": "4711 unbekannt, siehe kostenstellen_listen." } ] } }
Drei Angaben machen den Unterschied: was war falsch (Feld beim Namen nennen), warum (Bereich, Format, Vorbedingung), was hilft weiter (der konkrete nächste Aufruf). Nicht hinein gehören interne Pfade, Zugangsdaten und Stacktraces.
Modul: Den Server testen und diagnostizieren
Lokal oder entfernt, und OAuth 2.1
| Merkmal | Lokal | Entfernt |
|---|---|---|
| Nutzerkreis | eine Person | viele gleichzeitig |
| Zugangsdaten | aus der Umgebung | OAuth 2.1 |
| Betriebsaufwand | gering | wie ein Dienst |
| Datenzugriff | was der Nutzer darf | was der Scope erlaubt |
Sobald ein zweiter Mensch denselben Server braucht, ist die lokale Variante am Ende — und der Umstieg ist kürzer als gedacht: gleiche Semantik, gleiche Fachlogik, neu sind Autorisierung, Betrieb und Telemetrie.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
resource_metadata="https://anliegen.example/.well-known/oauth-protected-resource",
scope="anliegen:lesen"
Zwei Verschärfungen dieser Revision: Der Client prüft vor dem Einlösen des Codes den Issuer (RFC 9207), und Client ID Metadata Documents sind der empfohlene Weg zur Client-Identität. Zusammen entschärft das Mix-up- und Confused-Deputy-Angriffe.
POST /mcp HTTP/1.1
Authorization: Bearer eyJhbGciOi...
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: anliegen_status_setzen
Accept: application/json, text/event-stream
Die Autorisierung gehört in jede einzelne HTTP-Anfrage — es gibt keine Sitzung, die sie trägt. Der Server prüft nicht nur die Signatur, sondern den Empfänger des Tokens, und reicht es nie an ein Drittsystem durch.
Modul: Remote MCP und OAuth-Autorisierung
Eingaben, Isolation und Notbremse
inputSchema: z.object({
anliegenId: z.string().regex(/^AN-\d{6}$/),
feld: z.enum(["status", "frist", "kostenstelle"]),
wert: z.string().max(120),
})
// Zusaetzlich im Server pruefen: existiert die Anfrage,
// darf der Aufrufer sie sehen, passt der Wert zum Feld
Das Schema begrenzt die Form, der Server entscheidet über Existenz und Berechtigung. Vier Injektionswege ins Zielsystem: Suchbegriffe in Abfragen, Pfadangaben aus dem erlaubten Verzeichnis heraus, Bezeichner als URL-Fragment, Schemareferenzen auf fremde Adressen.
| Mittel | Verhindert |
|---|---|
| Sandbox oder Container | Zugriff jenseits des Arbeitsbereichs |
| Netzwerkregeln | Abfluss an unbekannte Ziele |
| Rate Limits je Identität | kaskadierende Fehler und Kostenangriffe |
| Kill Switch | dass ein Fehllauf bis zum Ende läuft |
Lokale Server laufen über stdio oder beschränken den HTTP-Zugriff — an localhost gebunden, mit Token oder Unix-Socket, und mit Prüfung des Origin-Headers gegen DNS-Rebinding.
Modul: Least Privilege, Isolation und Freigaben
Betrieb, Resilienz und Versionierung
| Mittel | Fängt ab |
|---|---|
| Timeout je Operation | hängende Aufrufe, die den Lauf blockieren |
| Begrenzte Wiederholung | Sturm auf ein bereits überlastetes System |
| Circuit Breaker | Dauerlast auf ein ausgefallenes Zielsystem |
| Rate Limit je Identität | kaskadierende Fehler und Kostenangriffe |
Wiederholungen setzen Idempotenz voraus. Agenten belasten anders als Menschen: Ein Fehlschlag löst sofort den nächsten Versuch aus, und eine Schleife erzeugt in Sekunden, wofür ein Team Stunden bräuchte.
| Änderung | Weg |
|---|---|
| Neues optionales Feld | direkt möglich |
| Neues Pflichtfeld | neues Werkzeug, altes abkündigen |
| Geänderte Semantik | neuer Name, beide parallel |
| Entfernung | Frist ankündigen, dann entfernen |
Auch eine „verbesserte” Beschreibung ist eine Verhaltensänderung — sie steuert, was das Modell wählt.
Modul: Betrieb, Katalog und Team-Governance
Typische Fallen
console.logim Werkzeug macht einen stdio-Server unbrauchbar — jede Ausgabe auf stdout zerstört den Nachrichtenstrom. Logs gehören auf stderr.- Die Werkzeugliste wechselt je Verbindung. Das verbietet die Spezifikation; Listen sind dynamisch, aber nicht verbindungsabhängig.
- Ein Zustandsbezeichner gilt als Nachweis der Berechtigung. Er ist ein Bezeichner, mehr nicht — geprüft wird bei jedem Aufruf.
cacheScope: publicbei nutzerabhängigen Listen liefert die gefilterte Sicht eines Nutzers an alle anderen aus.- Der Server merkt sich den Nutzer aus der ersten Anfrage. Es gibt keine Sitzung mehr, auf die sich das stützen ließe.
- Sensible Parameter werden mit
x-mcp-headerin HTTP-Header gespiegelt — und landen damit in Logs und Zwischenstellen. - Das Gateway entscheidet nach Header, der Server arbeitet nach Rumpf.
Genau dafür gibt es
-32020; der Server gleicht beides ab. - Der Teilerfolg wird als Gesamtfehler gemeldet und zwingt zum vollständigen Wiederholen, statt je Eintrag zu benennen, was gelang.
- Jeder REST-Endpunkt wird eins zu eins zum Werkzeug. Der Katalog explodiert, und die Auswahl des Modells wird unsicher.
- Der Inspector wird erst geholt, wenn nichts mehr geht — als Diagnosewerkzeug ist er am Anfang am billigsten.