SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-28

Wat is een stateless MCP-server en wat is er veranderd?

De MCP-revisie van 28-07-2026 verwijdert de initialize handshake en sessies. Lees wat dit betekent voor uw load balancer, health checks, time-outs en authenticatie.

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 onderliggende HTTP-sessie zijn verwijderd. Alles hier gaat over de serverkant van die verbinding. Als de agent-kant nog nieuw voor u is, behandelt een stapsgewijs pad voor het leren van AI-agents de lus die beslist om een tool aan te roepen voordat al deze HTTP-details relevant worden.

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 clients te verbreken, en kan als vier identieke processen in plaats van één draaien. Een sessie-georiënteerde server kan dit alles niet zonder extra infrastructuur.

Het Model Context Protocol is een stateless protocol: alle informatie die nodig is om een verzoek te verwerken, is in het verzoek zelf vervat. Een server verwerkt elk verzoek onafhankelijk; er mag geen status worden afgeleid uit eerdere verzoeken, zelfs niet als deze op dezelfde verbinding of stream plaatsvinden.

Stateless betekent niet dat uw server niets opslaat. Uw database, queue en cache zijn nog steeds aanwezig. Het betekent dat het protocol geen status op de verbinding bijhoudt. De server mag een verbinding, proces of open socket daarom niet beschouwen als vervanging voor "deze client, midden in een gesprek". Dit onderscheid is het duidelijkst bij een app die de eigen gegevens al beheert: de alleen-lezen MCP-server van openGym beantwoordt vragen over trainingsgeschiedenis die in de eigen database van de app staat. Niets aan die opslag is afhankelijk van de verbinding waarop een bepaald verzoek binnenkomt.

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 de notifications/initialized notificatie. Er is geen sprake meer van een handshake (SEP-2575).
  • De Mcp-Session-Id header en sessiebeëindiging met HTTP DELETE (SEP-2567).
  • De zelfstandige HTTP GET stream waarop servers notificaties pushten. Deze is vervangen door subscriptions/listen, een standaard POST-verzoek waarvan het antwoord een langdurige stream is.
  • Hervatbaarheid van SSE (server-sent events) streams. De Last-Event-ID header en per-event ID's zijn verwijderd; een onderbroken stream verliest het lopende verzoek en de client moet dit opnieuw indienen als een nieuw verzoek met een nieuw verzoek-ID.
  • ping, logging/setLevel en notifications/roots/list_changed. Het logniveau is nu een veld per verzoek, io.modelcontextprotocol/logLevel in _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 het meest 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 nieuwe InitializeRequest. Elke uitrol werd 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. DELETE was optioneel, en clients die de verbinding sloten zonder dit te sturen, lieten vermeldingen achter.
  • Lijstresultaten konden per verbinding verschillen, waardoor caching vóór 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 enige configuratie 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 body van het verzoek 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 dat een verplicht veld mist, is onjuist geformatteerd; 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 zijn. 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 somt op 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 aanpassen. 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 lijstweergave te hervatten. Codeer daarom de positie in de cursor en onderteken deze, of sla de positie op in een gedeelde opslag voor alle replica's. Een ongeldige cursor moet resulteren in -32602. Onderteken de cursor, aangezien 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 waarin de gewenste typen worden benoemd: toolsListChanged, promptsListChanged, resourcesListChanged en resourceSubscriptions. De server antwoordt met notifications/subscriptions/acknowledged en houdt die responsstroom open. Als de stroom wordt verbroken, 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 bepaald door de verbinding. Een server met echte gebruikersgegevens, zoals een zelfgehoste MCP-mailserver, gebruikt dit patroon in plaats van een sessie: de identifier voor de mailbox of het concept is een tool-argument, zodat 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 voor de volgende aanroep iets hoeft te worden hervat 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-antwoord, wat door elke proxy wordt afgehandeld. De uitzondering is het streaming-antwoord, waarbij standaardinstellingen van de proxy tegen u kunnen werken. Dit is het onderdeel dat verandert wanneer u overstapt van een demo op een laptop 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 antwoorden standaard buffert. Hierdoor worden SSE-events vastgehouden totdat een buffer vol is of het antwoord eindigt. De specificatie vereist ook dat servers X-Accel-Buffering: no meesturen bij SSE-antwoorden, en nginx respecteert die header. Een correcte server geeft uw proxy dus uit zichzelf de juiste instructie. Stel de directive desondanks in, omdat u daarmee het deel beheert waar u zelf 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. Uw logs tonen dan een gezond proces, terwijl uw client een verbroken stream rapporteert. Verhoog deze waarde alleen voor de MCP-locatie, niet voor de gehele server. Servers worden ook aangemoedigd om tijdens rustige periodes een SSE-commentaarregel (een regel die begint met een dubbele punt) als keep-alive te sturen. Dit 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 het antwoord Content-Type: text/event-stream bevat, waardoor streaming zonder extra directives 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 check niet op het MCP-eindpunt met GET, omdat een server die alleen 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.

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 moet implementeren. Een 400 met -32022 betekent dat uw checker vroeg om een versie die deze build niet ondersteunt; dit is precies wat u wilt detecteren na een upgrade van dependencies. 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 lopende verzoeken. Drain het verkeer, laat openstaande POST-verzoeken afronden, start het nieuwe proces en clients sturen verzoeken die mislukten opnieuw in. Het enige dat u nog steeds verliest, is een open subscriptions/listen-stream, omdat die stream een actieve verbinding is met één specifiek proces. Statelessness heeft sessie-affiniteit verwijderd, maar geen verbindingsaffiniteit voor een stream die op dat moment open is. 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" vertelt gedeelde tussenliggende systemen dat zij het antwoord mogen cachen. Dit is alleen veilig omdat list-resultaten niet langer variëren per verbinding, wat een direct gevolg is van het verwijderen van sessies.

Waarom authenticatie verandert wanneer er geen sessie is

Bij een sessie was het verleidelijk om eenmalig te authenticeren op 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 verloopdatum en zonder mogelijkheid tot intrekking, uitgegeven door uw eigen server. Het verwijderen van sessies elimineert deze kortere weg en de vervanging ervan 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. De 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 doorgeven 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 er 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, wat 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 het 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 latency: geef de voorkeur aan tokens die u lokaal kunt verifiëren op basis van een handtekening, een doelgroep en een verloopdatum, of cache het validatieresultaat voor een kort venster, geïndexeerd op 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. Scopes beperken alleen wat een token mag doen zodra het verzoek u bereikt; op de machine waarop de agent draait, bepalen plugins die tool-toestemmingsregels en budgetlimieten toevoegen welke aanroepen überhaupt worden gedaan.

Wat is waar over deze revisie, en wat niet

Alles hierboven beschrijft revisie 2026-07-28. Het beschrijft niet MCP voor altijd, en het beschrijft niet de server die u vorig jaar heeft ingezet.

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 deze 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 sessiegerelateerde problemen hierboven uw verantwoordelijkheid. Hetzelfde geldt aan de clientzijde: een agentproces op uw eigen machine, zoals de configuratie in een coding agent draaien op een VPS, is alleen stateless in deze zin als de bibliotheek die het gebruikt 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 door een expliciete identificatie 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 steeds sticky sessions nodig op mijn load balancer?

Niet voor normale 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-responsstroom, wat een enkele open verbinding is naar één enkel proces. Deze eindigt wanneer dat proces eindigt, en de client verstuurt 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-stroom?

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 responsstroom van een subscriptions/listen-verzoek in plaats van via een zelfstandige GET-stroom. 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 vermeldt 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.