MCP-Spezifikation 2026-07-28: Die Session ist weg — Migrationsleitfaden für den Wechsel auf Stateless

Am 28. Juli 2026 ist eine neue Revision der Spezifikation des Model Context Protocol erschienen und
damit zur aktuellen (Current) Protokollversion geworden. Acht Monate nach der vorigen Revision
2025-11-25. MCP-Mitbegründer David Soria Parra nannte das Release in der offiziellen Ankündigung
“das Wichtigste, was es in MCP gegeben hat, seit vor über einem Jahr Remote MCP kam”.
In einem Satz zusammengefasst: MCP ist stateless geworden. Der initialize-Handshake ist
verschwunden, der Header Mcp-Session-Id ebenfalls. Wer aktuell einen MCP-Server oder -Client
betreibt, hat es hier nicht mit Release Notes zu tun, die man “irgendwann mal” liest, sondern mit
Breaking Changes, die Codeänderungen erzwingen.
Die Wellen schlagen bereits. Am 6. August hat Cloudflare McpAgent — bislang der Weg, MCP-Server in
Workers zu bauen — auf deprecated und feature-frozen gesetzt und empfiehlt den Umstieg auf
stateless Handler. Wenn es keine Sessions mehr gibt, braucht man auch kein Durable Object mehr, das
sie festhält. Darum geht es ausführlich in Abschnitt 12.
Dieser Artikel gibt nicht einfach die Liste der Spec-Änderungen wieder, sondern stellt in den Mittelpunkt, welcher Code tatsächlich angefasst werden muss. Zu jedem Punkt steht die SEP-Nummer dabei — wer die Begründung braucht, öffnet direkt den zugehörigen PR.
1. Die Session ist weg (SEP-2567)
Das ist die größte Änderung. Im Streamable-HTTP-Transport wurden Sessions auf Protokollebene und
der Header Mcp-Session-Id entfernt.
Daraus folgt eine weitere Bedingung: tools/list, resources/list und prompts/list dürfen
pro Verbindung keine unterschiedlichen Werte mehr zurückgeben. Früher waren Implementierungen
nach dem Muster “diese Session gehört zu einem authentifizierten Nutzer, also zeigen wir auch die
Admin-Tools” möglich — jetzt hängen Listenantworten nicht mehr von der Verbindung ab.
Was also tun, wenn ein Server Zustand über mehrere Aufrufe hinweg halten muss? Die Antwort der Spec ist eindeutig: Der Server stellt einen expliziten Handle aus, der als ganz normales Tool-Argument hin- und hergereicht wird.
// Vorher — eine Struktur, die sich auf den Session-Header verließ
app.post('/mcp', async (c) => {
const sessionId = c.req.header('Mcp-Session-Id');
const state = await sessions.get(sessionId); // dieser Header kommt jetzt nicht mehr an
// ...
});
// Nachher — Zustand als servergenerierter Handle, explizit über Tool-Argumente übergeben
server.tool('open_workspace', {}, async () => ({
resultType: 'complete',
content: [{ type: 'text', text: 'Workspace geöffnet' }],
structuredContent: { workspaceHandle: 'ws_01J8ZQ...' }, // vom Server ausgestellt
}));
server.tool('write_file', {
workspaceHandle: z.string(), // der Client schickt ihn beim nächsten Aufruf unverändert mit
path: z.string(),
body: z.string(),
}, async ({ workspaceHandle, path, body }) => { /* ... */ });
Aus implizitem Verbindungszustand werden explizite Daten. Das macht den Betrieb mit mehreren Instanzen hinter einem Load Balancer oder serverless deutlich einfacher — es ist schlicht egal, welche Instanz die Anfrage annimmt.
2. Der initialize-Handshake ist weg (SEP-2575)
initialize und notifications/initialized wurden komplett gestrichen. Stattdessen trägt jede
Anfrage die für sie selbst nötigen Informationen in _meta mit sich.
_meta-Schlüssel |
Richtung | Pflicht | Inhalt |
|---|---|---|---|
io.modelcontextprotocol/protocolVersion |
Client → Server | Ja | Protokollversion ("2026-07-28") |
io.modelcontextprotocol/clientCapabilities |
Client → Server | Ja | Für diese Anfrage relevante Client-Fähigkeiten |
io.modelcontextprotocol/clientInfo |
Client → Server | Nein (SHOULD) | Name und Version des Clients |
io.modelcontextprotocol/logLevel |
Client → Server | Nein | Minimales Log-Level für diese Anfrage |
io.modelcontextprotocol/serverInfo |
Server → Client (_meta des Ergebnisses) |
Nein (SHOULD) | Name und Version des Servers |
// So sieht ab jetzt jede Anfrage aus
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "write_file",
"arguments": { "path": "a.txt", "body": "hi" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { },
"io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}
}
Eine Anfrage ohne Pflichtfelder ist malformed. Der Server MUSS sie mit JSON-RPC -32602
(Invalid params) ablehnen, und bei HTTP MUSS der Antwortstatus 400 Bad Request lauten. Passt die
Version nicht, gibt er UnsupportedProtocolVersionError (-32022) zurück.
Auch für Capability-Deklarationen gilt eine neue Regel: Der Server DARF sich NICHT (MUST NOT) auf
Fähigkeiten verlassen, die der Client nicht deklariert hat. Braucht er zur Bearbeitung einer
Anfrage eine nicht deklarierte Fähigkeit, muss er MissingRequiredClientCapabilityError (-32021)
zurückgeben und die fehlenden Fähigkeiten in data.requiredCapabilities auflisten.
Die Spec nagelt die Bedeutung von “stateless” sehr deutlich fest: Der Server DARF Kontext NICHT (MUST NOT) aus früheren Anfragen derselben Verbindung ableiten. Sie stellt sogar ausdrücklich klar, dass selbst ein stdio-Prozess weder eine Konversation noch eine Session ist — ein Client darf über denselben Transport völlig unabhängige Anfragen mischen, und der Server darf die Identität von Verbindung oder Prozess nicht als Ersatz für Session-Kontinuität behandeln.
Ein kompletter Handshake-Roundtrip fällt damit weg, was in Umgebungen mit häufigen Cold Starts — Edge-Runtimes oder serverless — spürbar etwas bringt. Umgekehrt muss jeder Code angefasst werden, der sich auf Informationen stützte, die “einmal beim Initialisieren” geholt und danach dauerhaft verwendet wurden.
3. server/discover: jetzt Pflichtimplementierung (SEP-2575)
Dieser RPC füllt die Lücke, die der Handshake hinterlässt. Der Server MUSS server/discover
implementieren. Er dient dazu, unterstützte Protokollversionen, Fähigkeiten und Identität
bekanntzugeben.
Aus Client-Sicht gibt es zwei Verwendungen:
- vor anderen Anfragen aufrufen, um die Version auszuwählen (MAY)
- bei STDIO als Sonde für Abwärtskompatibilität — kommt eine Antwort, ist es modern; kommt keine, ist es Legacy
So sieht ein DiscoverResult aus. Vorsicht bei den Feldnamen — die Liste der unterstützten
Versionen heißt supportedVersions, und die Serveridentität steht nicht auf oberster Ebene des
Ergebnisses, sondern in _meta.
{
"resultType": "complete",
"supportedVersions": ["2026-07-28", "2025-11-25"],
"capabilities": { "tools": {}, "resources": {} },
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "my-mcp-server", "version": "2.0.0" }
}
}
Auch der Fehler, den der Server bei nicht passender Version zurückgibt, ist festgelegt. Die Liste der unterstützten Versionen muss mitgeschickt werden, damit der Client neu wählen kann.
{
"jsonrpc": "2.0", "id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
}
}
Wer einen Server baut, sollte bei dieser Migration genau diesen Code zuerst ergänzen. Fehlt er, ist die Implementierung nicht spezifikationskonform.
Alte und neue Clients gleichzeitig bedienen
Die Spec unterscheidet drei Sorten von Implementierungen.
| Begriff | Bedeutung |
|---|---|
| Modern | Version, Identität und Fähigkeiten reisen pro Anfrage in _meta mit (2026-07-28 und neuer) |
| Legacy | Session wird über den initialize-Handshake aufgebaut (2025-11-25 und älter) |
| Dual-era | Implementierung, die beides unterstützt |
Ein Dual-era-Server entscheidet danach, wie der Client ihn anspricht. Kommt eine Anfrage mit
modernem _meta, wird sie stateless bearbeitet; kommt initialize, greift der Legacy-Pfad. Beides
darf (MAY) über denselben Endpunkt bedient werden.
Die Erkennung auf Client-Seite unterscheidet sich je nach Transport.
- stdio — mit
server/discoveranklopfen; kommt kein erkennbarer moderner Fehler zurück, gilt der Server als Legacy - Streamable HTTP — eine moderne Anfrage senden und bei
400 Bad Requesterst den Body prüfen, dann zurückfallen
Das Ergebnis dieser Prüfung ist eine Eigenschaft des Servers, nicht der einzelnen Anfrage. Bei stdio SOLLTE es für die Lebensdauer des Prozesses gecacht werden, bei HTTP pro Origin.
Eine Kombination verdient besondere Aufmerksamkeit: Legacy-Client → moderner Server schlägt fehl.
Ein Legacy-Client hat schlicht kein Mittel, auf eine höhere Version zu wechseln. Deshalb SOLLTE ein
rein moderner Server in der Fehlermeldung auf eine initialize-Anfrage die unterstützten
Versionen nennen — diese Meldung ist womöglich der einzige Hinweis, den Nutzer eines
Legacy-Clients überhaupt zu sehen bekommen.
4. Subscriptions funktionieren anders (SEP-2575)
Der HTTP-GET-Endpunkt sowie resources/subscribe und resources/unsubscribe sind verschwunden und
in einem einzigen subscriptions/listen zusammengeführt — ein langlebiger POST-Antwortstream.
Der Client entscheidet sich explizit (Opt-in) für die Benachrichtigungsarten, die er erhalten möchte.
toolsListChangedpromptsListChangedresourcesListChangedresourceSubscriptions
Der Server bestätigt das und markiert die gesendeten Notifications mit
io.modelcontextprotocol/subscriptionId.
Hier lauert eine leicht zu übersehende Stelle: Anfragebezogene Notifications wie
notifications/progress und notifications/message laufen NICHT über den
subscriptions/listen-Stream. Sie fließen weiterhin über den Antwortstream der ursprünglichen
Anfrage. Wenn plötzlich keine Fortschrittsanzeige mehr erscheint, lohnt hier der erste Blick.
5. MRTR — wie der Server beim Client zurückfragt (SEP-2322)
Bisher gab es eine eigene Richtung, in der der Server Anfragen an den Client stellte: roots/list,
sampling/createMessage, elicitation/create und Ähnliches. Um die Spec wörtlich wiederzugeben:
Der Server MUSS solche Anfragen ab jetzt nach dem MRTR-Muster senden, das bisherige Muster
servergetriebener Anfragen wird nicht mehr unterstützt. Das ist ein Breaking Change.
Der neue Ablauf sieht so aus.
- Der Client sendet eine Anfrage (
id: 1) - Braucht der Server mehr Informationen, antwortet er mit einem
InputRequiredResultmitresultType: "input_required" - Der Client fragt den Nutzer und sendet die ursprüngliche Anfrage mit einer neuen
iderneut, diesmal mit der Antwort im Gepäck
An dieser Stelle geht am meisten schief: inputRequests und inputResponses sind keine Arrays,
sondern Maps. Der Schlüssel ist ein vom Server vergebener Bezeichner, der Wert ein vollständiges
Anfrageobjekt (method + params) vom Typ ElicitRequest, CreateMessageRequest oder
ListRootsRequest.
// Schritt 2 — Antwort des Servers
{
"jsonrpc": "2.0", "id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Bitte geben Sie Ihren GitHub-Benutzernamen ein",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
},
"requestState": "AEAD-protected blob"
}
}
Die Antwort ist eine Map mit denselben Schlüsseln, deren Werte die Ergebnisobjekte der jeweiligen Anfragen sind.
// Schritt 3 — Wiederholung mit neuer id, Antwort und requestState unverändert mitgeschickt
{
"jsonrpc": "2.0", "id": 2,
"method": "tools/call",
"params": {
"name": "delete_file",
"arguments": { "path": "a.txt" },
"inputResponses": {
"github_login": { "action": "accept", "content": { "name": "octocat" } }
},
"requestState": "AEAD-protected blob"
}
}
requestState — der Mechanismus, der MRTR stateless macht
requestState ist ein opaker String, der nur für den Server eine Bedeutung hat. Der Server
kodiert darin den Kontext, den er braucht, schickt ihn hinunter, und der Client gibt ihn unverändert
zurück. Genau deshalb braucht der Server keinen State Store.
Die Regeln sind ziemlich streng.
- Der Server MUSS in jedem
InputRequiredResultmindestens eines voninputRequestsundrequestStatemitschicken - Der Client MUSS den erhaltenen Wert exakt unverändert zurückspiegeln und DARF ihn NICHT (MUST NOT) inspizieren, parsen oder verändern. Hat er keinen erhalten, darf er auch keinen in die Wiederholung packen
- Die JSON-RPC-
idder Wiederholung MUSS sich von der ursprünglichen Anfrage unterscheiden — es ist eine eigenständige Anfrage
Besonders wichtig sind die Sicherheitsanforderungen. Dieser Wert läuft über den Client und ist damit eine von Angreifern manipulierbare Eingabe (die Spec schreibt mit MUST vor, ihn genau so zu behandeln). Wenn er Autorisierung, Ressourcenzugriff oder Geschäftslogik beeinflusst, MUSS der Server seine Integrität schützen (etwa per HMAC oder AEAD) und einen State, dessen Prüfung fehlschlägt, ablehnen. Weglassen darf man das nur, wenn eine Manipulation nichts weiter bewirkt, als dass die Anfrage fehlschlägt.
Zum Schutz vor Replay SOLLTE Folgendes in die integritätsgeschützte Payload aufgenommen und bei jedem Empfang geprüft werden.
- das authentifizierte Subjekt — vorgelegt von einem anderen Subjekt: ablehnen
- eine kurze Gültigkeitsdauer (TTL) — nach Ablauf vorgelegt: ablehnen
- eine Kennung der ursprünglichen Anfrage (Methodenname + Digest der wesentlichen Parameter) — bei einer nicht passenden Anfrage vorgelegt: ablehnen
Diese Maßnahmen verkleinern allerdings nur das Replay-Fenster und garantieren keine einmalige Verwendung. Wenn Einmalverwendung zwingend nötig ist, MUSS der Server sie separat erzwingen.
Nur drei Anfragen dürfen MRTR verwenden
| Client-Anfrage | InputRequiredResult erlaubt |
|---|---|
prompts/get |
Ja |
resources/read |
Ja |
tools/call |
Ja |
Bei allen anderen Anfragen DARF es NICHT (MUST NOT) gesendet werden. Außerdem darf der Server
keine Anfragearten in inputRequests packen, die der Client nicht als Fähigkeit deklariert hat —
hat der Client elicitation nicht deklariert, darf elicitation/create dort nicht auftauchen.
Aus einer bidirektionalen Anfrage ist ein unidirektionaler Retry geworden. Für den Transport ist das deutlich einfacher, aber der Client muss die Retry-Schleife selbst implementieren. Alle Callbacks, die bisher servergetriebene Anfragen behandelt haben, müssen auf diese Struktur umgezogen werden.
6. resultType ist in jedem Ergebnis Pflicht (SEP-2322)
Jedes Ergebnis trägt jetzt verpflichtend das Feld resultType.
"complete"— ein gewöhnliches Ergebnis"input_required"— ein MRTR-Zwischenergebnis
Für die Abwärtskompatibilität gibt es eine ausdrückliche Regel: Lässt ein Server einer früheren
Protokollversion das Feld weg, MUSS der Client es als "complete" behandeln. Wer einen Client
baut, sollte diesen Zweig unbedingt einbauen.
7. Was gestrichen wurde
ping
logging/setLevel
notifications/roots/list_changed
tasks/result (→ ersetzt durch Polling via tasks/get)
tasks/list
notifications/elicitation/complete
Das Log-Level wird jetzt pro Anfrage über io.modelcontextprotocol/logLevel in _meta festgelegt.
Und: Der Server DARF für Anfragen ohne dieses Feld KEINE (MUST NOT) notifications/message
senden. Wer bisher bedingungslos Logs herausgeschickt hat, muss eine Bedingung ergänzen.
Auch die Wiederaufnahme von SSE-Streams ist weg. Mit dem Entfernen des Headers Last-Event-ID und
der SSE-Event-IDs aus Streamable HTTP gilt: Bricht der Antwortstream ab, ist die laufende Anfrage
verloren. Der Client MUSS sie mit einer neuen Request-ID erneut senden. Code, der beim Reconnect
auf ein Fortsetzen gebaut hat, geht hier stillschweigend kaputt.
8. Tasks sind vom Core in eine Extension gewandert (SEP-2663)
Die bislang experimentellen Tasks sind aus dem Kernprotokoll herausgelöst und in die offizielle
Extension io.modelcontextprotocol/tasks umgezogen. Das war kein reiner Umzug — das Design wurde
neu aufgesetzt.
| Änderung | Inhalt |
|---|---|
tasks/result entfernt |
War blockierend. Ersetzt durch Polling via tasks/get |
tasks/update neu |
Für die Übergabe von Eingaben Client → Server |
tasks/list entfernt |
— |
| Rückgabe von Handles | Der Server darf Task-Handles ohne Opt-in pro Anfrage von sich aus zurückgeben |
Extensions werden über die Map extensions in den Capabilities bekanntgegeben. Die Bezeichner folgen
der Namenskonvention für _meta-Schlüssel, ein Präfix ist Pflicht.
{
"capabilities": {
"tools": {},
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}
Auch für den Fall, dass nur eine Seite die Extension unterstützt, gibt es eine Regel: Die unterstützende Seite MUSS auf das Kernverhalten zurückfallen oder mit einem passenden Fehler ablehnen.
9. Deprecated — Roots, Sampling, Logging (SEP-2577)
Drei Funktionen wurden zur Abkündigung vorgemerkt. Während der Deprecation-Phase funktionieren sie vollständig weiter, neue Implementierungen sollen sie aber nicht mehr verwenden. Die Spec empfiehlt folgenden Ersatz.
| Abgekündigt | Ersatz |
|---|---|
| Roots | Verzeichnisse und Dateien über Tool-Parameter, Ressourcen-URIs oder Serverkonfiguration übergeben |
| Sampling | Direkte Anbindung an die API des LLM-Anbieters |
| Logging | Bei stdio stderr, sonst OpenTelemetry |
Weitere Dinge wurden gleich mit als deprecated eingestuft.
- HTTP+SSE-Transport — seit
2025-03-26bereits deprecated, jetzt offiziell im Status Deprecated. Umzug auf Streamable HTTP nötig (SEP-2596) "thisServer"und"allServers"beiincludeContext— Feld weglassen oder"none"verwenden- OAuth 2.0 Dynamic Client Registration (RFC 7591) — ersetzt durch Client ID Metadata Documents. Für Autorisierungsserver, die diese nicht unterstützen, bleibt es aus Kompatibilitätsgründen erhalten
Wichtig ist hier die ebenfalls neu eingeführte Feature-Lifecycle-Policy. Sie definiert die drei Zustände Active / Deprecated / Removed; ein abgekündigtes Feature bleibt mindestens 12 Monate in der Spec, bevor es zur Entfernung ansteht. Das gilt allerdings nicht bedingungslos — greift die Ausnahme für beschleunigte Entfernung (expedited removal), verkürzt sich die Frist auf mindestens 90 Tage. Das ist etwa bei einem aktiven Sicherheitsrisiko der Fall. Die genannten Features verschwinden also nicht schon in der nächsten Revision, aber “ein Jahr lang auf jeden Fall sicher” ist die falsche Lesart.
10. Caching und Routing — leise Änderungen mit großer Praxiswirkung
Sie stehen nicht auf der Liste der Major Changes, wirken sich auf reale Implementierungen aber sofort aus.
Listenergebnisse sind cachebar geworden (SEP-2549). Die Ergebnisse von tools/list,
prompts/list, resources/list, resources/read und resources/templates/list tragen
verpflichtend ttlMs und cacheScope. ttlMs ist ein Frischehinweis in Millisekunden,
cacheScope legt mit "public" / "private" fest, ob zwischengeschaltete Caches das Ergebnis
teilen dürfen. Ziel ist weniger Polling.
Request-Header sind Pflicht geworden (SEP-2243). Jeder Streamable-HTTP-POST MUSS Mcp-Method und
Mcp-Name mitführen. So lässt sich routen, ohne den Body zu parsen. Wer MCP-Traffic vor einem
Reverse Proxy oder einer WAF unterscheiden musste, schaut jetzt nur noch auf die Header.
tools/list SOLLTE in deterministischer Reihenfolge antworten. Grund sind Client-Caching und die
Trefferquote des LLM-Prompt-Caches. Wer die Tool-Liste aus einer Map zieht und sie jedes Mal in
anderer Reihenfolge ausgibt, sollte sortieren — der Prompt-Cache dürfte bisher jedes Mal gebrochen
sein.
OpenTelemetry-Trace-Kontext ist standardisiert (SEP-414). traceparent, tracestate und
baggage werden über _meta propagiert.
11. Fehlercodes wurden neu nummeriert
Es gibt jetzt eine Vergabepolitik für Fehlercodes. Der JSON-RPC-Bereich für Serverfehler wird so aufgeteilt.
-32000bis-32019— implementierungsdefinierter Bereich (bestehende Verwendungen in SDKs bleiben gültig)-32020bis-32099— für die MCP-Spec reserviert
Entsprechend haben sich einige Nummern geändert.
| Fehler | Vorher | Nachher |
|---|---|---|
HeaderMismatch |
-32001 |
-32020 |
MissingRequiredClientCapability |
-32003 |
-32021 |
UnsupportedProtocolVersion |
-32004 |
-32022 |
| Ressource nicht gefunden | -32002 |
-32602 (Invalid Params) |
Hier entsteht leicht ein Missverständnis: Die ersten drei Codes sind überhaupt erst in der Draft-Phase dieser Revision entstanden und existieren in Code, der eine offiziell veröffentlichte Vorgängerversion implementiert, gar nicht. Tatsächlich in bestehendem Code stecken kann nur die letzte Zeile.
Dass “Ressource nicht gefunden” von -32002 auf -32602 (Invalid Params) wechselt, dient der
Angleichung an die JSON-RPC-Spezifikation. Wer diese Version implementiert, DARF -32002 NICHT
(MUST NOT) mehr senden; Clients SOLLTEN -32002 von Servern älterer Versionen aber weiterhin
akzeptieren.
12. Wer Cloudflare Workers nutzt — McpAgent ist abgekündigt
Am besten lässt sich am Beispiel Cloudflare zeigen, welche Wellen diese Spec-Änderung in realen
Implementierungen schlägt. Am 6. August 2026 hat Cloudflare mit “The next generation of MCP”
McpAgent abgeräumt.
Warum das die logische Konsequenz ist, ahnt man nach den Abschnitten 1 und 2. Bisher waren Durable
Objects faktisch Pflicht, um einen MCP-Server in Workers zu betreiben: Man musste die über
Mcp-Session-Id identifizierte Session und den daran hängenden SSE-Stream am Leben halten, also
brauchte man etwas, das Anfragen an dieselbe Instanz bindet. Mit dem Wegfall der Session fällt diese
Voraussetzung weg.
Cloudflare formuliert es so.
This release of the MCP 2026-07-28 specification … removes the need for
McpAgent. While Durable Objects remain the right primitive when an application itself needs state, MCP itself no longer requires a Durable Object to speak the protocol.
Laut Dokumentation ist der aktuelle Status von McpAgent “deprecated and feature-frozen”.
Unbrauchbar ist er damit nicht von heute auf morgen, aber neue Funktionen kommen nicht mehr dazu.
Der Ersatz heißt createMcpHandler; die offizielle Beschreibung lautet, er erzeuge aus einer
Server-Factory des MCP SDK v2 einen aufrufbaren, stateless MCP-Request-Handler.
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
} satisfies ExportedHandler;
Keine Durable-Object-Klasse, kein Session-Routing. Pro Anfrage wird der Server neu erzeugt, die Anfrage bearbeitet, fertig.
Auch hier lauert ein Missverständnis: Durable Objects sind nicht überflüssig geworden. Weggefallen ist nur das DO, das “zum Sprechen des MCP-Protokolls” nötig war. Braucht die Anwendung selbst Zustand, ist ein DO weiterhin das richtige Werkzeug. Die offizielle Dokumentation trifft genau diese Unterscheidung.
Store cross-request data behind an authenticated handle in a Durable Object, D1, KV, or R2 rather than an MCP session ID.
Genau darum ging es beim “explizit vom Server ausgestellten Handle” aus Abschnitt 1. Der Zustand liegt weiterhin in DO, D1, KV oder R2 — nur der Schlüssel, der auf ihn zeigt, ist jetzt ein authentifizierter Handle statt einer MCP-Session-ID.
Auch der Migrationsaufwand kommt nicht auf einen Schlag. Laut Cloudflare nimmt der /mcp-Endpunkt
“sowohl das neue Protokoll als auch stateless Anfragen von Streamable-HTTP-Clients aus 2025” an. Selbst
wenn es noch Nutzer mit alten Clients gibt, lässt sich der Server also zuerst umziehen.
Bei den Kosten ist Vorsicht geboten. Cloudflare spricht nur qualitativ von Einsparungen “durch weniger bewegliche Teile” und nennt keine konkreten Zahlen. Dass die Rechnung ohne DO-Instanzen tendenziell sinkt, ist klar — aber man sollte nicht mit einem bestimmten Prozentwert im Kopf herangehen.
13. Migrations-Checkliste
Wer einen Server betreibt, geht sie der Reihe nach durch.
-
server/discoverimplementieren — Pflicht, sonst nicht spezifikationskonform - Code, der von
Mcp-Session-Idabhängt, entfernen → servergenerierte Handles als Tool-Argumente - Handler für
initialize/notifications/initializedentfernen - Protokollversion und Client-Fähigkeiten pro Anfrage aus
_metalesen -
resultType: "complete"in allen Ergebnissen ergänzen -
resources/subscribe/unsubscribe→subscriptions/listen - Servergetriebene Anfragen (
roots/list,sampling/createMessage,elicitation/create) → MRTR - Handler für
pingundlogging/setLevelentfernen - Guard einbauen, damit für Anfragen ohne
logLevelkeinenotifications/messagegesendet werden -
ttlMsundcacheScopein Listenergebnissen ergänzen -
tools/listsortieren (Trefferquote des Prompt-Caches) - Fehlercode-Konstanten gegenprüfen
- Bei HTTP+SSE-Transport auf Streamable HTTP umsteigen
- Tasks in die offizielle Extension überführen —
tasks/result→ Polling viatasks/get,tasks/updateneu,tasks/listentfernt - Autorisierung:
issder Autorisierungsantwort verifizieren (MUST), Credentials nach Issuer keyen und NICHT (MUST NOT) bei anderen Autorisierungsservern wiederverwenden - Bei DCR
application_typeangeben und den Umstieg auf Client ID Metadata Documents prüfen - Bei Cloudflare Workers:
McpAgent→createMcpHandler, Zustand hinter authentifizierten Handles
Für Clients kommen noch zwei Punkte dazu.
- Antworten ohne
resultTypeals"complete"behandeln (Kompatibilität mit alten Servern, MUST) - MRTR-Retry-Schleife implementieren —
requestStateunverändert zurückspiegeln,idder Wiederholung anders als die der Originalanfrage - Erwartung an SSE-Resume streichen — bricht der Stream ab, mit neuer Request-ID neu senden
Fazit
Die vier Tier-1-SDKs — TypeScript, Python, Go und C# — sprechen bereits zum Release-Zeitpunkt
2026-07-28. Das Rust-SDK unterstützt sie im Beta-Status. Auch die offizielle Ankündigung räumt ein,
dass “für Entwickler, die sich auf Session-Identifier verlassen haben, ein gewisser Migrationsaufwand
anfällt”.
Die Richtung ist dabei eindeutig: impliziten Verbindungszustand abbauen und alles explizit in die Anfrage selbst packen. Wer MCP-Server in Umgebungen betreibt, in denen Instanzen ständig kommen und gehen — Edge, serverless —, macht mit dieser Änderung kein Minus, sondern ein Plus. Der Grund, einen Session Store festzuhalten, ist schlicht entfallen.
Die Deprecation-Frist beträgt mindestens 12 Monate, es muss also nicht heute alles umgebaut werden.
Eine Ausnahme gibt es allerdings: server/discover. Das ist keine Abkündigung, sondern eine neue
Pflichtanforderung — wer sagen will, dass er die neue Spec unterstützt, muss sie jetzt einbauen.