Skip to main content

MCP versione 2026-07-28: cosa cambia e perché

Alessandro Mengoli
Serie: Model Context Protocol - Parte 3

Dalla sua introduzione a fine 2024 il Model Context Protocol ha avuto un’adozione molto rapida e continua. Come naturale evoluzione, e per rispondere alle tante richieste arrivate nel tempo, è stata rilasciata la versione 2026-07-28, che introduce grandi cambiamenti.

Nei precedenti articoli della serie abbiamo visto cos’è MCP e come costruire un server in .NET. Qui vediamo cosa è cambiato e perché.

Il cambio da stateful a stateless

Il più grande cambiamento è il passaggio da stateful a stateless. Tutti gli altri cambiamenti, grandi e piccoli, discendono da lì.

Quando il protocollo è nato, il suo scopo era principalmente quello di collegare processi locali attraverso una pipe, dove client e server possono scriversi quando vogliono. Quella simmetria su HTTP non esiste: parla il client, risponde il server. Per rendere comunque possibile la comunicazione server → client, erano stati introdotti dei meccanismi appositi: una sessione tenuta aperta, identificata da un Mcp-Session-Id, su cui il server poteva scrivere quando ne aveva bisogno.

Questo ha due costi. Il primo è che bisogna mantenere lo stato. Il secondo, più fastidioso, è che lega la richiesta del client all’istanza esatta del server che ha aperto quella sessione, e questo non permette di scalare: serve che il load balancer mandi sempre lo stesso client alla stessa istanza, e se quella cade, la sessione muore con lei.

La nuova versione interrompe questo flusso per sposarsi al meglio con HTTP e soprattutto per garantire scalabilità e affidabilità.

MRTR: Multi Round-Trip Requests

Se il server ha bisogno di informazioni dal client, semplicemente risponde: nella risposta chiede quello che gli serve e chiude la comunicazione. Il client, una volta ottenute le informazioni necessarie, richiama il server con la stessa chiamata arricchita della risposta.

A sinistra una sola chiamata HTTP che resta aperta mentre il server interroga l'utente, a destra due chiamate indipendenti legate da requestState

Facciamo un esempio concreto. Il tool è close_ticket: chiude un ticket di assistenza, ma prima vuole farsi confermare il motivo. È il caso minimo che richiede un’interazione a metà chiamata, ed è esattamente il punto in cui le due revisioni divergono.

Nel round 1 il client chiama tools/call. Il server non ha il motivo di chiusura, quindi non arriva in fondo al lavoro: risponde con resultType: input_required, la domanda da girare all’utente e un requestState. La chiamata HTTP finisce qui: il server lascia andare la connessione, l’handler termina, in memoria non resta niente. Il requestState è la sola continuità fra i due round, lo produce il server e il client lo rimanda indietro così com’è.

Nel round 2 il client ripete la stessa tools/call, aggiungendo il requestState ricevuto e la risposta dell’utente dentro inputResponses. Questa volta il server ha tutto quello che gli serve e chiude con resultType: complete.

Il campo da guardare è proprio resultType: input_required significa “mi manca qualcosa, richiamami”, complete che il giro è finito.

La differenza fra i due mondi si legge a colpo d’occhio nelle tracce dello stesso identico tool, eseguito prima con una sessione e poi in MRTR.

Traccia del vecchio modello con sessione: initialize, canale GET aperto, elicitation annidata e DELETE finale

Con la sessione: 26 span, profondità 8. Si vedono l’initialize, l’elicitation/create annidata dentro il tools/call e il DELETE finale che smonta la sessione. In mezzo c’è un GET / che da solo dura 0,15s su una traccia di 0,37s: è il canale server → client, aperto per tutta la sessione.

Traccia MRTR: server/discover, tools/list e due tools/call fratelli, senza GET e senza DELETE

Con MRTR: 19 span, profondità 4. I due tools/call sono fratelli, non uno dentro l’altro. Nessuna barra GET, nessun DELETE.

La differenza di profondità è il cambio di filosofia reso visibile: prima una chiamata conteneva una conversazione, ora sono due chiamate indipendenti.

Addio handshake

Sempre nell’ottica dello stateless, è stato rimosso anche il meccanismo di inizializzazione: l’exchange initialize/notifications/initialized e l’Mcp-Session-Id lasciano il posto a richieste standalone.

L’handshake faceva due lavori, e i due lavori hanno preso strade opposte.

La parte client — versione del protocollo, capability e identità — serviva al server per sapere con chi stava parlando. Ora viaggia in ogni richiesta dentro _meta, sotto tre chiavi:

io.modelcontextprotocol/protocolVersion
io.modelcontextprotocol/clientCapabilities
io.modelcontextprotocol/clientInfo

Sono, letteralmente, il contenuto dell’initialize spalmato su ogni richiesta. E non sono facoltative: se ne manca una, il server rifiuta la richiesta.

{"error":{"code":-32602,"message":
  "Requests using protocol version '2026-07-28' must include
   '_meta/io.modelcontextprotocol/protocolVersion'."}}

La parte serverserverInfo e serverCapabilities — permetteva al client di sapere che cosa era in grado di fare il server. Ora è diventata una richiesta ordinaria, server/discover, ed è facoltativa: un client che sa già quale tool vuole chiamare può saltarla e andare dritto al tools/call.

Si passa quindi da una negoziazione, con due parti che si mettono d’accordo e ricordano l’accordo, a una dichiarazione ripetuta a ogni richiesta.

👉 Un trucco pratico: con l'SDK .NET, una GET sull'endpoint di un server stateless risponde 405 Method Not Allowed, perché in quella modalità i verbi GET e DELETE non vengono nemmeno registrati. Non è garantito dalla specifica, ma è un modo rapido per capire da fuori come sta girando un server.

Le altre modifiche

  • Routing basato su header. Sono stati introdotti Mcp-Method e Mcp-Name. Dato che è tutto basato su POST verso lo stesso endpoint, un proxy che avesse voluto instradare le richieste avrebbe dovuto leggere e analizzare il body JSON: ora gli bastano due header.
  • Liste cacheabili. Le risposte di tools/list, prompts/list, resources/list e resources/read portano ora ttlMs e cacheScope: il client sa per quanto tenerle e con che ambito. Senza una sessione da cui dipendere, la cache diventa possibile.
  • Roots e Sampling deprecate. Erano richieste che partivano dal server verso il client. Continuano a funzionare per almeno dodici mesi, ma le nuove implementazioni non dovrebbero adottarle.
  • Logging deprecato. Era una notifica e non una domanda in attesa di risposta, quindi non potrebbe nemmeno avere un veicolo MRTR. La soluzione diventa, giustamente, OpenTelemetry — e se vuoi approfondire, c’è la serie dedicata su questo blog.
  • Elicitation viva. Non è deprecata: cambia solo il veicolo, e diventa il caso d’uso principale di MRTR.
  • Autorizzazione. Anche qui ci sono novità, ma sono abbastanza corpose da meritare un articolo a parte.

Pro, contro e a cosa fare attenzione

Come in tutte le cose, ci sono pro e contro, e punti su cui bisogna prestare attenzione.

A fronte di una maggiore scalabilità del sistema, abbiamo più round-trip, quindi più latenza: un’interazione che prima stava dentro una chiamata ora ne richiede due. Su una rete locale si parla di millisecondi, su una connessione lenta di molto di più.

E soprattutto dobbiamo prestare attenzione a come scriviamo il server, perché da ora in avanti va gestita l’idempotenza.

⚠️
Attenzione: il round 1 e il round 2 sono due chiamate allo stesso tool con gli stessi argomenti. Tutto quello che il tool fa prima di chiedere l'informazione che gli manca viene quindi eseguito due volte. Gli effetti collaterali vanno spostati dopo il punto in cui il tool ha tutto quello che gli serve, oppure protetti con una chiave di deduplicazione.

Conclusioni

MCP è passato da “il server può richiamare il client nel mezzo di una chiamata” a “il server risponde e si dimentica di te”. La fine delle sessioni, gli header nuovi, le liste cacheabili e le feature deprecate discendono tutti da questa scelta.

Il prezzo è qualche round-trip in più e la responsabilità dell’idempotenza. Quello che si ottiene è un server che si mette dietro un load balancer qualsiasi senza configurazioni particolari, e che può essere riavviato senza effetti drastici sulle conversazioni in corso.

Nel prossimo articolo vediamo la pratica: come scrivere in .NET un server MCP con la nuova revisione e come migrare un server esistente senza rompere i client vecchi.

Risorse utili