Server MCP stateless: cosa è cambiato davvero
La revisione MCP 2026-07-28 elimina sessioni e handshake initialize: scopri cosa cambia per reverse proxy, health check, timeout e autenticazione.
Che cos’è un server MCP stateless
Un server MCP stateless non conserva lo stato del singolo client tra una richiesta e l’altra. Ogni richiesta contiene la versione del protocollo, le funzionalità del client e le credenziali necessarie al server per rispondere. Per questo, qualsiasi processo su qualsiasi macchina può gestire qualsiasi richiesta. MCP (Model Context Protocol, il formato wire che gli agenti usano per raggiungere gli strumenti) ha reso questo comportamento obbligatorio nella revisione 2026-07-28, che ha rimosso l’handshake initialize e la sessione HTTP sottostante.
Questo è l’intero vantaggio operativo. Un server che non conserva dati per client può essere eseguito dietro un normale load balancer senza affinità di sessione, può essere riavviato durante un deploy senza interrompere i client e può essere eseguito come quattro processi identici invece di uno. Un server orientato alle sessioni non offre nessuno di questi vantaggi senza componenti aggiuntivi.
Il Model Context Protocol è un protocollo stateless: tutte le informazioni necessarie per elaborare una richiesta sono contenute nella richiesta stessa. Il server elabora ogni richiesta in modo indipendente; non deve dedurre alcuno stato dalle richieste precedenti, nemmeno quando usano la stessa connessione o lo stesso stream.
Stateless non significa che il server non memorizzi nulla. Il database, la coda e la cache continuano a essere presenti. Significa che il protocollo non trasporta stato sulla connessione. Il server non deve quindi trattare una connessione, un processo o un socket aperto come sostituti di “questo client, nel corso della conversazione”.
Cosa ha rimosso la revisione del 2026-07-28
2026-07-28 è la revisione corrente della specifica ad agosto 2026. Rispetto a 2025-11-25, rimuove cinque elementi che esistevano per supportare le sessioni.
- La richiesta
initializee la notificanotifications/initialized. Non esiste alcun handshake (SEP-2575). - L'header
Mcp-Session-Ide la terminazione della sessione con HTTPDELETE(SEP-2567). - Lo stream HTTP
GETautonomo su cui i server inviavano le notifiche. È sostituito dasubscriptions/listen, una normale richiesta POST la cui risposta è uno stream di lunga durata. - La possibilità di riprendere gli stream SSE (server-sent events). L'header
Last-Event-IDe gli ID dei singoli eventi sono stati rimossi. Se lo stream si interrompe, la richiesta in corso viene persa e il client deve inviarla di nuovo come nuova richiesta con un nuovo request ID. ping,logging/setLevelenotifications/roots/list_changed. Il livello di log è ora un campo per richiesta,io.modelcontextprotocol/logLevelin_meta.
È stato aggiunto un metodo, che ogni server deve implementare. server/discover restituisce in una sola chiamata le versioni del protocollo supportate dal server, le funzionalità disponibili e la relativa identità. È l'elemento più simile a un handshake tra quelli rimasti, ma per i client la sua chiamata è facoltativa.
Perché il trasporto con sessione era difficile da gestire in produzione
In 2025-11-25 e nelle versioni precedenti, un server poteva generare un ID di sessione durante l’inizializzazione e restituirlo nell’header Mcp-Session-Id sulla InitializeResult. Il client doveva quindi inviare quell’header in ogni richiesta successiva. La versione del protocollo negoziata e le capacità del client restavano nella memoria del server, associate a quell’ID. Ognuna di queste scelte comportava un costo operativo.
- Un riavvio eliminava la tabella delle sessioni. La specifica richiedeva che il server rispondesse con
404 Not Founda qualsiasi richiesta contenente un ID di sessione non più valido e che il client ricominciasse con un nuovoInitializeRequest. Ogni distribuzione diventava un evento di riconnessione per tutti i client connessi. - Una seconda replica non conosceva le sessioni della prima. Il ridimensionamento orizzontale richiedeva l’instradamento persistente sul bilanciatore oppure un archivio condiviso delle sessioni, letto da ogni replica a ogni richiesta.
- La tabella delle sessioni occupava memoria e cresceva con i client inattivi.
DELETEera facoltativo e i client che chiudevano la connessione senza inviarlo lasciavano le relative voci nella tabella. - I risultati delle liste potevano variare in base alla connessione, quindi il caching davanti al server non era sicuro.
La rimozione delle sessioni elimina tutti e quattro i problemi contemporaneamente. È questo il cambiamento da comprendere prima di modificare qualsiasi configurazione.
Che cosa contiene ora ogni richiesta
Ogni richiesta POST all’endpoint MCP è autonoma. La versione del protocollo e le funzionalità del client vengono trasmesse nel corpo della richiesta, nel campo _meta. Alcuni campi vengono replicati negli header HTTP, così un componente intermedio può usarli per il routing senza analizzare il JSON.
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 e io.modelcontextprotocol/clientCapabilities sono obbligatori in ogni richiesta. clientInfo non è obbligatorio, ma i client dovrebbero inviarlo. Una richiesta priva di un campo obbligatorio è malformata. Il server deve quindi rifiutarla con l’errore JSON-RPC -32602 e lo stato HTTP 400 Bad Request.
L’header Mcp-Method è obbligatorio in ogni richiesta. Mcp-Name è obbligatorio su tools/call, resources/read e prompts/get. Il valore dell’header deve corrispondere al corpo della richiesta. Se elabora il corpo, il server deve rifiutare una mancata corrispondenza con 400 Bad Request e i codici di errore -32020, HeaderMismatch. Questa regola è necessaria perché un load balancer che esegue il routing in base all’header e un server che esegue l’operazione in base al corpo usano due fonti di verità diverse. Se usi questi header per il routing o il rate limiting, controlla prima MCP-Protocol-Version. Le revisioni precedenti non verificavano la corrispondenza tra header e corpo. In quelle versioni, quindi, il valore dell’header non è affidabile.
Un’incompatibilità di versione è ora un normale errore relativo alla singola richiesta, non più un handshake fallito. Un server che non implementa la versione richiesta risponde con 400 Bad Request e gli errori -32022, UnsupportedProtocolVersion. Nell’elemento data.supported indica le versioni supportate. Il client ne seleziona una dall’elenco e ripete la richiesta.
Dove finisce lo stato: token, cursori e sottoscrizioni
Lo stato non è scomparso. È stato spostato in elementi che puoi visualizzare e registrare nei log.
Le credenziali vengono incluse in ogni richiesta. Non esiste una sessione a cui associare un'identità, quindi il token di accesso viene inviato in ogni chiamata HTTP e convalidato ogni volta. I dettagli sono disponibili nella sezione sull'autenticazione riportata di seguito.
I cursori devono contenere la propria posizione. La paginazione su tools/list, resources/list, prompts/list e resources/templates/list usa una stringa cursore opaca, che i client non devono analizzare o modificare. Su un server a processo singolo era comune mantenere l'offset in memoria, associato alla sessione. In assenza di sessione, il cursore deve essere sufficiente per consentire a qualsiasi replica di riprendere l'elenco. Codifica quindi la posizione nel cursore e firmalo, oppure salvala in uno storage condiviso da tutte le repliche. Un cursore non valido deve restituire -32602. Firmalo perché un cursore opaco è comunque un input fornito dal client, che il codice decodifica e considera attendibile.
Le sottoscrizioni appartengono a una richiesta, non a una connessione. Un client che vuole ricevere notifiche sulle modifiche invia subscriptions/listen con un filtro che specifica i tipi richiesti: toolsListChanged, promptsListChanged, resourcesListChanged e resourceSubscriptions. Il server risponde con notifications/subscriptions/acknowledged e mantiene aperto il relativo stream di risposta. Se lo stream si interrompe, il server non conserva nulla e il client invia nuovamente subscriptions/listen per riattivarlo.
Lo stato applicativo tra chiamate diventa un handle esplicito. Quando un server deve realmente ricordare qualcosa tra una chiamata e l'altra, la specifica prevede un identificatore generato dal server e restituito come un normale argomento dello strumento. L'identificatore compare nello schema dello strumento, può essere registrato nei log e non è mai implicito nella connessione. Un server con dati effettivi per utente, ad esempio un server email MCP self-hosted, usa questo modello invece di una sessione: l'identificatore della casella o della bozza è un argomento dello strumento, quindi qualsiasi replica può gestire la chiamata successiva. Molti strumenti non hanno bisogno di alcun handle: uno strumento di ricerca basato sulla tua istanza SearXNG riceve una query e restituisce i risultati, senza nulla da conservare per la chiamata successiva e senza motivo di considerare quale replica abbia risposto.
Distribuzione: reverse proxy, timeout e controlli di integrità
L'endpoint MCP è un percorso che accetta POST. La maggior parte del traffico consiste in una richiesta breve e in una risposta JSON, che qualsiasi proxy gestisce. L'eccezione è la risposta in streaming, per la quale le impostazioni predefinite del proxy diventano un problema. È questo l'aspetto che cambia quando si passa da una demo su laptop a un server MCP in esecuzione su un VPS.
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 è importante perché nginx memorizza nel buffer le risposte inoltrate per impostazione predefinita. In questo modo gli eventi SSE restano nel buffer finché questo non si riempie o la risposta non termina. La specifica richiede inoltre che i server inviino X-Accel-Buffering: no nelle risposte SSE, e nginx rispetta questo header. Un server corretto comunica quindi autonomamente al proxy il comportamento da adottare. Impostare anche la direttiva è comunque necessario, perché questa è la parte sotto il tuo controllo.
proxy_read_timeout è impostato su 60 secondi per impostazione predefinita. Un flusso subscriptions/listen che resta inattivo più a lungo viene chiuso da nginx, non dal server. Di conseguenza, i log mostrano un processo sano mentre il client rileva l'interruzione del flusso. Aumenta il valore solo nella location MCP, non sull'intero server. I server dovrebbero inoltre inviare una riga di commento SSE, cioè una riga che inizia con due punti, come keep-alive durante i periodi di inattività. In questo modo gli intermediari non fanno scadere il flusso.
Con Caddy serve meno configurazione. Per impostazione predefinita, Caddy esegue il buffering parziale per migliorare l'efficienza sulla rete e scarica immediatamente la risposta quando questa contiene Content-Type: text/event-stream. Lo streaming funziona quindi senza direttive aggiuntive.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Osserva a quale percorso punta questo controllo di integrità. Non eseguire un controllo attivo sull'endpoint MCP con GET. Un server che implementa solo questa revisione risponde 405 Method Not Allowed a GET e DELETE, mentre il metodo predefinito dei controlli di integrità di Caddy è GET. Il proxy contrassegnerebbe quindi come inattivo un backend perfettamente sano. Esponi al proxy un percorso semplice come /healthz e verifica separatamente il protocollo con una richiesta 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":{}}}}'Un 200 che contiene un elenco supportedVersions indica che il processo è attivo e comunica usando il protocollo. Un 404 con errore JSON-RPC -32601 indica che il processo è attivo, ma non fornisce server/discover, che ogni server 2026-07-28 deve implementare. Un 400 con -32022 indica che il verificatore ha richiesto una versione non supportata da questa build. È esattamente il caso da rilevare dopo l'aggiornamento di una dipendenza. nginx open source non dispone di controlli di integrità attivi. Usa quindi max_fails e fail_timeout passivi sull'upstream ed esegui il controllo del protocollo dal sistema di monitoraggio.
Un rolling restart ora comporta la perdita delle richieste in corso e di nient'altro. Esegui il drain, lascia terminare le richieste POST aperte, avvia il nuovo processo e lascia che i client ripetano le richieste fallite. L'unico elemento che viene comunque perso è ogni flusso subscriptions/listen aperto, perché quel flusso è una connessione attiva verso uno specifico processo. L'assenza di stato elimina la necessità della session affinity. Non elimina però la connection affinity per un flusso attualmente aperto, e nessuna regola di routing può risolvere questo problema. Un client può distinguere i due casi: un flusso che termina con il risultato subscriptions/listen vuoto è stato chiuso correttamente; un flusso che termina senza quel risultato è stato interrotto e il client può considerarlo un motivo per riconnettersi.
Ora il caching diventa possibile per la prima volta. I risultati dei metodi list contengono ttlMs e cacheScope, mentre cacheScope: "public" comunica agli intermediari condivisi che possono memorizzare nella cache la risposta. Questo è sicuro solo perché i risultati delle liste non variano più in base alla connessione, una conseguenza diretta della rimozione delle sessioni.
Perché l’autenticazione cambia quando non esiste una sessione
Con una sessione, era naturale autenticarsi una volta su initialize e poi considerare l’ID di sessione una prova valida per tutte le operazioni successive. Usato in questo modo, un ID di sessione è una credenziale bearer senza destinatario, scadenza o meccanismo di revoca, generata dal proprio server. Eliminare le sessioni rimuove questa scorciatoia e richiede un sostituto più rigoroso.
Un server MCP protetto agisce come resource server OAuth 2.1. Ogni richiesta HTTP del client deve contenere Authorization: Bearer <access token> e il server valida il token a ogni richiesta. La validazione include l’audience: il server deve confermare che il token sia stato emesso specificamente per quel server, in conformità a RFC 8707 (Resource Indicators for OAuth 2.0), e non deve accettare o inoltrare token destinati ad altri servizi. I client richiedono l’audience corretta inviando il parametro resource con l’URI canonico del server.
Il meccanismo di discovery si avvia con una challenge. Quando arriva una richiesta senza un token utilizzabile, il server risponde con 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"Il client legge resource_metadata, scarica quel documento (RFC 9728, OAuth 2.0 Protected Resource Metadata, che i server MCP devono implementare), individua l’authorization server ed esegue il flusso. Un token valido ma con autorizzazioni insufficienti produce 403 Forbidden con error="insufficient_scope" e gli scope richiesti per quell’operazione.
Questo comporta due conseguenze operative. La validazione del token avviene a ogni richiesta invece che una sola volta per sessione. Di conseguenza, un round trip di rete verso un endpoint di introspection per ogni chiamata incide sulla latenza. È preferibile usare token verificabili localmente tramite firma, audience e scadenza oppure memorizzare nella cache il risultato della validazione per un breve intervallo, indicizzato dal token. Inoltre, poiché non esiste una sessione che mantenga l’identità, l’autorizzazione deve essere calcolata dal token a ogni chiamata. Questo modello è più trasparente rispetto al modello basato sulle sessioni e si integra con la prassi più ampia di mantenere le credenziali fuori dal processo dell’agent, descritta in mantenere i secret fuori da un agent AI.
Che cosa vale per questa revisione e che cosa invece non vale
Tutto quanto descritto sopra si riferisce alla revisione 2026-07-28. Non descrive MCP in modo permanente e non descrive il server che hai distribuito lo scorso anno.
I client e i server che usano 2025-11-25 e le revisioni precedenti comunicano ancora secondo il modello di handshake. La specifica definisce legacy queste revisioni e definisce moderne quelle che usano metadati per richiesta. Un server che supporta soltanto questa revisione, quando comunica con un client meno recente, dovrebbe rispondere con 405 Method Not Allowed a GET o DELETE sull'endpoint MCP, ignorare qualsiasi header Mcp-Session-Id senza generarne o ripeterne uno e ignorare Last-Event-ID, perché gli stream non possono essere ripresi. Un server compatibile con entrambe le epoche può gestire entrambi i modelli sullo stesso endpoint: una richiesta che contiene _meta viene gestita senza stato, mentre una richiesta initialize seleziona la semantica di sessione precedente.
Controlla quindi la stringa della revisione prima di dare per scontato tutto questo. Se il tuo SDK invia ancora initialize, le sessioni sono ancora effettive per la tua distribuzione e devi ancora gestire i problemi legati alle sessioni descritti sopra. Lo stesso vale lato client: un processo agent sul tuo stesso server, come nella configurazione descritta in eseguire un coding agent su un VPS, è senza stato in questo senso soltanto se la libreria che utilizza comunica usando una revisione moderna. Leggi la versione negoziata dal runtime, quindi consulta la revisione corrispondente della specifica e considera questa pagina come la descrizione di una specifica revisione denominata, non del protocollo in generale.
FAQ
Un server MCP stateless significa che non posso memorizzare nulla?
No. Stateless descrive il protocollo, non l'applicazione. Database, code e cache continuano a funzionare esattamente come prima. Cambia il modo in cui si referenzia lo stato che coinvolge più chiamate: è necessario usare un identificatore esplicito che il client invia in ogni richiesta, ad esempio un handle generato dal server come argomento di uno strumento. Non è invece possibile dedurre il contesto dalla connessione: la specifica stabilisce che un server non deve basarsi sulle richieste precedenti effettuate sulla stessa connessione per determinare capacità, versione del protocollo o identità del client, perché ogni richiesta fornisce questi dati in _meta.
Devo ancora usare sessioni persistenti sul load balancer?
Non per le richieste ordinarie. Nella revisione 2026-07-28 ogni POST contiene la propria versione del protocollo, le capacità e le credenziali. Qualsiasi replica può quindi rispondere a qualsiasi richiesta e il round-robin è sufficiente. L'unico elemento di lunga durata rimasto è il flusso di risposta subscriptions/listen, ovvero una singola connessione aperta verso un singolo processo. Il flusso termina quando termina quel processo e il client invia nuovamente subscriptions/listen per ristabilirlo. Si tratta della durata della connessione, non dell'affinità di sessione, quindi nessuna regola di routing la impedisce.
Che cosa è successo a Mcp-Session-Id e al flusso HTTP GET?
Entrambi sono stati rimossi nella revisione 2026-07-28, in base a SEP-2567 e SEP-2575. Un server che implementa soltanto questa revisione dovrebbe rispondere con 405 Method Not Allowed a GET e DELETE sull'endpoint MCP e dovrebbe ignorare un header Mcp-Session-Id invece di restituirlo. Le notifiche di modifica avviate dal server ora viaggiano nel flusso di risposta di una richiesta subscriptions/listen, anziché in un flusso GET autonomo. I server che devono continuare a servire client meno recenti implementano il comportamento della revisione precedente insieme a quello della presente revisione.
Come posso eseguire un controllo di integrità su un server MCP senza handshake?
Usa due livelli. Configura il controllo attivo del proxy su un percorso HTTP semplice esposto dall'applicazione, perché un GET verso l'endpoint MCP restituisce correttamente 405 e farebbe considerare non disponibile un backend integro. Controlla quindi direttamente il protocollo inviando una POST a server/discover, che ogni server 2026-07-28 deve implementare, e verifica che la risposta sia HTTP 200 e che riporti una versione del protocollo usata dai client. Un 404 con errore JSON-RPC -32601 indica che il processo è in esecuzione, ma non gestisce quel metodo. Un 400 con -32022 indica che la versione richiesta non è supportata da quella build.