Wat is een stateless MCP-server? Uitleg revisie 2026-07-28
De MCP-revisie van 2026-07-28 verwijdert de initialize handshake en sessies. Ontdek wat dit betekent voor uw reverse proxy, health checks, time-outs en authenticatie-instellingen.
Wat een stateless MCP-server is
Een stateless MCP-server houdt geen per-client status bij tussen verzoeken. Elk verzoek bevat de protocolversie, de client-mogelijkheden en de inloggegevens die de server nodig heeft om het verzoek te beantwoorden. Hierdoor kan elk proces op elke machine elk verzoek afhandelen. MCP (Model Context Protocol, het wire-formaat dat agents gebruiken om tools te bereiken) heeft dit tot een regel gemaakt in revisie 2026-07-28, waarbij de initialize-handshake en de HTTP-sessie die daaronder lag, zijn verwijderd.
Dat is het volledige operationele doel. Een server die niets per client bijhoudt, kan achter een standaard load balancer staan zonder sessie-affiniteit, kan tijdens een deploy worden herstart zonder dat clients de verbinding verliezen, en kan als vier identieke processen draaien in plaats van als één. Een sessie-georiënteerde server kan dit niet zonder extra infrastructuur.
Het Model Context Protocol is een stateless protocol: alle informatie die nodig is om een verzoek te verwerken, zit in het verzoek zelf. Een server verwerkt elk verzoek onafhankelijk; er mag geen status worden afgeleid uit eerdere verzoeken, zelfs niet als deze via dezelfde verbinding of stream binnenkomen.
Stateless betekent niet dat uw server niets opslaat. Uw database, uw wachtrij en uw cache zijn nog steeds aanwezig. Het betekent dat het protocol geen status meedraagt over de verbinding. De server mag een verbinding, een proces of een open socket daarom niet beschouwen als een vervanging voor "deze client, midden in een gesprek".
Wat is verwijderd in revisie 2026-07-28
2026-07-28 is de huidige revisie van de specificatie per augustus 2026. Vergeleken met 2025-11-25 zijn er vijf zaken verwijderd die dienden ter ondersteuning van sessies.
- Het
initialize-verzoek en denotifications/initialized-notificatie. Er is geen enkele vorm van handshake meer (SEP-2575). - De
Mcp-Session-Id-header en sessiebeëindiging met HTTPDELETE(SEP-2567). - De zelfstandige HTTP
GET-stream waarop servers notificaties pushten. Deze is vervangen doorsubscriptions/listen, een reguliere POST waarvan het antwoord een langdurige stream is. - De hervatbaarheid van SSE (server-sent events)-streams. De
Last-Event-ID-header en per-event ID's zijn verwijderd; een verbroken stream betekent dat het lopende verzoek verloren gaat en de client dit opnieuw moet indienen als een nieuw verzoek met een nieuw verzoek-ID. ping,logging/setLevelennotifications/roots/list_changed. Het logniveau is nu een veld per verzoek,io.modelcontextprotocol/logLevelin_meta.
Er is één methode toegevoegd die elke server moet implementeren. server/discover retourneert in één aanroep de ondersteunde protocolversies, mogelijkheden en identiteit van de server. Dit is het enige dat nog enigszins op een handshake lijkt, en het aanroepen ervan is optioneel voor clients.
Waarom het sessietransport lastig was in productie
In 2025-11-25 en eerdere versies kon een server bij initialisatie een sessie-ID aanmaken en deze retourneren in de Mcp-Session-Id-header bij de InitializeResult. De client moest die header vervolgens bij elk volgend verzoek meesturen. De onderhandelde protocolversie en de mogelijkheden van de client werden in het geheugen van de server opgeslagen, gekoppeld aan dat ID. Elk van deze keuzes brengt operationele kosten met zich mee.
- Een herstart verwijderde de sessietabel. De specificatie vereiste dat de server op elk verzoek met een ongeldig sessie-ID antwoordde met
404 Not Found, en vereiste dat de client opnieuw begon met een nieuweInitializeRequest. Elke uitrol werd daardoor een herverbindingsgebeurtenis voor elke verbonden client. - Een tweede replica kende de sessies van de eerste replica niet. Schalen betekende sticky routing bij de load balancer, of een gedeelde sessie-opslag die elke replica bij elk verzoek moest uitlezen.
- De sessietabel was geheugen dat groeide naarmate er meer inactieve clients waren.
DELETEwas optioneel, en clients die de verbinding verbroken zonder dit te sturen, lieten vermeldingen achter. - Lijstresultaten konden per verbinding verschillen, waardoor caching voor de server onveilig was.
Het verwijderen van sessies heft al deze vier punten tegelijk op. Dat is de wijziging die u moet begrijpen voordat u configuraties aanpast.
Wat elk verzoek nu bevat
Elke POST naar het MCP-eindpunt staat op zichzelf. De protocolversie en de client-mogelijkheden worden meegestuurd in de request body onder _meta, en geselecteerde velden worden gespiegeld in HTTP-headers zodat een tussenliggende partij hierop kan routeren zonder JSON te hoeven parsen.
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"location": "Seattle, WA"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}io.modelcontextprotocol/protocolVersion en io.modelcontextprotocol/clientCapabilities zijn vereist bij elk verzoek. clientInfo is niet verplicht, hoewel clients dit wel zouden moeten meesturen. Een verzoek waarbij een verplicht veld ontbreekt is ongeldig; de server moet dit daarom afwijzen met JSON-RPC-fout -32602 en HTTP 400 Bad Request.
De Mcp-Method-header is vereist bij elk verzoek. Mcp-Name is vereist bij tools/call, resources/read en prompts/get. De waarde van de header moet overeenkomen met de body. Een server die de body verwerkt, moet een mismatch afwijzen met 400 Bad Request en foutcode -32020, HeaderMismatch. Deze regel bestaat omdat een load balancer die routeert op basis van de header en een server die de body uitvoert, twee verschillende bronnen van waarheid vormen. Als u routeert of rate-limiting toepast op basis van deze headers, controleer dan eerst MCP-Protocol-Version: eerdere revisies valideerden de header nooit tegen de body, waardoor de waarde van de header in die versies niet betrouwbaar is.
Een versieconflict is nu een standaardfout per verzoek in plaats van een mislukte handshake. Een server die de gevraagde versie niet implementeert, antwoordt met 400 Bad Request met fout -32022, UnsupportedProtocolVersion, en vermeldt wat wel wordt ondersteund in data.supported. De client kiest er een uit die lijst en probeert het opnieuw.
Waar de status is gebleven: tokens, cursors en subscriptions
De status is niet verdwenen. Deze is verplaatst naar locaties die u kunt inzien en loggen.
Credentials worden bij elk verzoek meegestuurd. Er is geen sessie waaraan een identiteit kan worden gekoppeld, dus het access token reist mee met elke HTTP-aanroep en wordt telkens gevalideerd. Details vindt u in de onderstaande sectie over authenticatie.
Cursors moeten hun eigen positie bevatten. Paginering op tools/list, resources/list, prompts/list en resources/templates/list maakt gebruik van een ondoorzichtige cursor-string; clients mogen deze niet parsen of wijzigen. Op een server met één proces was het gebruikelijk om de offset in het geheugen bij te houden, gekoppeld aan de sessie. Zonder sessie moet de cursor voldoende informatie bevatten voor elke replica om de lijst te hervatten. Codeer daarom de positie in de cursor en onderteken deze, of sla de positie op in een opslag die alle replica's delen. Een ongeldige cursor moet -32602 retourneren. Onderteken de cursor, omdat een ondoorzichtige cursor nog steeds input is die door de client wordt aangeleverd, die uw code decodeert en vertrouwt.
Subscriptions horen bij een verzoek, niet bij een verbinding. Een client die notificaties over wijzigingen wil ontvangen, verstuurt subscriptions/listen met een filter dat de gewenste typen specificeert: toolsListChanged, promptsListChanged, resourcesListChanged en resourceSubscriptions. De server antwoordt met notifications/subscriptions/acknowledged en houdt die respons-stream open. Als de stream wegvalt, behoudt de server niets en verstuurt de client opnieuw subscriptions/listen om de verbinding te herstellen.
Applicatiestatus tussen aanroepen wordt een expliciete handle. Wanneer een server daadwerkelijk iets moet onthouden tussen aanroepen door, is het antwoord van de specificatie een door de server gegenereerde identifier die als een gewoon tool-argument wordt meegegeven. Deze verschijnt in het tool-schema, kan worden gelogd en wordt nooit impliciet door de verbinding bepaald. Een server met echte gebruikersgegevens, zoals een zelfgehoste MCP-mailserver, gebruikt dit patroon in plaats van een sessie: de mailbox- of concept-identifier is een tool-argument, waardoor elke replica de volgende aanroep kan afhandelen. Veel tools hebben helemaal geen handle nodig: een zoektool ondersteund door uw eigen SearXNG-instantie ontvangt een query en geeft resultaten terug, zonder dat er iets hoeft te worden hervat voor de volgende aanroep en zonder dat het uitmaakt welke replica het antwoord gaf.
Deployment: reverse proxy, timeouts, health checks
Het MCP-eindpunt is één pad dat POST-verzoeken accepteert. Het meeste verkeer bestaat uit een kort verzoek en een JSON-respons, wat elke proxy probleemloos afhandelt. De uitzondering is de streaming-respons, waarbij de standaardinstellingen van een proxy vaak tegenwerken. Dit is het onderdeel dat verandert wanneer u overstapt van een lokale demo naar een MCP-server die op een VPS draait.
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off is van belang omdat nginx geproxiede responses standaard buffert, waardoor SSE-events worden vastgehouden totdat een buffer vol is of de respons eindigt. De specificatie vereist ook dat servers X-Accel-Buffering: no meesturen bij SSE-responses, en nginx respecteert die header; een correct geconfigureerde server geeft uw proxy dus zelf de juiste instructie. Stel de richtlijn desondanks in, omdat dit het deel is waar u controle over heeft.
proxy_read_timeout staat standaard op 60 seconden. Een subscriptions/listen-stream die langer dan dat stil blijft, wordt door nginx gesloten, niet door uw server. Hierdoor tonen uw logs een gezond proces, terwijl de client een verbroken stream rapporteert. Verhoog deze waarde alleen voor de MCP-locatie, niet voor de gehele server. Het wordt servers tevens aangeraden om tijdens rustige periodes een SSE-commentaarregel (een regel die begint met een dubbele punt) als keep-alive te sturen, wat voorkomt dat tussenliggende systemen de stream voortijdig beëindigen.
Caddy vereist minder configuratie. Het buffert standaard gedeeltelijk voor efficiëntie op het netwerk en flusht onmiddellijk wanneer de respons Content-Type: text/event-stream bevat, waardoor streaming zonder extra richtlijnen werkt.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Let op waar de health check naar verwijst. Richt een actieve controle niet op het MCP-eindpunt met GET, omdat een server die enkel deze revisie implementeert 405 Method Not Allowed antwoordt op GET en DELETE, terwijl de standaard health-methode van Caddy GET is. De proxy zou een perfect functionerende backend dan als onbereikbaar markeren. Serveer een eenvoudig pad zoals /healthz voor de proxy en controleer het protocol afzonderlijk met een POST-verzoek.
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'Een 200 met een supportedVersions-lijst betekent dat het proces actief is en het protocol spreekt. Een 404 met JSON-RPC-fout -32601 betekent dat het proces actief is, maar server/discover niet ondersteunt, wat elke 2026-07-28-server verplicht moet implementeren. Een 400 met -32022 betekent dat uw controleur vroeg om een versie die deze build niet ondersteunt; dit is precies wat u wilt detecteren na een upgrade van afhankelijkheden. De open-sourceversie van nginx heeft geen actieve health checks; gebruik daarom passieve max_fails en fail_timeout op de upstream en voer de protocolcontrole uit vanuit uw monitoring.
Een rolling restart kost u nu alleen de verzoeken die op dat moment in behandeling zijn. Drain het verkeer, laat openstaande POST-verzoeken afronden, start het nieuwe proces en laat clients de mislukte verzoeken opnieuw indienen. Het enige dat u nog verliest, is een openstaande subscriptions/listen-stream, omdat die stream een actieve verbinding is met één specifiek proces. Statelessness heeft sessie-affiniteit verwijderd, maar geen verbindingsaffiniteit voor een actieve stream, en geen enkele routeringsregel lost dat op. Een client kan het verschil zien: een stream die eindigt met het lege subscriptions/listen-resultaat is correct afgesloten, terwijl een stream die zonder dit resultaat eindigt, is verbroken; de client kan dit als reden zien om opnieuw verbinding te maken.
Caching wordt voor het eerst mogelijk. Resultaten van de list-methoden bevatten nu ttlMs en cacheScope, en cacheScope: "public" geeft gedeelde tussenliggende systemen aan dat ze de respons mogen cachen. Dit is alleen veilig omdat list-resultaten niet langer per verbinding variëren, wat een direct gevolg is van het verwijderen van sessies.
Waarom authenticatie verandert wanneer er geen sessie is
Met een sessie was het verleidelijk om eenmalig te authenticeren bij initialize en het sessie-ID vervolgens als bewijs voor alle daaropvolgende acties te beschouwen. Een sessie-ID dat op die manier wordt gebruikt, is een bearer-credential zonder doelgroep, zonder vervaldatum en zonder intrekkingsmogelijkheid, uitgegeven door uw eigen server. Het verwijderen van sessies elimineert die kortere weg en de vervanging daarvan is strikter.
Een beveiligde MCP-server fungeert als een OAuth 2.1 resource server. Elk HTTP-verzoek van de client moet Authorization: Bearer <access token> bevatten en de server valideert het token bij elk verzoek. Validatie omvat de doelgroep: de server moet bevestigen dat het token specifiek voor hem is uitgegeven, conform RFC 8707 (Resource Indicators for OAuth 2.0), en mag geen tokens accepteren of doorsturen die voor iets anders bedoeld zijn. Clients vragen de juiste doelgroep aan door de parameter resource mee te sturen met de canonieke URI van de server.
Discovery verloopt via een challenge. Wanneer een verzoek binnenkomt zonder bruikbaar token, antwoordt de server met 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"De client leest resource_metadata, haalt dat document op (RFC 9728, OAuth 2.0 Protected Resource Metadata, dat MCP-servers moeten implementeren), vindt de autorisatieserver en voert de flow uit. Een geldig token met onvoldoende rechten resulteert in 403 Forbidden met error="insufficient_scope" en de scopes die voor die operatie vereist zijn.
Dit heeft twee gevolgen voor de manier waarop u dit uitvoert. Tokenvalidatie vindt nu plaats bij elk verzoek in plaats van eenmaal per sessie, dus een netwerk-round-trip naar een introspectie-endpoint per aanroep zal zichtbaar zijn in uw latentie: geef de voorkeur aan tokens die u lokaal kunt verifiëren op basis van een handtekening, een doelgroep en een vervaldatum, of cache het validatieresultaat voor een kort venster, gekoppeld aan het token. En omdat er geen sessie is die een identiteit vasthoudt, moet de autorisatie bij elke aanroep vanuit het token worden berekend. Dat is eerlijker dan het sessiemodel was, en het sluit aan bij de bredere praktijk om credentials buiten het agent-proces te houden, wat wordt behandeld in geheimen buiten een AI-agent houden.
Wat is waar over deze revisie, en wat niet
Alles hierboven beschrijft revisie 2026-07-28. Het beschrijft niet MCP voor eeuwig, en het beschrijft niet de server die u vorig jaar heeft geïmplementeerd.
Clients en servers op 2025-11-25 en eerder gebruiken nog steeds het handshake-model. De specificatie noemt die revisies legacy, en noemt de revisies met per-request-metadata modern. Een server die alleen deze revisie ondersteunt en een oudere client ontmoet, moet 405 Method Not Allowed antwoorden op GET of DELETE op het MCP-eindpunt, elke Mcp-Session-Id-header negeren zonder er een aan te maken of te echoën, en Last-Event-ID negeren omdat streams niet hervatbaar zijn. Een server die beide tijdperken ondersteunt, kan beide op één eindpunt bedienen: een verzoek met moderne _meta wordt stateless afgehandeld, en een initialize-verzoek selecteert de oudere sessiesemantiek.
Controleer dus de revisiestring voordat u op deze informatie vertrouwt. Als uw SDK nog steeds initialize verstuurt, zijn sessies nog steeds reëel voor uw implementatie en blijven de hierboven genoemde sessiegerelateerde problemen uw verantwoordelijkheid. Hetzelfde geldt voor de clientzijde: een agentproces op uw eigen machine, zoals de configuratie in een coding agent draaien op een VPS, is alleen in deze zin stateless als de gebruikte bibliotheek een moderne revisie spreekt. Lees de versie die uw runtime onderhandelt, lees vervolgens de bijbehorende revisie van de specificatie, en beschouw deze pagina als een beschrijving van één specifieke revisie in plaats van het protocol in het algemeen.
FAQ
Betekent een stateless MCP-server dat ik niets kan opslaan?
Nee. Stateless beschrijft het protocol, niet uw applicatie. Databases, wachtrijen en caches werken precies zoals voorheen. Wat verandert, is dat status die meerdere aanroepen beslaat, moet worden verwezen via een expliciete identifier die de client bij elk verzoek meestuurt, zoals een door de server gegenereerde handle in een tool-argument. Wat u niet mag doen, is context afleiden uit de verbinding: de specificatie stelt dat een server niet mag vertrouwen op eerdere verzoeken over dezelfde verbinding om mogelijkheden, protocolversie of client-identiteit vast te stellen, omdat elk verzoek deze gegevens aanlevert in _meta.
Heb ik nog sticky sessions nodig op mijn load balancer?
Niet voor reguliere verzoeken. Onder revisie 2026-07-28 draagt elke POST zijn eigen protocolversie, mogelijkheden en inloggegevens, dus elke replica kan elk verzoek beantwoorden en round-robin is prima. Het enige langlevende onderdeel dat overblijft is de subscriptions/listen-responsstream, wat een enkele open verbinding is naar één proces. Deze eindigt wanneer dat proces eindigt, en de client stuurt opnieuw subscriptions/listen om deze te herstellen. Dat is verbindingslevensduur in plaats van sessie-affiniteit, en geen enkele routeringsregel voorkomt dit.
Wat is er gebeurd met Mcp-Session-Id en de HTTP GET-stream?
Beide zijn verwijderd in revisie 2026-07-28, onder SEP-2567 en SEP-2575. Een server die alleen deze revisie implementeert, moet 405 Method Not Allowed antwoorden op GET en DELETE op het MCP-eindpunt, en moet een Mcp-Session-Id-header negeren in plaats van deze terug te sturen. Door de server geïnitieerde wijzigingsmeldingen verlopen nu via de responsstream van een subscriptions/listen-verzoek in plaats van een zelfstandige GET-stream. Servers die oudere clients moeten blijven bedienen, implementeren het gedrag van de eerdere revisie naast deze.
Hoe voer ik een health check uit op een MCP-server zonder handshake?
Gebruik twee niveaus. Richt de actieve controle van de proxy op een standaard HTTP-pad dat uw applicatie serveert, omdat een GET naar het MCP-eindpunt correct 405 retourneert en een gezonde backend als down zou markeren. Controleer vervolgens het protocol zelf door server/discover te POSTen, wat elke 2026-07-28-server moet implementeren, en verifieer dat het antwoord HTTP 200 is en een protocolversie bevat die uw clients gebruiken. Een 404 met JSON-RPC-fout -32601 betekent dat het proces draait maar die methode niet serveert, en een 400 met -32022 betekent dat de versie waar u om vroeg niet wordt ondersteund door die build.