MCP-Server implementieren: Semantik vor Handlern
Einführung · 11 Abschnitte · ~11 Min Lesezeit · Stand
Ein Handler ist noch kein Serververtrag
Nach dem Entwurf der öffentlichen Verträge beginnt die schwierigere Arbeit: Ein MCP-Server verarbeitet nicht nur einzelne Aufrufe, sondern lebt in Verbindungen, veröffentlicht Listen, kann Updates signalisieren und darf den Host bei Modell- oder Nutzereingaben nur über vereinbarte Rückkanäle einbeziehen. Der Server bestimmt seine Fachlogik; der Host behält die Hoheit über Anwendung, Nutzerinteraktion und Modellzugriff.
Die Implementierung sollte deshalb nicht als Sammlung globaler Handler entstehen. Behandle jede Capability als einen Vertrag mit Geltungsbereich, Lebenszyklus und Fehlerpfad. Das verhindert die gefährlichsten Defekte: Daten einer anderen Verbindung, doppelte oder verlorene Listenobjekte, unkontrollierte Modellaufrufe und Abbrüche mit weiterlaufenden Seiteneffekten.
Zustand gehört zur Verbindung und Identität
Ein Prozess kann viele MCP-Sitzungen bedienen. Informationen wie angemeldete Identität, Mandant, ausgehandelte Capabilities, Cursor-Snapshots und Subscriptions dürfen daher nicht versehentlich als ein globaler Serverzustand geführt werden. Sie gehören zur MCP-Sitzung und, wo Berechtigungen gelten, zur geprüften Identität. Laufende Requests und Event-Lieferung haben zusätzlich einen kurzlebigen Stream- oder Request-Kontext.
| Prozessweit | Statische Konfiguration oder bewusst geteilte, synchronisierte Infrastruktur |
|---|---|
| MCP-Sitzung | Negotiated Capabilities, Subscriptions und Cursor-Snapshots bei Streamable HTTP |
| Pro Stream oder Request | Event-Lieferung, laufende Arbeit und temporäre Transportressourcen |
| Pro Identität oder Mandant | Autorisierte Datenansicht und fachlicher Zugriffskontext |
Bei Streamable HTTP beendet das Schließen eines SSE- oder TCP-Streams nicht automatisch die MCP-Sitzung: Der Client kann denselben Session-Identifier bei späteren HTTP-Requests weiterführen. Der Server räumt deshalb nur Stream-Zustand beim Streamende. Sitzungszustand endet bei expliziter Session-Terminierung, serverseitigem Ablauf oder einer klaren Löschregel. Bei stdio endet der Lebenszyklus dagegen mit Prozess und Streams. Eine WebSocket-Implementierung wäre ein eigener Custom Transport und muss dieselben Lifecycle-Regeln dokumentieren.
Listen brauchen Ordnung und eine definierte Sicht
Prompts, Resources und Tools können über Listenoperationen entdeckt werden. Bei kleinen, statischen Mengen ist eine vollständige Antwort praktisch. Bei vielen oder dynamischen Einträgen braucht die Antwort Pagination. Entscheidend ist nicht die Seitengröße, sondern dass ein Client beim Weiterblättern keine zufälligen Duplikate oder Lücken erhält.
- Datenansicht für Verbindung und Identität bestimmen
- Deterministisch nach stabiler Reihenfolge lesen
- Seite liefern
- Undurchsichtigen `nextCursor` ausgeben
- Folgeaufruf gegen dieselbe definierte Sicht prüfen
Wähle ausdrücklich eine Semantik: etwa eine Snapshot-Sicht für die Dauer einer Pagination oder eine stabile Sortierung mit einer nachvollziehbaren Fortsetzungsgrenze. Welche Wahl passt, hängt von Änderungsrate und Speicherkosten ab. Ohne diese Entscheidung verschieben Einfügungen und Löschungen die nächste Seite unbemerkt.
Kurzcheck
Während ein Host Resources durchblättert, werden Einträge archiviert und neue angelegt. Die UI darf im Durchlauf weder Duplikate noch Lücken sehen. Welche Zusage braucht der Server?
- Eine identitätsgebundene, stabil geordnete Sicht und einen opaque Cursor, der nur gegen diese Sicht fortsetzt.
- Einen öffentlichen Offset gegen den jeweils aktuellen Datenbestand, damit der Client die Position versteht.
- Eine zufällige Reihenfolge pro Seite, damit neue Einträge gleich schnell sichtbar werden.
Richtig. Die Zusage betrifft eine definierte Fortsetzung. Der Cursor bleibt serverkontrolliert; Änderungen am Live-Bestand verschieben nicht unbemerkt die bereits begonnene Liste.
Ein Cursor ist ein Server-Token, kein Client-Format
Der Client gibt einen Cursor unverändert zurück; er soll ihn weder verstehen noch konstruieren müssen. Der Server behandelt ihn als opaque Token und bindet ihn an die Datenansicht, Identität und gegebenenfalls Ablaufzeit. Er darf einen Cursor intern signieren, verschlüsseln oder auf serverseitigen Zustand abbilden — die Repräsentation ist aber keine öffentliche API.
Ein abgelaufener, widerrufener oder zur falschen Identität gehörender Cursor ist ein erwartbarer Zustand. Antworte kontrolliert mit einem geeigneten Fehler oder einer Neustart-Anweisung, statt still mit einem anderen Offset fortzufahren. Das ist besonders wichtig, wenn sich Berechtigungen oder der zugrunde liegende Bestand zwischen zwei Seiten ändern.
Resource-Updates signalisieren Änderungen, sie liefern keinen Cache
Resources sind anwendungsgetriebener Kontext. Ein Client liest ihren Inhalt über die Resource-URI; ein Server kann Änderungen an einer einzelnen abonnierten Resource oder an der verfügbaren Liste signalisieren, wenn er die jeweilige Capability angekündigt hat. Eine Update-Notification transportiert dabei nicht automatisch den neuen Inhalt.
| `listChanged` | Die Menge verfügbarer Resources hat sich geändert; Clients können die Discovery-Liste neu laden. |
|---|---|
| `subscribe` | Ein Client kann Änderungen einer konkreten Resource abonnieren; der Server signalisiert danach deren Update. |
| Cache-Entscheidung | Der Client oder Host entscheidet, ob, wann und mit welcher Staleness er erneut liest und Kontext ersetzt. |
Der Server hält Subscriptions nur für unterstützte, erfolgreiche Anmeldungen und bindet sie an MCP-Sitzung, Resource-URI und verifizierte Identität. Er räumt sie bei Unsubscribe, expliziter Session-Terminierung oder Ablauf auf. Das Ende eines Streamable-HTTP-SSE-Streams löscht sie nicht vorschnell, weil die Sitzung weiterlaufen und ein Stream wiederverbunden werden kann. Er sendet keine globalen Updates an andere Sessions, nur weil ein Objekt irgendwo geändert wurde.
Sampling fordert beim Client Modellarbeit an
Muss eine Serverfunktion eine Modellantwort erzeugen, stellt der Server eine Sampling-Anfrage an den MCP-Client. Nur wenn der Client Sampling als Capability ausgehandelt hat, darf der Server sie nutzen. Der Client beziehungsweise Host wählt Modellzugang, Richtlinien, Kosten- und Sicherheitsgrenzen und kann die Anfrage für Nutzer sichtbar machen oder ablehnen.
Der Server hinterlegt dafür nicht heimlich einen Modellschlüssel und umgeht keine Host-Policy. Er kann seine fachliche Aufgabe koordinieren und eine begrenzte Sampling-Anfrage formulieren, aber der Rückkanal bleibt Teil des Client-Vertrags. Unterstützt der Client Tool-Use innerhalb des Sampling-Flows nicht, fordert der Server diese Variante ebenfalls nicht an.
Elicitation holt Eingaben sichtbar über den Client ein
Benötigt ein Workflow eine Auswahl oder Fachangabe, fragt der Server den Nutzer mittels Elicitation über den Client. Auch diese Capability und ihr Modus müssen ausgehandelt sein. Form Mode sammelt flache, strukturierte und nicht sensible Daten im Client; die Anfrage erklärt Zweck und Felder so, dass Nutzer prüfen, ändern, ablehnen oder abbrechen können.
Passwörter, API-Schlüssel, Zugriffstokens und Zahlungsdaten gehören nicht in Form Mode. Für sensible Interaktionen verwendet der Server URL Mode: Der Client zeigt Ziel-Domain und Zweck, holt vor Navigation Consent ein, und die geheimen Daten passieren nicht den MCP-Client. Ein Server darf deshalb kein scheinbar harmloses Freitextfeld als Secret-Sammelstelle verwenden.
Tool-Ergebnisse sind publizierte Output-Verträge
Ein Tool mit maschinenlesbarem Ergebnis veröffentlicht sein outputSchema bereits in der Tool-Definition. Liefert es später structuredContent, muss dieses serverproduzierte JSON dem veröffentlichten Schema entsprechen; Clients sollten die Übereinstimmung prüfen. Damit bleibt ein Ergebnis für Host-Logik, Tests und spätere Serverversionen berechenbar.
| `outputSchema` | Der beim Discovery veröffentlichte Vertrag für strukturierte Ergebnisse |
|---|---|
| `structuredContent` | Die konkrete serverproduzierte JSON-Antwort, die diesem Vertrag entsprechen muss |
| TextContent | Eine zusätzliche serialisierte Darstellung für Kompatibilität und Modellkontext |
Text ist kein Ersatz für ein versprochenes Schema: Ein Host müsste daraus Felder erraten, obwohl der Server einen maschinenlesbaren Vertrag anbieten kann. Umgekehrt ergänzt eine Textdarstellung structuredContent, weil ältere oder modellzentrierte Clients strukturiertes Ergebnis nicht vollständig auswerten. Die Felder, Typen und Semantik entwickelt der Server deshalb versioniert und bewusst.
Cancellation ist kooperativ und kein Rollback
Eine Cancellation-Notification kann einen noch laufenden Request markieren. Der Empfänger sollte Verarbeitung beenden, zugehörige Ressourcen freigeben und für den abgebrochenen Request keine Antwort mehr senden. Wegen Netzwerklatenz kann die Cancellation aber nach Abschluss eintreffen oder nicht umsetzbar sein; beide Seiten müssen diese Race Conditions vertragen.
Eine robuste Serverfunktion propagiert den Abbruch in Datenbankabfragen, Streams und Child-Jobs, prüft ihn vor irreversiblen Schritten und dokumentiert den Fachzustand. Ein Abbruch garantiert weder, dass ein externer API-Call nie ausgeführt wurde, noch dass bereits entstandene Seiteneffekte automatisch zurückgerollt werden. Dafür braucht die Fachlogik Idempotenz, Kompensation oder einen expliziten Status.
Eine Implementierung verbindet die Grenzen bewusst
- Sitzung initialisieren und Capabilities binden
- Identität und Sitzungskontext herstellen
- Discovery mit stabiler Sicht und Cursorn anbieten
- Updates nur für vereinbarte Subscriptions signalisieren
- Client-Rückkanäle für Sampling oder Elicitation prüfen
- Schema-konformes strukturiertes Tool-Ergebnis liefern
- Bei Cancellation Arbeit und Ressourcen kooperativ beenden
- Stream oder Sitzung nach jeweiliger Lifecycle-Regel räumen
Nicht jeder Server braucht jede optionale Capability. Weniger veröffentlichte Semantik bedeutet weniger Zustand und weniger Fehlerpfade. Veröffentliche eine Capability erst, wenn ihr Lifecycle, ihre Berechtigungsgrenze und ihr Testszenario klar sind; ein ausgelassenes Feature ist sicherer als eine halb implementierte Capability. Trenne dabei in Streamable HTTP immer kurzlebige Streams von langlebiger Sitzungssemantik.
Die neun Laufzeitgrenzen im Check
- Isolation: Datenansicht und verbindungsbezogenen Zustand nicht vermischen.
- Capability: Nur verhandelte, vollständig betriebene Funktionen verwenden.
- Rückkanal und Fehler: Host- und Nutzersouveränität sowie sichere Fachsemantik erhalten.