MCP ist stateless geworden: Was die Revision 2026-07-28 aus unserem Server entfernt hat
Wenn du einen MCP-Server betreibst, hast du irgendwo eine Session. Wahrscheinlich eine Tabelle, vielleicht eine Map im Speicher. Ein Client verbindet sich, schickt initialize, bekommt eine Mcp-Session-Id zurück und trägt diesen Header danach bei jedem Request mit. Du speicherst die Zeile. Du lässt sie nach einer Weile ablaufen. Du sorgst dafür, dass ein Request auf der Instanz landet, der sie gehört — oder du teilst den State zwischen den Instanzen.
Die Revision 2026-07-28 hat das gelöscht. Nicht deprecated: aus dem Protokollkern entfernt. Der Handshake ist weg, der Session-Header ist weg, und jeder Request trägt jetzt seine eigene Protokollversion und Client-Identität. Der Release-Post formuliert es so: „any request can now land on any server instance behind a plain round-robin load balancer without needing shared storage“ — jeder Request kann jetzt auf jeder Server-Instanz hinter einem einfachen Round-Robin-Load-Balancer landen, ohne dass geteilter Speicher nötig wäre.
Archyls MCP-Server bedient jetzt Clients, die die neue Revision sprechen. Dieser Post ist, was das gekostet hat, was wir danach gemessen haben, die eine Sache, die wir im ersten Anlauf falsch hatten, und was wir nicht gemacht haben. Wenn du einen MCP-Server pflegst, sind die interessanten Teile wahrscheinlich die Design-Entscheidung in der Mitte, der Bug, den das Audit des neuen Transports im alten zutage gefördert hat, und die Checkliste am Ende, um bei deinem nachzusehen.
Was die Revision tatsächlich entfernt hat
Direkt aus dem Changelog, die Teile, die eine Server-Implementierung betreffen:
- Sessions auf Protokollebene und der
Mcp-Session-Id-Header sind aus dem Streamable-HTTP-Transport entfernt. List-Endpunkte variieren nicht mehr pro Verbindung. - Der
initialize/notifications/initialized-Handshake ist entfernt. Jeder Request trägt seine Protokollversion und seine Client-Capabilities in_meta, und auf Streamable HTTP reist dieselbe Version imMCP-Protocol-Version-Header mit. server/discoverist neu und verpflichtend. Server MÜSSEN es implementieren, um unterstützte Protokollversionen, Capabilities und Identität bekanntzugeben. Clients DÜRFEN es vor allem anderen aufrufen; es steht ihnen ebenso frei, einen Request zu schicken und einen Versionsfehler zu behandeln.ping,logging/setLevelundnotifications/roots/list_changedsind entfernt.- Versionskonflikte liefern
UnsupportedProtocolVersionErrorzurück, mit der Liste der Versionen, die der Server unterstützt, damit der Client es erneut versuchen kann.
Da steht noch mehr drin (Multi Round-Trip Requests, subscriptions/listen, cachebare List-Ergebnisse, ein neu nummerierter Block von Fehlercodes, Härtung der Authorization), und ich komme darauf zurück, welche davon wir gemacht und welche wir ausgelassen haben. Die fünf oben sind die, die die Form eines Servers ändern und nicht seine Features.
Ein Punkt, bei dem Präzision sich lohnt, weil er die Entscheidung verändert: Das hier ist kein Release Candidate mehr. Der Release Candidate wurde am 21. Mai 2026 eingefroren und öffnete ein zehnwöchiges Validierungsfenster für SDK-Maintainer und Client-Implementierer. Dieses Fenster schloss sich am 28. Juli 2026, als die Spezifikation ausgeliefert wurde, und die Versioning-Seite nennt 2026-07-28 jetzt „the current protocol version“ — die aktuelle Protokollversion. Alle vier Tier-1-SDKs (TypeScript, Python, Go, C#) sprechen sie seit dem Release-Tag, Rust ist in Beta. Wenn du gewartet hast, bis sich der RC setzt: Er hat sich gesetzt.
Was das für einen Server mit 181 Tools bedeutet hat
Archyls MCP-Server stellt 181 Tools über das C4-Modell bereit: Projekte, Systeme, Container, Components, Relationships, ADRs, Docs, Contracts, Conformance, Drift, DORA, Ownership. Vor dieser Änderung saßen alle 181 hinter einer Session.
Konkret, in unserem Backend:
- Jede Verbindung hat eine Zeile in einer
mcp_sessions-Tabelle angelegt, mit 24 Stunden Ablauffrist und einer Hintergrund-Goroutine, die abgelaufene und verwaiste Zeilen wegräumt. - SSE-Response-Channels lebten in einer
map[string]chan *JSONRPCMessageauf dem Server-Struct, mit der Session-ID als Schlüssel, was eine Verbindung an den Prozess band, der sie geöffnet hatte. Diese Map ist inzwischen umgezogen, und der Grund war am Ende ein Bug und keine Präferenz. Ich komme weiter unten darauf zurück. - Vier Handler (
tools/list,tools/call,resources/list,resources/read) begannen mit denselben drei Zeilen:
if !session.Initialized {
return s.errorResponse(msg.ID, ErrCodeInvalidRequest, "Session not initialized", nil)
}
Dieser Guard ist der interessante. Er stellt eine Frage, die das neue Protokoll unbeantwortbar gemacht hat: Hat dieser Aufrufer den Handshake abgeschlossen? Es gibt keinen Handshake, den man abschließen könnte.
Die Entscheidung, die die Änderung klein gehalten hat
Der verlockende Zug ist, diesen vier Handlern Statelessness beizubringen. Eine zweite Bedingung hinzufügen, oder ein session.Stateless || vor jede Prüfung, oder das Ganze in eine Middleware hochziehen.
Wir haben nichts davon gemacht. Die Guards sind unberührt. Stattdessen bekommt ein Request, der 2026-07-28 deklariert, eine In-Memory-Session, die für genau diesen einen Request gebaut wird und den Guard per Konstruktion erfüllt:
func (s *Server) NewStatelessSession(userID, organizationID uuid.UUID, protocolVersion string) *Session {
now := time.Now()
return &Session{
Session: &mcpsession.Session{
ID: "",
UserID: userID,
OrganizationID: organizationID,
Initialized: true,
ProtocolVersion: protocolVersion,
Transport: "streamable",
LastAccessedAt: now,
CreatedAt: now,
},
Stateless: true,
}
}
Nichts wird persistiert. Keine ID wird vergeben. Kein SSE-Channel wird registriert. Initialized: true ist keine Lüge und kein Bypass: Unter dieser Revision ist der Request tatsächlich initialisiert, weil das Protokoll seine eigene Version mitführt und der Handshake, den er sonst abgeschlossen hätte, nicht mehr existiert.
Warum diese Rahmung mehr zählt, als sie aussieht: Diese vier Guards sitzen auf einem Pfad, der an Autorisierung grenzt. Jeder einzelne ist der Unterschied zwischen einem Tool-Call, der läuft, und einem, der abgelehnt wird. Vier Aufrufstellen zu editieren, die alle eine sicherheitsförmige Frage beantworten, sind vier Gelegenheiten, eine Prüfung zu schwächen, verteilt über einen Diff, den ein Reviewer auf einmal im Kopf halten muss. Das Objekt zu bauen, das die Guards ohnehin erwarten, ist eine neue Funktion, und jede bestehende Prüfung behält ihre exakte Bedeutung.
Es scheitert außerdem in eine sichere Richtung. Wenn unsere Versionserkennung falsch liegt und ein stateless Request als Legacy-Request missverstanden wird, ist die Konsequenz, dass eine Session-Zeile für ihn angelegt wird. Nichts wird durchgelassen, was vorher nicht durchgelassen worden wäre. Das umgekehrte Design — die Guards aufweichen und sie an einen Versionsstring hängen — scheitert andersherum.
Statelessness hat uns auch keine Mandantentrennung gekostet, weil die Identität ohnehin nie aus der Session-Zeile kam. Die stateless Session trägt den User und die Organisation, die aus dem API-Key oder dem OAuth-Token dieses Requests aufgelöst wurden, Scopes werden bei jedem Aufruf neu abgeleitet, sodass das Widerrufen eines Keys sofort wirkt, und tools/call verweigert weiterhin eine Session ohne gebundenen Mandanten. Es gibt jetzt eine Sache weniger zu stehlen: keine gespeicherte Session-ID, die sich replayen ließe. Auf dem Legacy-Pfad haben wir die entsprechende Prüfung behalten, damit eine Session-ID nicht dazu führen kann, dass ein Credential unter der Identität handelt, die in der Session eines anderen gespeichert ist.
Das Routing, in einem switch
Die ganze Entscheidung lebt im HTTP-Handler, noch bevor der JSON-RPC-Body überhaupt geparst wird:
switch {
case mcp.IsModernProtocolVersion(requestedVersion):
// Stateless: the request describes itself, so nothing is looked up,
// nothing is written, and no Mcp-Session-Id comes back.
session = h.mcpServer.NewStatelessSession(auth.UserID, auth.OrganizationID, requestedVersion)
case sessionID != "":
// Handshake-based client with a session: look it up, and check it
// belongs to this credential.
default:
// Legacy client that has not handshaken yet: mint a session as before.
}
Zwei Details darin, die man leicht übersieht:
IsModernProtocolVersion ist ein String-Vergleich gegen "2026-07-28". Revisionen sind YYYY-MM-DD, also ist lexikalische Ordnung chronologische Ordnung, und eine künftige Revision landet standardmäßig auf der stateless Seite, statt auf den Handshake zurückzufallen. Sie kommt nur so weit, wenn wir sie unterstützen: Eine unbekannte Version wird vor dem switch abgelehnt, mit der Liste der unterstützten Versionen in den Error-Daten, damit der Client es erneut versuchen kann.
Und der Response-Header:
if !session.Stateless {
c.Set("Mcp-Session-Id", session.ID)
}
Eine stateless Session hat keine ID. Eine leere Mcp-Session-Id zurückzuspiegeln würde einem Client sagen, er solle etwas wiederverwenden, das nicht existiert — ein schlimmerer Bug, als sie gar nicht zu senden, und genau die Sorte Sache, die nur gegen einen Client auffällt, den du nicht selbst geschrieben hast.
Der Rest des Changelogs, richtig gelesen
Der Versions-Switch ist die interessante Entscheidung. Der Rest der Revision ist eine Liste kleiner Anforderungen, die leicht zu übersehen und billig zu prüfen sind, also sind wir das Changelog Zeile für Zeile noch einmal durchgegangen. Vier davon sind in diesem Durchgang gelandet.
resultType auf jedem Result. Die Revision macht das Feld verpflichtend: "complete" für eine fertige Antwort, "input_required" für das Zwischenergebnis im Multi-Round-Trip-Muster. Clients wird gesagt, sie sollen sein Fehlen bei einem älteren Server als "complete" behandeln, aber ein Client, der die finale Revision liest, sucht danach. Unsere tragen es jetzt, aus einem Result-Struct, das in jedem Result-Typ eingebettet ist, statt dass jeder Typ sich das Feld selbst merkt.
ttlMs und cacheScope auf List-Ergebnissen. Verpflichtend für tools/list, prompts/list, resources/list, resources/read und resources/templates/list, über ein neues CacheableResult-Interface. Wir hosten drei dieser fünf, und sie liefern 60000 und private. Sechzig Sekunden sind ein Hinweis, kein Vertrag: lang genug, um einen Agenten davon abzuhalten, in jedem Zug 181 Tools neu aufzulisten, kurz genug, dass ein mitten in der Session registriertes Tool schnell auftaucht. private ist eine Entscheidung, kein Default, den wir übernommen haben. Jedes Ergebnis, das wir zurückgeben, ist auf die Organisation des Aufrufers begrenzt, also darf kein geteilter Intermediär eines cachen und einem anderen Mandanten aushändigen.
DELETE /mcp von einem Client, der 2026-07-28 deklariert. DELETE hat eine Session auf Protokollebene beendet, und es gibt keine Sessions auf Protokollebene mehr. Die Spezifikation sagt, man solle mit 405 antworten, und genau das bekommt ein moderner Client. Ein handshake-basierter Client behält das alte Verhalten.
Eine nicht implementierte Methode liefert jetzt HTTP 404 mit JSON-RPC -32601 im Body. Der Statuscode allein ist mehrdeutig: Ein Legacy-HTTP+SSE-Server, der den modernen Endpunkt gar nicht hostet, antwortet ebenfalls mit 404. Der JSON-RPC-Body ist das, was die beiden auseinanderhält, und die Spezifikation ist explizit darin, dass ein Client ihn benutzt, um zu entscheiden, ob er auf initialize zurückfällt oder es erneut versucht.
Und eine Sache, die wir im ersten Anlauf falsch hatten
Unser Unsupported-Version-Fehler lieferte -32600, das generische JSON-RPC „invalid request“. Das war vertretbar — bis genau zu dieser Revision, die eine Policy zur Vergabe von Fehlercodes definiert, die den JSON-RPC-Server-Error-Bereich aufteilt: -32000 bis -32019 bleibt implementierungsdefiniert, -32020 bis -32099 gehört der Spezifikation. Die während des Drafts eingeführten Codes wurden in diesen Block umnummeriert. HeaderMismatch ging -32001 → -32020, MissingRequiredClientCapability -32003 → -32021, und UnsupportedProtocolVersion -32004 → -32022.
Ein Client, der gegen die finale Revision geschrieben ist, sucht nach -32022. Er hätte nicht erkannt, was wir geschickt haben, und der Fehlermodus ist genau der, den diese ganze Revision vermeiden soll: Der Client kann „falsche Version, hier sind die, die ich spreche“ nicht von „dein Request war fehlerhaft“ unterscheiden, also hat er nichts, womit er es erneut versuchen könnte.
Nichts hat das gefangen außer dem Changelog ein zweites Mal zu lesen, was das eigene Argument dieses Posts ist, zurückgerichtet auf uns. Die Umnummerierung ist Punkt 12 der Minor Changes, nach den Einträgen zu OpenTelemetry-_meta-Keys und JSON-Schema-Keywords. Es ist die Sorte Zeile, die man überfliegt.
Ein Rename hat uns nichts gekostet. Resource-not-found wanderte von -32002 zu -32602, passend zu JSON-RPCs „invalid params“, und resources/read antwortete bei einer unbekannten URI ohnehin schon mit -32602.
Was wir gemessen haben
All das wurde gegen einen laufenden Container auf diesem Build gemessen, mit einem echten API-Key, sodass wir die Zeilen in Postgres direkt zählen konnten.
| Test | Ergebnis |
|---|---|
tools/list mit MCP-Protocol-Version: 2026-07-28, ohne Handshake |
181 Tools |
Mcp-Session-Id in dieser Response zurückgespiegelt |
keine |
resultType bei tools/list und bei server/discover |
complete |
ttlMs / cacheScope bei tools/list |
60000 / private |
server/discover |
["2026-07-28", "2025-03-26"] |
| Nicht unterstützte Version deklariert | -32022, unterstützte Liste in den Error-Daten |
DELETE /mcp von einem Client, der 2026-07-28 deklariert |
405 |
| Unbekannte Methode | 404 mit -32601 |
Legacy-initialize-Handshake |
funktioniert weiterhin |
Legacy-tools/list mit einer Session-ID |
181 Tools |
mcp_sessions-Zeilen, angelegt durch 10 stateless Requests |
0 |
mcp_sessions-Zeilen, angelegt durch 3 Legacy-Requests |
3 |
Das letzte Paar ist das, worauf man schauen sollte. Zehn Requests, keine Zeilen. Die drei Legacy-Requests kamen jeweils ohne Session-ID an, also hat jeder eine angelegt; ein wohlerzogener handshake-basierter Client, der seine ID wiederverwendet, bekommt eine Zeile für die Lebensdauer seiner Session, nicht eine pro Aufruf. Der Punkt ist die Null: Auf dem stateless Pfad gibt es nichts zu schreiben, nichts, das ablaufen könnte, und nichts, was die Cleanup-Goroutine finden könnte.
Der Request, der die erste Zeile dieser Tabelle erzeugt hat, gerichtet auf den öffentlichen Endpunkt:
curl -s https://api.archyl.com/mcp \
-H "X-API-Key: $ARCHYL_API_KEY" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Kein initialize. Keine Session. 181 Tools.
Der Bug, den der deprecated Transport versteckt hat
Das Audit des neuen Transports hat uns dazu gebracht, den alten anzusehen, und der alte hatte einen echten Bug.
Der HTTP+SSE-Transport aus 2024-11-05 teilt eine Konversation auf zwei Verbindungen auf. Der Client öffnet mit GET einen langlebigen Stream, das erste Event des Servers sagt ihm, wohin er POSTen soll, und von da an geht jede Nachricht per POST raus, während jede Antwort auf dem Stream zurückkommt. Diese beiden Verbindungen müssen nicht auf derselben Instanz landen.
Unserer nahm an, dass sie es tun. Response-Channels lebten in dieser map[string]chan *JSONRPCMessage auf dem Server-Struct, also schrieb ein POST, der von Instanz B behandelt wurde, seine Antwort in einen Channel, der auf Instanz B existierte und den auf Instanz B niemand las. Der Stream war auf Instanz A. Der Client wartete.
Was das schlimmer macht als einen Design-Geruch: Es wurde nichts geloggt. Kein Error, keine Warnung, kein fehlgeschlagener Request. Der POST lieferte 202 Accepted, was stimmte — die Nachricht war angenommen worden —, und die Antwort ging nirgendwohin. Von außen ist das nicht von einem langsamen Tool-Call zu unterscheiden. Es passiert nur in einem horizontal skalierten Deployment, also genau dort, wo man am wenigsten Lust hat, etwas von Hand zu reproduzieren.
Die Map ist jetzt ein Redis-Pub/Sub-Router in streamrouter.go. Eine Antwort für einen Stream, den dieser Prozess hält, wird direkt zugestellt und macht den Umweg nie. Eine Antwort für einen Stream, der woanders gehalten wird, wird auf mcp:stream:<sessionID> publiziert, und die Instanz, die diesen Stream hält, ist darauf subscribed. Jede Instanz kann den POST übernehmen. Es ist keine Session-Affinität nötig, und es muss keine Sticky-Session-Regel in einer Load-Balancer-Config gepflegt werden, an deren Entstehung sich niemand erinnert.
Zwei Dinge sind dazu erwähnenswert, denn ein Router ist eine Dependency.
Redis liegt jetzt auf diesem Pfad. Ist es beim Start nicht erreichbar, fällt der Router auf reine lokale Zustellung zurück und loggt eine Warnung, statt den Start zu verweigern — denn lokal-only ist für eine einzelne Instanz korrekt und wird erst falsch, sobald es eine zweite gibt. Der Fehler ist mit Absicht laut: Die Alternative ist genau das stille Hängen, das wir gerade beseitigt haben. Wenn du das deployst, ist die Startzeile, nach der du suchen musst, MCP stream router: Redis connected. Ihr Fehlen ist die ganze Geschichte.
Und der Router repariert Routing, nicht Ort. Der Stream ist weiterhin eine Verbindung, die ein Prozess hält; Redis trägt Antworten zu diesem Prozess, es verschiebt den Stream nicht. Dieser Teil ist nicht reduzierbar. Eine offene Verbindung lebt dort, wo sie geöffnet wurde, in jedem Protokoll.
Was wir nicht gemacht haben
Hier hört eine Ankündigung normalerweise auf. Zwei Dinge sind es wert, klar gesagt zu werden, weil du beide nachprüfen kannst.
Archyl spricht 2026-07-28 auf dem Pfad, auf den es ankommt. Es ist nicht Ende-zu-Ende stateless.
Der stateless Pfad ist tatsächlich stateless: kein Session-Lookup, kein Session-Write, keine Mcp-Session-Id, nichts, was einen Request an einen Prozess bindet. Dieser Pfad kann hinter einem einfachen Round-Robin-Load-Balancer sitzen.
Unser Server beantwortet außerdem weiterhin den älteren HTTP+SSE-Transport unter /sse, aber wir dokumentieren ihn nicht mehr. Jede Seite, auf der früher diese URL stand, zeigt jetzt /mcp, und das ist der einzige Endpoint, um dessen Konfiguration wir überhaupt noch jemanden bitten.
Der Grund ist die Dependency, die wir gerade hinzugefügt haben. Der Router entfernt die Affinitätsanforderung nur dort, wo Redis erreichbar ist. Wo es das nicht ist, fällt die Zustellung auf lokal-only zurück, was bei einer Instanz korrekt und bei zweien stillschweigend falsch ist. Unsere eigene Produktion betreibt heute kein Redis, wir laufen also mit genau diesem Fallback. Uns ist es lieber, alle auf den Transport zu zeigen, dessen Korrektheit nicht von einer Instanzenzahl abhängt, als einen zu veröffentlichen, dessen Korrektheit davon abhängt.
Was für /sse wahr bleibt, wo immer es läuft: Der Stream ist eine Verbindung, die ein einzelner Prozess hält, und eine Session-Zeile existiert in Postgres für dessen Lebensdauer. Die Affinitätsanforderung zu entfernen ist nicht dasselbe wie den State zu entfernen. Wir kündigen kein Datum an, an dem wir diesen Transport abschalten.
Die Uhr für diesen Transport ist allerdings nicht unsere, und sie läuft kürzer, als wir angenommen hatten. HTTP+SSE ist seit der Revision 2025-03-26 deprecated; was 2026-07-28 getan hat, war, ihn unter der neuen Feature-Lifecycle-Policy als Deprecated neu einzustufen. Diese Policy setzt ein Mindestfenster von zwölf Monaten zwischen Deprecation und Eignung zur Entfernung an, und das bekommen Roots, Sampling und Logging: früheste Entfernung in „the first revision released on or after 2027-07-28“ — der ersten Revision, die am oder nach dem 28. Juli 2027 erscheint. HTTP+SSE bekommt keine zwölf Monate, weil es schon lange vor der Existenz dieser Policy deprecated war. Das Register der deprecated Features führt als früheste Entfernung „Three months after SEP-2596 reaches Final“ — drei Monate, nachdem SEP-2596 den Status Final erreicht. Die Entfernung bleibt eine Entscheidung der Core Maintainer, die bei der Release-Vorbereitung getroffen wird, und kann auch später fallen, aber wenn du HTTP+SSE irgendwo betreibst, ist das die Zeile, die du lesen solltest.
Wir haben die Form der Revision implementiert, nicht die ganze Revision. Was ausgeliefert wird, ist Versionsaushandlung, der stateless Request-Pfad, server/discover, der Unsupported-Version-Fehler mit dem richtigen Code, resultType, die Cache-Hinweise, und die 405 und 404, die der Transport verlangt, daneben der Handshake-Pfad für Clients, die ihn noch brauchen. Und das hier ist nicht da:
- Die Request-Header
Mcp-MethodundMcp-Nameund die Validierung, die dazugehört. Das ist die größte Lücke. Die Revision verlangt, dass ein POST seinemethodund seinparams.nameoderparams.uriin Header spiegelt, und verlangt vom Server, jede Abweichung mit400und-32020 HeaderMismatchabzulehnen. Der Grund ist nicht Ordentlichkeit. In den Worten der Spezifikation selbst: Es „prevents potential security vulnerabilities when different components in the network rely on different sources of truth (e.g., a load balancer routing on the header value while the MCP server executes based on the body value)“ — es verhindert potenzielle Sicherheitslücken, wenn verschiedene Komponenten im Netz sich auf verschiedene Wahrheitsquellen stützen (etwa ein Load Balancer, der nach dem Header-Wert routet, während der MCP-Server nach dem Body-Wert ausführt). Dieselbe Regel gilt fürMCP-Protocol-Version, dessen Wert mit dem im_metades Requests übereinstimmen MUSS. Wir lesen die Version ausschließlich aus dem Header und sehen_metanie an, können also eine Abweichung, die wir ablehnen müssten, gar nicht erkennen. Der Header ist verfügbar, bevor der Body geparst ist, deshalb lesen wir ihn dort. Das ist kein Grund, den Cross-Check auszulassen. subscriptions/listenund Multi Round-Trip Requests mitInputRequiredResult. Ganze Features statt Fixes. Wir habenresources/subscribenie implementiert, also kostet uns die Methode, die es ersetzt, heute nichts.- Validierung des
Origin-Headers. Die Spezifikation markiert sie als MUSS, mit403bei einem ungültigen Origin, als Verteidigung gegen DNS-Rebinding. Wir machen das auf/mcpnicht. extensionsauf Capabilities und deterministische Reihenfolge austools/list. Das zweite ist ein SHOULD, gerichtet auf clientseitiges Caching und die Trefferquote von LLM-Prompt-Caches. Unsere kommen aus einer Go-Map, die Reihenfolge ist also das, was diese Map uns an dem Tag gibt.- Dynamic Client Registration. Diese Revision erklärt sie zugunsten von Client ID Metadata Documents für deprecated, und wir stellen weiterhin
POST /registerbereit. Es bleibt verfügbar für Authorization Server, die den Ersatz nicht unterstützen, das ist also eine Migration und kein Bruch, auf derselben Zwölf-Monats-Uhr wie Roots, Sampling und Logging. server/discoversitzt hinter demselben API-Key wie alles andere auf/mcp. Es antwortet keinem anonymen Aufrufer, was eine bewusste Entscheidung ist und nicht das, was ein Client erwartet, der einen Server entdeckt.
Der Rest ist Arbeit, und er steht auf der Liste, statt erledigt zu sein.
Wenn du deinen eigenen MCP-Server betreibst
Die Checks, die sich gegen deinen lohnen:
- Schick
tools/listmitMCP-Protocol-Version: 2026-07-28und ohne Handshake. Wenn du „session not initialized“ bekommst, bedient dein Server die aktuelle Revision nicht. - Ruf
server/discoverauf. Es ist jetzt verpflichtend. Wenn es method-not-found zurückgibt, ist das die kleinste Lücke, die du schließen kannst. - Deklariere eine Version, die du nicht unterstützt. Prüf, dass der Fehler die Liste der Versionen trägt, die du unterstützt, und dass sein Code
-32022ist und nicht ein generischer. Das ist der Check, den wir nicht bestanden haben. - Lies irgendein Result. Jedes davon braucht
resultType, und deine List-Ergebnisse brauchen obendreinttlMsundcacheScope. - Sieh dir an, was du bei einem stateless Request in
Mcp-Session-Idzurückspiegelst. Leer ist schlimmer als gar nicht da. - Zähl deine Writes. Schick zehn stateless Requests und prüf, ob irgendetwas in deinem Session-Store gelandet ist. Diese Zahl ist die ehrliche Antwort darauf, ob die Migration funktioniert hat.
- Wenn du HTTP+SSE noch bedienst und mehr als eine Instanz betreibst, POSTe an eine, während der Stream von einer anderen gehalten wird. Ein Client, der hängt, ohne dass etwas in den Logs steht, ist der Bug, den wir hatten. Lies dann die oben genannte Zeile im Deprecation-Register.
Die Lücke zwischen „akzeptiert den neuen Versions-Header“ und „tatsächlich stateless“ ist der Ort, an dem die meiste Arbeit liegt, und nur Schritt 6 sagt dir, auf welcher Seite du stehst.
Verbinden
Der Endpunkt ist unverändert, und beide Revisionen funktionieren dagegen. Für Claude Code eine .mcp.json im Root deines Projekts:
{
"mcpServers": {
"archyl": {
"type": "http",
"url": "https://api.archyl.com/mcp",
"headers": {
"X-API-Key": "YOUR_API_KEY"
}
}
}
}
Dein Client wählt die Revision. Spricht er 2026-07-28, wird er ohne Handshake und ohne Session bedient. Tut er es nicht, ändert sich für ihn nichts.
Das vollständige Setup für Claude Code, Cursor, VS Code, Codex, Warp, Windsurf und Antigravity, samt der Scopes, die entscheiden, was ein Agent ändern darf, steht in der Dokumentation zum MCP-Server.