MCP Engineering

MCP-Server implementieren: Zustand, MRTR und Abbruch

Ein Handler speichert den letzten Tenant an der Verbindung, ein abgelaufener Cursor wechselt still auf den Live-Bestand, und nach einer Cancellation kommt doch noch Erfolg zurück. Drei lokale Abkürzungen verletzen denselben Grundsatz: Laufzeitsemantik braucht explizite Besitzer und Grenzen.

Lehrtext · 11 Abschnitte · zuletzt geprüft: 2026-09-05

Ein Handler ist noch kein Serververtrag

Nach dem Entwurf der öffentlichen Verträge beginnt die schwierigere Arbeit: Ein moderner MCP-Server verarbeitet jeden Request unabhängig, veröffentlicht Listen, kann Updates über angeforderte Streams signalisieren und kann zusätzliche Client-Eingaben nur über Multi Round-Trip Requests 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 Vertrag mit Geltungsbereich, Lebenszyklus und Fehlerpfad. Das verhindert die gefährlichsten Defekte: Zustand aus einer fremden Aufgabe, doppelte oder verlorene Listenobjekte, unkontrollierte Rückfragen und Abbrüche mit weiterlaufenden Seiteneffekten. Die Transportverbindung ist dabei kein fachlicher Kontext; alles Notwendige kommt aus Request, Autorisierung oder einem expliziten Handle.

LeitfrageBei jeder Serverfunktion: Wer besitzt den Zustand, wer darf sie anfordern, und was passiert bei Änderung oder Abbruch?

Die aktuelle Revision trennt Wire-Buchhaltung von Fachcode. resultType unterscheidet vollständige und eingabebedürftige Ergebnisse. Request-Metadaten nennen Version und Client-Capabilities. SDKs können diese Felder erzeugen, aber sie korrigieren kein falsches Zustandsmodell. Ein Handler, der den zuletzt gesehenen Tenant global speichert, bleibt auch mit einem konformen SDK falsch. Preis: Explizite Request-Inputs und Handles erzeugen mehr Validierung; sie machen dafür Wiederholung, horizontale Skalierung und Incident-Rekonstruktion möglich.

Zustand braucht einen expliziten Handle und Identität

Revision 2026-07-28 kennt keine Protokollsession. Ein Prozess und eine Verbindung dürfen Requests verschiedener Aufgaben, Threads und Nutzer tragen. Der Server darf Version, Capabilities, Identität oder Fachzustand nicht aus früherem Verkehr auf derselben Verbindung ableiten. Benötigt eine Anwendung Zustand über Requests hinweg, gibt sie einen opaken Handle zurück und verlangt ihn bei jedem Folgeaufruf als Argument.

EINE VERBINDUNG · eigener ZustandinitializeCapabilitiesRequestscancel / closeKeine Verbindung übernimmt implizit Zustand einer anderen.
Transport, einzelner Request, expliziter Fachhandle und autorisierte Identität haben getrennte Lebensdauern.
Prozessweit
Statische Konfiguration und bewusst synchronisierte Infrastruktur
Pro Request
Version, Client-Capabilities, Autorisierung und laufende Arbeit
Expliziter Handle
Fachzustand wie Warenkorb, Transaktion oder Arbeitsauftrag
Pro Identität oder Mandant
Autorisierte Sicht; bei jedem Handle-Zugriff erneut geprüft

Ein Handle ist bei authentifizierten Servern ein Name, keine Berechtigung. cart_123 darf den Zugriff nur adressieren; der Server bindet ihn bei jedem Call an den aktuellen Principal und Tenant. Ohne Authentifizierung wirkt ein Handle zwangsläufig als Bearer Token und braucht hohe Entropie sowie kurze Lebensdauer. Er soll opak sein, damit Clients keine interne Struktur erraten oder nachbauen.

MerksatzTeile Infrastruktur bewusst, aber nie zufällig Zustand oder Sicht zwischen Requests und Identitäten.

Das Legacy-Altsystem darf intern weiter zustandsbehaftet sein. Der Wrapper übersetzt seine Transaktion in einen expliziten MCP-Vertrag: begin_import liefert einen Handle, add_record und commit_import verlangen ihn. Timeout, Abbruch und Aufräumen werden dokumentiert. Nachteil: Der Server braucht persistente Handle-Ablage oder rekonstruierbaren Zustand; dafür ist die Transaktion nicht mehr an einen bestimmten Worker oder Stream gebunden.

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 Autorisierung und Filter bestimmenDeterministisch nach stabiler Reihenfolge lesenSeite liefernUndurchsichtigen nextCursor ausgebenFolgeaufruf erneut autorisieren und fortsetzen

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.

Trade-offSnapshots machen Listen konsistent, kosten aber Zustand oder Speicher. Eine fortlaufend aktuelle Sicht ist günstiger, muss aber ihre möglichen Veränderungen klar behandeln.

Definiere außerdem, was ein Listencursor nicht verspricht. Er ist kein Änderungsfeed, keine dauerhafte Referenz auf ein Objekt und keine Autorisierung. Der Server darf seine Gültigkeit bewusst begrenzen und muss erschöpfte Seiten ohne nextCursor abschließen. Ein Client kann Antworten gemäß ttlMs und cacheScope cachen; der Cache ersetzt aber keine seitenübergreifende Momentaufnahme. Wer Aktualität benötigt, startet einen neuen Listendurchlauf oder nutzt eine passende Subscription als Invalidierungssignal. Diese Trennung hält Discovery, Caching und Änderungsmeldung unabhängig testbar.

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.

Treffer. 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.

MerksatzOpaque bedeutet: Der Server kann Fortsetzung und Gültigkeit ändern, ohne den Client zu brechen oder ihm fremde Sicht zu öffnen.

Bei cachebaren Listen trägt jede Seite ihr eigenes ttlMs und cacheScope. Es gibt keine seitenübergreifende Konsistenzgarantie. Benötigt der Client einen vollständigen konsistenten Snapshot, beginnt er bei Drift oder ungültigem Cursor wieder ohne Cursor. Der Server darf einen Cursor nie als Ersatz für Autorisierung akzeptieren. Ändern sich Rechte zwischen Seiten, gewinnt die aktuelle Policy; ein Fehler oder Neustart ist sicherer als Fortsetzung in einer nicht mehr erlaubten Sicht.

Resource-Updates signalisieren Änderungen, sie liefern keinen Cache

Resources sind anwendungsgetriebener Kontext. Ein Client liest ihren Inhalt über die Resource-URI. Für Änderungen öffnet er im modernen Protokoll einen subscriptions/listen-Request und fordert nur die benötigten Notification-Typen an. Der Server bestätigt als erste Streamnachricht, welche Teilmenge er tatsächlich unterstützt. Eine Update-Notification transportiert nicht automatisch den neuen Inhalt.

resourcesListChanged
Die Menge verfügbarer Resources änderte sich; der Client kann neu listen
resourceSubscriptions
Der Client nennt konkrete URIs für notifications/resources/updated
subscriptionId
Korreliert jede gelieferte Notification mit dem Listen-Request
Cache-Entscheidung
Notification macht betroffene Daten stale; der Client liest kontrolliert neu

Der Stream ist selbst ein langlebiger Request, keine Session. Ein kontrollierter Abschluss endet mit einem Result; ein abrupter Abbruch liefert keine Replay-Garantie. Der Client öffnet bei weiterem Interesse neu und liest abhängige Listen oder Resources erneut. In einem Multi-Worker-Server braucht die Implementierung einen Event-Bus zwischen dem Worker, der eine Änderung erkennt, und dem Worker, der den Stream hält. Ein rein prozesslokaler Bus verliert Signale über Instanzgrenzen.

MerksatzEine Notification sagt „prüfe erneut“ — nicht „übernimm diesen Inhalt blind“.

MRTR ersetzt spontane Server-zu-Client-Requests

Der moderne Core besitzt keinen allgemeinen Server-zu-Client-Requestkanal. Benötigt ein laufender tools/call, prompts/get oder resources/read zusätzliche Client-Eingabe, antwortet der Server mit resultType: input_required. Darin stehen benannte inputRequests und optional ein opaker requestState. Der Client erfüllt unterstützte Anforderungen und sendet den ursprünglichen Methodenaufruf als neuen Request mit neuer JSON-RPC-ID erneut.

Client startet fachlichen RequestServer liefert input_requiredHost erfüllt oder verweigert EingabenClient wiederholt mit inputResponses und requestStateServer validiert und setzt fort

requestState wird bytegenau zurückgegeben und vom Client nicht interpretiert. Er darf trotzdem keine ungeschützten Secrets enthalten, weil er den Host passiert. Der Server validiert jede Antwort und begrenzt Rundenzahl sowie Gesamtzeit, sonst kann ein fehlerhafter Workflow endlos Eingaben verlangen. Sampling und Roots sind seit dieser Revision deprecated; neue Implementierungen sollen Modellprovider direkt integrieren beziehungsweise Dateien über Toolparameter, Resource-URIs oder Serverkonfiguration erhalten.

PreisMRTR funktioniert ohne persistente Rückkanalverbindung, verlangt aber idempotente Wiederaufnahme und explizite Zustandsübergabe.

Behandle jede Runde wie eine neue, potenziell wiederholte Nachricht. Binde requestState kryptografisch oder serverseitig an Methode, Identität, ursprüngliche Argumente und Ablaufzeit. Verarbeite nur benannte Responses, deren Typ zur angeforderten Eingabe passt; unbekannte, doppelte oder verspätete Antworten werden nicht still übernommen. Lege zudem ein Rundenzahl- und Zeitbudget fest. So wird aus MRTR keine Endlosschleife und ein Angreifer kann einen alten Zustand nicht in einen anderen Vorgang transplantieren. Weil der Client einzelne Anforderungen ablehnen kann, braucht jeder Workflow auch einen fachlich sauberen Abbruchpfad.

Elicitation holt Eingaben sichtbar über den Client ein

Benötigt ein Workflow eine Auswahl oder Fachangabe, kann ein input_required-Result eine Elicitation-Anforderung tragen. Die passende Client-Capability muss im aktuellen Request deklariert sein. Form Mode sammelt strukturierte, nicht sensible Daten im Host; 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 Host 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. Ablehnung und Abbruch sind normale Antworten, keine Fälle für stilles Wiederholen.

MerksatzNutzereingabe ist ein expliziter, rückkanalfähiger Vertrag — keine versteckte Aufforderung in Tool-Text.

Beim wiederholten fachlichen Request prüft der Server inputResponses gegen die ursprüngliche Anforderung und bindet requestState an Methode, Nutzer und Ablaufzeit. Er darf eine alte Antwort nicht für einen anderen Request übernehmen. Wird derselbe Wiederaufnahme-Request doppelt zugestellt, muss die Fachlogik eine Doppelwirkung verhindern. Nachteil: Sichere Wiederaufnahme ähnelt einer kleinen Zustandsmaschine; sie ist ehrlicher als versteckter Zustand in einem offenen Stream.

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.

MerksatzVeröffentlichtes Output Schema und ausgeliefertes structuredContent sind derselbe Vertrag zu zwei Zeitpunkten.

Seit 2026-07-28 darf structuredContent jeder JSON-Wert sein, nicht nur ein Objekt. Ein Array kann daher direkt zu einem Array-Schema passen. Für ältere Clients empfiehlt sich bei Dual-Era-Unterstützung weiterhin eine objektförmige Hülle oder kompatible Textdarstellung. Der Server wählt bewusst, welche Revisionen er bedient, und testet beide Wireformen getrennt. Ein SDK-Typ allein beweist nicht, dass die tatsächlich serialisierte Antwort zum veröffentlichten Schema passt.

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.

MerksatzCancellation beendet Warten und Arbeit nach Möglichkeit; sie beweist nicht, dass die Außenwelt unverändert blieb.

Nach einer Cancellation darf der Empfänger für den betroffenen Request keine weiteren Nachrichten senden. Das schließt ein spätes Erfolgsresultat aus, selbst wenn ein nicht abbrechbarer Downstream-Call doch fertig wird. Intern muss der Server den tatsächlichen Fachzustand weiterhin erfassen. Der nächste Aufruf darf nicht aus fehlender Response schließen, dass nichts passiert ist. Für wirkende Tools gehören deshalb Idempotency-Key, Statusabfrage oder Kompensationsweg zum Fachvertrag. Preis: Diese Mechanismen kosten Persistenz; ohne sie bleibt Wiederholung nach Timeout oder Abbruch blind.

Eine Implementierung verbindet die Grenzen bewusst

Request-Metadaten und Autorisierung prüfenExplizite Handles statt Connection-State verwendenDiscovery mit stabiler Sicht und Cursorn anbietenUpdates nur über angeforderte Subscription streamenMRTR-Antwort und Wiederaufnahme validierenSchema-konformes Tool-Ergebnis liefernCancellation propagieren und Fachzustand sichern

Nicht jeder Server braucht jede optionale Capability. Weniger veröffentlichte Semantik bedeutet weniger Zustand und 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 langlebige Subscription-Requests, einzelne Fachrequests und explizite Anwendungshandles. Keiner dieser Lebenszyklen darf als Ersatz für den anderen dienen.

ImplementierungsregelCapabilities sind Zusagen. Implementiere und teste ihre vollständige Semantik, bevor du sie aushandelst.

Eine kleine Zustandsmatrix macht diese Zusagen prüfbar: Für jeden Requesttyp notierst du erlaubte Eingänge, erzeugte Ausgänge, Persistenz, Autorisierungsprüfung, Timeout und Wiederholungsverhalten. Teste anschließend nicht nur den Happy Path, sondern doppelte Requests, abgelaufene Handles, Rechtewechsel zwischen Seiten, Streamverlust, abgelehnte Eingabe und Cancellation unmittelbar vor einem Seiteneffekt. Observability folgt denselben Grenzen: Logs korrelieren Request, Handle und Subscription, ohne Tokens oder Nutzereingaben offenzulegen. Erst wenn ein Incident aus diesen Signalen rekonstruierbar ist, ist die Laufzeitsemantik wirklich betreibbar.

Die neun Laufzeitgrenzen im Check

  • Isolation: Fachzustand über explizite, erneut autorisierte Handles tragen.
  • Listen: stabile Ordnung, opaque Cursor, Cachehinweise und Driftpfad zusammen entwerfen.
  • MRTR: zusätzliche Eingabe als input_required liefern und Wiederaufnahme validieren.
  • Subscription: nur angeforderte Notifications streamen und nach Verlust neu synchronisieren.
  • Abbruch: Arbeit kooperativ stoppen, keine Response mehr senden und Fachwirkung separat klären.

Vor dem Rollout lohnt ein letzter Invariantentest. Zwei gleichzeitige Requests dürfen weder Tenant noch Capability voneinander übernehmen. Ein abgelaufener Cursor darf keine neue Sicht vortäuschen. Ein wiederholter MRTR-Aufruf darf keine zweite Bestellung auslösen. Ein neu geöffneter Subscription-Stream darf verlorene Notifications nicht erfinden, sondern muss den Client zur aktuellen Quelle zurückführen. Und eine Cancellation darf keine späte Protokollantwort erzeugen, obwohl die Fachlogik einen bereits gestarteten Außeneffekt weiter beobachten muss. Diese Fälle gehören als automatisierte Tests neben die Handler. Sie prüfen nicht einzelne Bibliotheksfunktionen, sondern die Grenzen, an denen Produktionserfahrung zeigt, ob der Server wirklich stateless, autorisiert und wiederholbar arbeitet. Ergänze für jeden Fall ein korrelierbares, datensparsames Signal: Request-ID, gehashter Handle, Subscription-ID und Ergebnisstatus genügen meist. Inhalte, Tokens und freie Nutzereingaben gehören nicht in Telemetrie. So liefert ein Fehler Beweise für seine Zustandsgrenze, ohne beim Debugging ein zweites Datenleck zu erzeugen.

AusblickIm nächsten Modul geht es um Hosts und Clients. Hier baust du die Serverseite so, dass diese Integration zuverlässig überhaupt möglich ist.

Jetzt anwenden

Diesen Stoff gibt es als Modul mit bewerteten Entscheidungs-Checks — dieselbe Einführung, danach die Übungen.

Zum Modul →
Quellen & Aktualität12 Primärquellen · zuletzt geprüft:
  1. 01modelcontextprotocol.io/specification/2026-07-28/basic/index
  2. 02modelcontextprotocol.io/specification/2026-07-28/basic/versioning
  3. 03modelcontextprotocol.io/specification/2026-07-…ilities/cancellation
  4. 04modelcontextprotocol.io/specification/2026-07-…utilities/pagination
  5. 05modelcontextprotocol.io/specification/2026-07-…rns/multi-round-trip
  6. 06modelcontextprotocol.io/specification/2026-07-28/server/resources
  7. 07modelcontextprotocol.io/specification/2026-07-28/server/tools
  8. 08modelcontextprotocol.io/specification/2026-07-…er/utilities/caching
  9. 09modelcontextprotocol.io/specification/2026-07-…lities/subscriptions
  10. 10modelcontextprotocol.io/specification/2026-07-28/client/elicitation
  11. 11modelcontextprotocol.io/specification/2026-07-28/deprecated
  12. 12modelcontextprotocol.io/specification/2026-07-28/architecture