CodeGym /Corsi /ChatGPT Apps /Handshake e capabilities: come il client scopre cosa sa f...

Handshake e capabilities: come il client scopre cosa sa fare il server

ChatGPT Apps
Livello 6 , Lezione 2
Disponibile

1. Perché serve l’handshake

Se gli endpoint REST sono come un insieme di porte separate a cui si bussa tramite URL, MCP è piuttosto un dialogo continuo su un unico canale. Il client non invia semplicemente richieste sparse: prima stabilisce una sessione. L’handshake è il momento di presentazione all’inizio di questa sessione.

In MCP questo momento è realizzato come una richiesta speciale initialize che il client invia subito dopo aver stabilito il trasporto (STDIO, HTTP/stream, WebSocket — non importa). Nella richiesta comunica: «Parlo questa versione di MCP, ecco che cosa so fare e chi sono». Il server risponde: «Io supporto questa versione e queste capacità, lieto di conoscerti».

Dopo lo scambio riuscito, il client invia la notifica notifications/initialized e solo dopo inizia la fase operativa: tools/list, resources/list, tools/call e altre cose utili.

Per analogia, l’handshake MCP è come un contratto di locazione prima di portare i server in un data center. Finché non avete concordato le regole (formato del protocollo, quali servizi fornisce il data center, chi paga) — trasportare i server non ha senso.

Dal punto di vista pratico l’handshake risolve tre obiettivi:

  1. Verifica la compatibilità delle versioni del protocollo.
  2. Dichiara quali «primitivi» MCP il server supporta: tools, resources, prompts, logging, notifiche ecc.
  3. Fornisce metainformazioni su client e server — nome e versione dell’implementazione.

2. Ciclo di vita della connessione MCP: dove vive l’handshake

Per rendere il quadro meno astratto, vediamo uno scenario tipico (flow) della connessione, molto semplificato:

sequenceDiagram
    participant C as Cliente (ChatGPT/Inspector)
    participant S as Server MCP

    C->>S: (1) Stabiliamo il trasporto (STDIO/HTTP-stream)
    C->>S: (2) Request: "initialize"
    S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notification: "notifications/initialized"
    C->>S: (5) Request: "tools/list" / "resources/list"
    S-->>C: (6) Result: elenchi di strumenti/risorse
    C->>S: (7) Request: "tools/call" e altro

Dal punto di vista tecnico i passaggi sono così:

  1. Il trasporto è stabilito: per esempio, ChatGPT avvia il vostro server come subprocess e si collega via STDIO, oppure l’Inspector effettua una richiesta HTTP/stream a /mcp.
  2. Il client invia una richiesta JSON-RPC initialize.
  3. Il server risponde con un risultato JSON-RPC con i campi protocolVersion, capabilities e serverInfo.
  4. Il client invia la notification notifications/initialized — segnale: «ho letto tutto, si può lavorare».
  5. Il client chiama i metodi di discovery (tools/list, resources/list, prompts/list) in base a quanto visto nelle capabilities del server.
  6. Il server restituisce i metadati di strumenti/risorse/prompt.
  7. Poi arrivano le richieste «operative»: tools/call, resources/read e altre.

È importante notare che l’handshake è solo una normale chiamata JSON-RPC initialize. Nessuna magia. Dopo la lezione sul formato dei messaggi MCP sapete già analizzare tali richieste; l’unica differenza è che qui il metodo è sempre uno e «speciale», ed è eseguito per primo.

3. Che cosa invia il client in initialize

Analizziamo la richiesta initialize per parti. Ecco come può apparire una richiesta minima (semplificata per la lezione):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

Questo esempio è vicino a quello mostrato nella documentazione ufficiale di MCP. I campi principali in params:

protocolVersion

Una stringa con la versione della specifica MCP, spesso in formato data, per esempio "2025-06-18". Non è la versione della vostra applicazione, ma del protocollo stesso. Il client dice: «mi aspetto di parlare questa versione di MCP». Il server nella risposta deve confermarla oppure restituire un errore se non la conosce.

È una protezione contro la situazione «il client pensa una cosa, il server ne implementa un’altra». Se non si trova una versione comune, è meglio interrompere onestamente la connessione piuttosto che scambiarsi messaggi incompatibili.

capabilities del client

Un oggetto in cui il client dichiara quali funzionalità MCP supporta. Ad esempio, il client ChatGPT indica spesso la chiave elicitation, segnalando di poter gestire richieste all’utente (input aggiuntivo, conferme, ecc.).

Esempio:

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

Il server può usare questa informazione per capire quali funzionalità estese del protocollo abbia senso usare. Per esempio, elicitation significa che il client (ChatGPT) può porre all’utente domande di chiarimento e richiedere dati aggiuntivi.

clientInfo

Semplice metainformazione: nome e versione del client.

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

Dal punto di vista dello sviluppatore del server è oro per i log: potete sempre vedere quale client si è appena connesso — ChatGPT, MCP Inspector, un vostro client di test e quale numero di versione ha.

4. Cosa risponde il server: il risultato di initialize

La risposta a initialize è un normale risultato JSON-RPC con lo stesso id, ma nel campo result viene inserita la descrizione di ciò che il server sa fare.

Nella richiesta abbiamo guardato alle capabilities dal lato del client — cioè ciò che supporta. Ora analizziamo l’oggetto speculare nella risposta: le capabilities del server, cioè ciò che sa fare lui. In modo schematico:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {},
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.1.0"
    }
  }
}

Una struttura simile la vedrete anche nella descrizione ufficiale del protocollo e/o dell’SDK. Parti principali:

protocolVersion nella risposta

Il server o ripete la versione proposta dal client oppure (teoricamente) potrebbe scegliere un’altra versione comune, se ce ne sono più di una. Nelle implementazioni tipiche si conferma semplicemente la versione del client, se supportata. In caso contrario — il server deve restituire un errore e interrompere la comunicazione.

serverInfo

Metainformazione sul server: nome, versione.

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

Sembra noioso, ma proprio su questi dati poi filtrerete e cercherete nei log: «perché ChatGPT versione X non si accorda con il nostro server versione Y».

capabilities del server

Il campo più interessante. Qui il server dichiara quali primitivi MCP ed estensioni supporta: se può gestire tools/*, resources/*, prompts/*, se sa inviare notifiche sul cambio degli elenchi, ecc.

Se in capabilities non c’è la sezione tools, nessun client correttamente implementato chiamerà tools/list o tools/call. Allo stesso modo, l’assenza di resources significa che il client non invierà resources/list e resources/read.

In questo modo le capabilities sono un contratto leggero: «che cosa si può e non si può fare con questo server».

5. Capabilities come «lista di superpoteri»

D’ora in poi ci interessa solo il capabilities del server — quell’oggetto che arriva in risposta a initialize e determina quali primitivi MCP questo server supporta.

Vediamo più in dettaglio la sua struttura. Esempio (semplificato, ma vicino alla specifica):

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

Un esempio del genere è discusso nell’architettura ufficiale di MCP. Decodifichiamolo per sezioni.

Capabilities.tools

La presenza della chiave tools dice che il server sa rispondere ai metodi tools/list e tools/call. Se c’è anche il flag listChanged: true, significa che in futuro il server può inviare notifiche tools/list_changed quando il set di strumenti cambia.

Per ChatGPT è utile: si può mettere in cache l’elenco degli strumenti e, quando si riceve list_changed, aggiornarlo senza un reconnect completo.

Capabilities.resources

La sezione resources dichiara che il server supporta la gestione delle risorse: resources/list, resources/read, talvolta la ricerca. Flag interni:

  • subscribe: true — il client può sottoscriversi ai cambiamenti delle risorse (ad esempio per live log o aggiornamenti di file).
  • listChanged: true — il server può inviare la notifica resources/list_changed se risorse sono aggiunte o rimosse.

È particolarmente importante per grandi cataloghi o dati «vivi» che cambiano continuamente.

Capabilities.prompts

Se il server registra prompt predefiniti (per esempio modelli di richieste al modello, legati al vostro dominio), nel capabilities appare la chiave prompts. Anche qui può esserci il flag listChanged.

Il client, vedendo questa sezione, capisce che è disponibile il metodo prompts/list e forse prompts/get.

Capabilities.logging e altri

Alcune implementazioni dichiarano anche logging — significa che il server può inviare al client log strutturati tramite MCP, ad esempio per il debugging.

Possono apparire anche altre sezioni (per esempio, sampling o estensioni specifiche). È importante che il protocollo sia progettato fin dall’inizio per essere estensibile: potete aggiungere nuove chiavi alle capabilities e i client vecchi semplicemente le ignoreranno se non le conoscono.

Insight

Sperimentalmente è stato stabilito che ChatGPT App ignora i messaggi listChanged inviati. Al momento, quando scrivete un’app, non potete dichiarare un set di tools e poi aggiungerne o rimuoverne altri. Anche se il protocollo MCP lo consente.

Al momento della stesura di questo corso la situazione è la seguente: al momento della registrazione della vostra app nel ChatGPT Store, ChatGPT richiede alla vostra app l’elenco di tools e resources e lo memorizza in cache per sempre. Probabilità che la situazione cambi nel corso del 2026 — alta, probabilità che cambi nel primo trimestre del 2026 — bassa.

6. Discovery dopo l’handshake: come ottenere l’elenco di strumenti e risorse

L’handshake risponde alla domanda «che cosa sa fare il server in generale». Il passo successivo è il cosiddetto discovery: il client, tramite metodi concreti, estrae i dettagli — quali strumenti esistono, quali risorse sono disponibili, quali prompt sono predefiniti.

Per questo si usano i metodi di discovery: in sostanza tools/list, resources/list, prompts/list. Nella documentazione dell’architettura MCP si propone proprio questo racconto: handshake → discovery → chiamate agli strumenti.

Esempio di richiesta tools/list:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

La risposta del server contiene un array di strumenti: nomi, descrizioni, JSON Schema degli argomenti e talvolta metadati come categorie o icone.

Dopo di ciò, ChatGPT (o un altro client) mette in cache l’elenco e durante il dialogo lo usa per:

  • selezionare lo strumento adatto al compito dell’utente;
  • verificare che il nome dello strumento esista;
  • validare gli argomenti prima di inviare tools/call.

Con le risorse è una storia simile, solo che resources/list spesso supporta la paginazione tramite cursori per non trasferire subito un milione di record. Anche questo è descritto nella specifica MCP ed è trattato come un caso tipico per grandi cataloghi.

7. Handshake e capabilities sull’esempio della nostra app GiftGen

Nei moduli precedenti abbiamo costruito un’app didattica che aiuta a scegliere regali. Abbiamo già un widget, lo strumento suggest_gifts sul backend, e un qualche catalogo di regali. Ora immaginiamo come appare l’handshake per il server MCP gift-genius.

Esempio di handshake per GiftGen

Richiesta dal client:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

Risposta del nostro server:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.2.0"
    }
  }
}

Di fatto ripetiamo quasi gli esempi dell’architettura ufficiale MCP, adattando solo i nomi alla nostra app.

Che cosa apprende il client da questa risposta:

  • Ci sono strumenti (tools) e l’elenco può cambiare dinamicamente (listChanged: true).
  • Ci sono risorse (il nostro catalogo di regali, magari salvato in file o DB).
  • Ci sono prompt (per esempio un modello «Formula una breve descrizione del regalo per l’utente N»).
  • Il server può inviare log (comodo per gli inspector e per il debugging).

Poi il client esegue tools/list e vede ad esempio uno strumento così:

{
  "name": "suggest_gifts",
  "description": "Propone idee regalo in base al profilo del destinatario.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

E ora, quando l’utente scrive qualcosa come: «Suggerisci un regalo per mia sorella, 25 anni, budget fino a 50 dollari», il modello sa già: c’è lo strumento suggest_gifts con tale set di argomenti e può essere invocato tramite tools/call.

8. Come l’SDK nasconde l’handshake (ma perché è comunque importante capirlo)

Nel TypeScript SDK per MCP (quello che useremo nella prossima lezione) tutta questa storia con initialize e notifications/initialized è nascosta nel metodo connect. Codice indicativo:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "gift-genius",
  version: "1.0.0",
});

// Registrazione dello strumento — l’SDK, in base a ciò, configurerà automaticamente capabilities.tools
server.tool(
  "suggest_gifts",
  {
    description: "Propone idee regalo.",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... logica di selezione dei regali ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// Qui l’SDK:
// 1) riceve initialize dal client,
// 2) risponde con serverInfo e capabilities,
// 3) attende notifications/initialized,
// 4) poi inizia a gestire le chiamate tools/*.
await server.connect(transport);

L’SDK compone automaticamente le capabilities in base a ciò che registrate: se c’è almeno un server.tool(...), aggiungerà nelle capabilities la sezione tools. Se registrate risorse o prompt, compariranno resources e prompts.

Capire handshake e capabilities non serve per scrivere a mano il JSON (non fatelo mai), ma per:

  • leggere i log MCP e capire perché il client «non vede» i vostri strumenti;
  • diagnosticare incompatibilità di versione del protocollo;
  • se necessario, implementare un server personalizzato o un trasporto non standard.

9. Versioni del protocollo ed evoluzione delle funzionalità

Il campo protocolVersion nell’handshake non è una decorazione. Nella specifica MCP è esplicitamente sottolineato: è il modo per accordarsi su una versione compatibile del protocollo; se non si trova una versione comune, è meglio terminare la connessione.

Scenario tipico:

  1. Distribuite un server MCP in produzione con un SDK che implementa MCP versione "2025-06-18".
  2. Col tempo esce una nuova versione di MCP, aggiornate il client ma il server è ancora vecchio.
  3. Il client invia protocolVersion: "2026-02-01", il server non conosce tale versione e restituisce l’errore invalid protocol version (o analogo).

La pratica mostra che gli sviluppatori spesso ignorano questo campo e poi si stupiscono del perché la connessione non si stabilisca.

Approccio corretto alle versioni:

  • Sapere sempre quale versione di MCP supporta il vostro SDK (di solito nella documentazione/note di rilascio).
  • Quando aggiornate l’SDK — aggiornare consapevolmente la versione del protocollo.
  • Log e monitoraggio devono mostrare chiaramente gli errori di inizializzazione dovuti al mancato allineamento di protocolVersion.

Anche l’estensione delle funzionalità tramite le capabilities è legata all’evoluzione: nuove funzioni MCP vengono aggiunte come nuove chiavi in capabilities. I client vecchi le ignorano, i nuovi le possono usare. Questo pattern è descritto nella documentazione ufficiale MCP come modo per mantenere compatibilità all’indietro.

10. L’handshake visto da ChatGPT e dagli inspector

Cosa fa ChatGPT quando si collega a MCP

Quando in Dev Mode associate un server MCP a ChatGPT, la piattaforma dietro le quinte fa grosso modo quanto segue:

  1. Apre il trasporto (di solito HTTP/stream su /mcp).
  2. Invia initialize con protocolVersion, capabilities e clientInfo (qualcosa come «ChatGPT Enterprise, versione tale»).
  3. Riceve la risposta e mette in cache le capabilities del server.
  4. Esegue tools/list, resources/list, prompts/list in base alle capabilities viste.
  5. Durante il dialogo, quando il modello decide di chiamare uno strumento, confronta con questa cache: se esiste tale tool, qual è il suo schema degli argomenti, e come formare la chiamata.

Se le capabilities del server non contengono tools, ChatGPT non proverà nemmeno a proporre la vostra App come strumento. Se nelle capabilities c’è resources, ma senza il flag listChanged, ChatGPT può mettere in cache l’elenco delle risorse e non attendere notifiche di modifiche.

Come gli inspector e MCP Jam aiutano il debugging

Strumenti come MCP Jam / MCP Inspector fanno praticamente lo stesso: stabiliscono la connessione, eseguono l’handshake, vi mostrano le capabilities del server e vi permettono di chiamare a mano tools/list, tools/call e altro.

Dal punto di vista dello sviluppatore è un must-have:

  • si vede quale protocolVersion ha restituito realmente il server;
  • è subito visibile se nelle capabilities ci sono tools, resources, prompts;
  • si può capire, perché ChatGPT non vede gli strumenti (capabilities non dichiarate o handshake non riuscito).

Nell’ultima lezione di questo modulo userete questi strumenti più approfonditamente, ma già ora è utile capire che funzionano esattamente sopra l’handshake che stiamo analizzando.

11. Errori tipici nel lavorare con handshake e capabilities

In teoria tutto appare abbastanza lineare, ma in pratica proprio l’handshake e la dichiarazione delle capabilities diventano spesso la fonte di bug molto banali — soprattutto in Dev Mode o in MCP Inspector. Di seguito alcuni errori tipici con cui quasi sicuramente vi imbatterete, nel vostro codice o nei log dei colleghi.

Errore n. 1: formato errato della richiesta initialize.
Problema molto comune nelle implementazioni manuali di un server MCP senza SDK — perdere qualche campo obbligatorio di JSON-RPC. Per esempio, dimenticare jsonrpc: "2.0", confondere method (scrivere "init" invece di "initialize"), o rendere capabilities un boolean invece di un oggetto. La specifica MCP si aspetta un formato rigoroso; qualsiasi deviazione porta a errori di parsing e alla chiusura della connessione. La documentazione e le guide pratiche consigliano esplicitamente di assicurarsi per prima cosa che initialize sia strettamente conforme alla specifica, prima di guardare ad altro.

Errore n. 2: ignorare protocolVersion.
A volte gli sviluppatori copiano un esempio dalla documentazione e inseriscono una stringa arbitraria senza considerare il supporto dell’SDK. Di conseguenza, client e server parlano versioni diverse di MCP e la connessione non si stabilisce. L’errore può mascherarsi come «il client non si collega affatto». Bisogna trattare protocolVersion come un vero contratto: allineare questa versione tra il team frontend/piattaforma agentica e il team che sviluppa il server MCP.

Errore n. 3: capabilities dimenticate.
Situazione classica: avete registrato uno strumento sul server, ma in una implementazione manuale dell’handshake avete dimenticato di aggiungere "tools": {} nelle capabilities della risposta initialize. Nell’inspector vedete che gli strumenti ci sono, ma ChatGPT mostra «No tools available» — perché si fida delle capabilities e non fa tools/list, se la sezione tools non c’è. Le guide di troubleshooting per Apps SDK sottolineano: se ChatGPT non vede gli strumenti, prima di tutto controllate le capabilities.

Errore n. 4: tentare di usare metodi non dichiarati nelle capabilities.
Talvolta gli studenti sperimentano e, per esempio, inviano resources/list a un server che nelle capabilities non ha la sezione resources. Formalmente il server può rispondere Method not found, ma è più corretto non chiamare affatto tali metodi. MCP introduce le capabilities proprio come protezione da tentativi del genere. Il client deve prima guardare se esiste la sezione corrispondente in capabilities, e solo dopo invocare i metodi.

Errore n. 5: il server inizia a «parlare» prima di notifications/initialized.
Se il server, subito dopo aver inviato la risposta a initialize, inizia a mandare al client log o notifiche senza attendere notifications/initialized, alcuni client possono ignorare questi messaggi o persino chiudere la connessione. Nell’architettura ufficiale MCP si sottolinea che prima l’handshake deve concludersi e solo dopo la notifica di inizializzazione comincia la fase «operativa».

Errore n. 6: modificare lo schema degli strumenti senza segnalare il cambio dell’elenco.
Quando modificate il JSON Schema di uno strumento (rendete un campo obbligatorio, rinominate un argomento), ma non riavviate il server o non inviate la notifica che l’elenco degli strumenti è cambiato, la cache del client può contenere la versione vecchia dello schema. Questo porta a strani errori di validazione. La specifica propone di usare il flag listChanged e le notifiche tools/list_changed e resources/list_changed, per aiutare il client ad aggiornare la cache tempestivamente.

Errore n. 7: ottimizzazione prematura e «magia» intorno alle capabilities.
Talvolta gli sviluppatori iniziano a inventare schemi complessi con generazione dinamica delle capabilities, versioning per client e altra esotica, senza aver compreso i meccanismi di base. All’inizio è sufficiente dichiarare onestamente che cosa sa fare il server: tools, resources, prompts, logging. Ampliare le capabilities ha senso man mano che serve davvero, non «per il futuro». È più un anti‑pattern organizzativo che un errore puramente di protocollo, ma nei progetti in produzione capita spesso.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION