SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-28

Server MCP stateless: cosa è cambiato davvero

La revisione MCP 2026-07-28 ha rimosso sessioni e handshake initialize: scopri gli effetti su 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 capacità del client e le credenziali necessarie al server per rispondere. Di conseguenza, qualsiasi processo su qualsiasi macchina può gestire qualsiasi richiesta. MCP (Model Context Protocol, il formato wire usato dagli agenti per raggiungere gli strumenti) ha reso questa regola obbligatoria nella revisione 2026-07-28, che ha rimosso l’handshake initialize e la sessione HTTP sottostante. Questa sezione riguarda esclusivamente il lato server di tale protocollo. Se il lato agente è ancora poco chiaro, un percorso graduale per imparare a usare gli agenti AI descrive il ciclo decisionale che porta a chiamare uno strumento, prima di entrare nei dettagli HTTP.

Questo è il punto operativo principale. Un server che non conserva informazioni per client può essere collocato 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 che come uno solo. 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 appartengono alla stessa connessione o allo stesso stream.

Stateless non significa che il server non memorizzi nulla. Il database, la coda e la cache sono comunque presenti. Significa che il protocollo non conserva stato sulla connessione. Il server non deve quindi trattare una connessione, un processo o un socket aperto come sostituto di «questo client, nel mezzo di una conversazione». La distinzione è più evidente in un'applicazione che gestisce già i propri dati: server MCP in sola lettura di openGym risponde a domande sulla cronologia degli allenamenti memorizzata nel database dell'applicazione, senza che alcun dato dipenda dalla connessione su cui è arrivata una determinata richiesta.

Elementi rimossi nella revisione 2026-07-28

2026-07-28 è la revisione corrente della specifica ad agosto 2026. Rispetto a 2025-11-25, rimuove cinque elementi introdotti per supportare le sessioni.

  • La richiesta initialize e la notifica notifications/initialized. Non esiste alcun handshake (SEP-2575).
  • L’header Mcp-Session-Id e la terminazione della sessione con HTTP DELETE (SEP-2567).
  • Il flusso HTTP GET autonomo su cui i server inviavano le notifiche. È sostituito da subscriptions/listen, una normale richiesta POST la cui risposta è un flusso di lunga durata.
  • La possibilità di riprendere un flusso SSE (server-sent events). L’header Last-Event-ID e gli ID dei singoli eventi sono stati rimossi; se il flusso si interrompe, la richiesta in corso viene persa e il client deve reinviarla come nuova richiesta con un nuovo ID di richiesta.
  • ping, logging/setLevel e notifications/roots/list_changed. Il livello di log è ora un campo per richiesta, io.modelcontextprotocol/logLevel in _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 capacità e l’identità. È l’elemento più simile a un handshake tra quelli rimasti; per i client la sua chiamata è facoltativa.

Perché il trasporto basato su 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 funzionalità del client venivano conservate nella memoria del server e associate a quell'ID. Ognuna di queste scelte comportava un costo operativo.

  • Un riavvio eliminava la tabella delle sessioni. La specifica imponeva al server di rispondere con 404 Not Found a ogni richiesta contenente un ID di sessione non più valido e imponeva al client di ricominciare con un nuovo InitializeRequest. Ogni deploy diventava un evento di riconnessione per tutti i client connessi.
  • Una seconda replica non conosceva le sessioni della prima replica. Il dimensionamento orizzontale richiedeva un routing persistente sul load balancer oppure un archivio condiviso delle sessioni, letto da ogni replica a ogni richiesta.
  • La tabella delle sessioni occupava memoria e cresceva insieme al numero di client inattivi. DELETE era facoltativo e i client che si chiudevano senza inviarlo lasciavano le relative voci nella tabella.
  • I risultati delle operazioni di elenco potevano variare da una connessione all'altra, quindi non era sicuro usare una cache davanti al server.

La rimozione delle sessioni elimina tutti e quattro i problemi contemporaneamente. È questo il cambiamento da comprendere prima di modificare qualsiasi configurazione.

Cosa contiene oggi 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, e alcuni campi vengono riportati anche negli header HTTP, così un sistema intermedio può eseguire 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, anche se 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 il server elabora il corpo, 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 la richiesta 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, quindi in quelle versioni il valore dell'header non è attendibile.

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, l'errore -32022, UnsupportedProtocolVersion, e indica in data.supported le versioni supportate. Il client ne seleziona una dall'elenco e ritenta la richiesta.

Dove è finito lo stato: token, cursori, sottoscrizioni

Lo stato non è scomparso. È stato spostato in punti che è possibile 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 accompagna ogni chiamata HTTP e viene convalidato ogni volta. I dettagli sono riportati nella sezione sull'autenticazione qui sotto.

I cursori devono contenere la propria posizione. La paginazione su tools/list, resources/list, prompts/list e resources/templates/list usa una stringa di cursore opaca, che i client non devono analizzare né modificare. Su un server a processo singolo era comune mantenere l'offset in memoria, associandolo alla sessione. In assenza di sessione, il cursore deve contenere informazioni sufficienti per consentire a qualsiasi replica di riprendere l'elenco. È quindi necessario codificare la posizione nel cursore e firmarlo, oppure conservarla in uno storage condiviso da tutte le repliche. Un cursore non valido deve restituire -32602. È necessario firmarlo 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 ripristinarlo.

Lo stato applicativo tra chiamate diventa un handle esplicito. Quando un server deve effettivamente ricordare qualcosa tra una chiamata e l'altra, la specifica prevede un identificatore generato dal server e restituito come normale argomento dello strumento. L'identificatore compare nello schema dello strumento, può essere registrato nei log e non è mai implicito nella connessione. Un server che gestisce dati realmente associati ai singoli utenti, come un server email MCP self-hosted, usa questo modello invece di una sessione: l'identificatore della mailbox o della bozza è un argomento dello strumento, quindi qualsiasi replica può gestire la chiamata successiva. Molti strumenti non richiedono alcun handle: uno strumento di ricerca basato sulla propria istanza SearXNG riceve una query e restituisce i risultati, senza dover conservare informazioni per la chiamata successiva e senza alcun motivo per sapere quale replica ha risposto.

Deployment: 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 sono sfavorevoli. Questa è la differenza principale 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 per impostazione predefinita nel buffer le risposte inoltrate dal proxy. Gli eventi SSE restano quindi bloccati 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. nginx rispetta questo header, quindi un server conforme indica autonomamente al proxy il comportamento corretto. Imposta comunque anche la direttiva, perché questa è la parte sotto il tuo controllo.

proxy_read_timeout ha un valore predefinito di 60 secondi. Un flusso subscriptions/listen che rimane inattivo più a lungo viene chiuso da nginx, non dal server. I log mostrano quindi un processo funzionante, mentre il client vede il flusso interrotto. Aumenta il valore solo nella location MCP, non sull'intero server. Durante i periodi di inattività, i server possono inoltre inviare una riga di commento SSE, cioè una riga che inizia con i due punti, come keep-alive. In questo modo gli intermediari non interrompono il flusso per timeout.

Con Caddy è necessaria una configurazione minore. Per impostazione predefinita, Caddy esegue il buffering parziale per ottimizzare l'efficienza sul collegamento e svuota immediatamente il buffer quando la risposta 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
	}
}

Verifica l'obiettivo di questo controllo di integrità. Non indirizzare un controllo attivo all'endpoint MCP usando GET. Un server che implementa solo questa revisione risponde 405 Method Not Allowed a GET e DELETE, mentre il metodo predefinito per i controlli di integrità di Caddy è GET. Il proxy contrassegnerebbe quindi come non disponibile un backend perfettamente funzionante. Esponi al proxy un percorso semplice, ad esempio /healthz, e verifica il protocollo separatamente 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 controllo ha richiesto una versione non supportata da questa build. È esattamente il problema che vuoi rilevare dopo un aggiornamento di una dipendenza. La versione open source di nginx non dispone di controlli attivi di integrità. Usa quindi max_fails e fail_timeout passivi sull'upstream ed esegui il controllo del protocollo dal sistema di monitoraggio.

Un riavvio progressivo comporta ora la perdita delle sole richieste in corso. Esegui il drain, lascia terminare le richieste POST aperte, avvia il nuovo processo e lascia che i client ripetano le richieste non riuscite. L'unico elemento che viene comunque interrotto è un flusso subscriptions/listen aperto, perché il flusso è una connessione attiva verso uno specifico processo. L'assenza di stato ha eliminato l'affinità di sessione. Non ha eliminato l'affinità della connessione per un flusso attualmente aperto e nessuna regola di routing può risolvere questo problema. Il client può distinguere i due casi: un flusso che termina con il risultato subscriptions/listen vuoto è stato chiuso correttamente; un flusso che termina senza questo 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 di elenco contengono ora ttlMs e cacheScope, mentre cacheScope: "public" indica agli intermediari condivisi che possono memorizzare nella cache la risposta. Questo è sicuro solo perché i risultati degli elenchi 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 come prova per tutte le operazioni successive. Usato in questo modo, l’ID di sessione è una credenziale bearer senza audience, scadenza o meccanismo di revoca, generata dal proprio server. Eliminare le sessioni rimuove questa scorciatoia e impone un modello 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 verificare che il token sia stato emesso appositamente per quel server, secondo 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 processo di discovery inizia 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, recupera il documento indicato (RFC 9728, OAuth 2.0 Protected Resource Metadata, che i server MCP devono implementare), individua il authorization server ed esegue il flusso. Un token valido ma con autorizzazioni insufficienti riceve 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 anziché una sola volta per sessione, quindi un round trip di rete verso un endpoint di introspection per ogni chiamata incide sulla latenza: sono preferibili token verificabili localmente tramite firma, audience e scadenza, oppure è possibile memorizzare nella cache il risultato della validazione per un intervallo breve, usando il token come chiave. Inoltre, poiché non esiste una sessione che conservi l’identità, l’autorizzazione deve essere calcolata dal token a ogni chiamata. Questo modello è più trasparente del precedente basato sulle sessioni e si integra con la pratica più ampia di mantenere le credenziali fuori dal processo dell’agente, descritta in mantenere i secret fuori da un agente AI. Gli scope limitano soltanto ciò che un token può fare una volta che la richiesta raggiunge il server; sulla macchina in cui viene eseguito l’agente, i plugin dell’harness che aggiungono regole per i permessi degli strumenti e limiti di budget determinano quali chiamate vengono effettuate.

Cosa è vero per questa revisione e cosa non lo è

Tutto quanto descritto sopra riguarda la revisione 2026-07-28. Non descrive MCP per sempre e non descrive il server che avete 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 le revisioni con metadati per richiesta. Un server che supporta soltanto questa revisione, quando comunica con un client meno recente, dovrebbe rispondere 405 Method Not Allowed a GET o DELETE sull'endpoint MCP, ignorare qualsiasi header Mcp-Session-Id senza generarne né rifletterne uno e ignorare Last-Event-ID, perché gli stream non sono ripristinabili. Un server compatibile con entrambe le generazioni può gestirle sullo stesso endpoint: una richiesta che contiene _meta viene gestita senza stato, mentre una richiesta initialize seleziona la semantica delle sessioni meno recente.

Controllate quindi la stringa della revisione prima di considerare valide queste informazioni. Se il vostro SDK invia ancora initialize, le sessioni sono ancora effettive nella vostra distribuzione e dovete ancora gestire i problemi legati alle sessioni descritti sopra. Lo stesso vale lato client: un processo agent sul vostro sistema, come quello descritto in eseguire un agent di coding su un VPS, è stateless in questo senso soltanto se la libreria che usa comunica con una revisione moderna. Leggete la versione negoziata dal runtime, quindi consultate la revisione corrispondente della specifica e considerate questa pagina come la descrizione di una revisione specifica, non del protocollo in generale.

FAQ

Un server MCP senza stato significa che non posso memorizzare nulla?

No. Stateless descrive il protocollo, non l'applicazione. Database, code e cache continuano a funzionare come prima. Cambia il fatto che lo stato che si estende su più chiamate deve essere referenziato tramite un identificatore esplicito che il client invia in ogni richiesta, ad esempio un handle generato dal server in un argomento dello 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 le funzionalità disponibili, la versione del protocollo o l'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 funzionalità disponibili e le credenziali. Di conseguenza, qualsiasi replica può rispondere a qualsiasi richiesta e il round-robin è sufficiente. L'unico elemento di lunga durata rimasto è il flusso di risposta subscriptions/listen, che consiste in 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, e nessuna regola di routing lo impedisce.

Che fine hanno fatto Mcp-Session-Id e il flusso HTTP GET?

Entrambi sono stati rimossi nella revisione 2026-07-28, nell'ambito di 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 rimandarlo al client. Le notifiche di modifica avviate dal server viaggiano ora sul flusso di risposta di una richiesta subscriptions/listen, anziché su 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 verificare lo stato di un server MCP senza handshake?

Usa due livelli. Configura il controllo attivo del proxy su un percorso HTTP semplice servito dall'applicazione, perché un GET all'endpoint MCP restituisce correttamente 405 e farebbe considerare non disponibile un backend funzionante. Verifica quindi il protocollo stesso inviando una richiesta POST a server/discover, che ogni server 2026-07-28 deve implementare, e verifica che la risposta sia HTTP 200 e includa 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.