MCP: oltre ai tools, ci sono resources e prompts

Chi scrive il primo server MCP di solito fa la stessa cosa: prende le funzioni che vuole esporre e ne fa dei tools. Funziona, e i tutorial si fermano lì. Poi il server cresce, i tool diventano quattordici, e metà di loro sono get_qualcosa senza argomenti che restituiscono sempre lo stesso documento.
Il protocollo ha altre due primitive lato server, resources e prompts, ed è lì che vanno quasi tutti quei get_qualcosa. Quello che segue fa riferimento alla revisione corrente della specifica, la 2026-07-28, che è quella che la pagina Versioning indica come “current”. Attenzione: una ricerca porta spesso su una revisione precedente, e su alcuni punti (le sottoscrizioni, sotto) le differenze non sono trascurabili.
La differenza vera: chi decide
Le tre primitive non si distinguono per il formato dei dati. Si distinguono per chi decide quando usarle, e la specifica lo dice in una frase per ciascuna:
- i tools sono model-controlled: “the language model can discover and invoke tools automatically based on its contextual understanding and the user’s prompts”;
- le resources sono application-driven: “with host applications determining how to incorporate context based on their needs”;
- i prompts sono user-controlled: “exposed from servers to clients with the intention of the user being able to explicitly select them for use”.
Per i prompts la specifica aggiunge una precisazione che conviene tenere a mente: “This refers to who decides when the prompt is used, not who authors its content. Prompt content is defined by the server.”
Quindi la domanda da farsi, per ogni cosa che si vuole esporre, è: deve essere il modello a deciderlo da solo, l’applicazione, o la persona davanti allo schermo?
Resources: contesto da leggere, indirizzato da un URI
Una resource è un dato che dà contesto al modello: “such as files, database schemas, or application-specific information”. Ognuna è identificata da un URI. Il client le elenca con resources/list e le legge con resources/read:
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "file:///project/src/main.rs"
}
}
La risposta ha un array contents, con text per il testo o blob in base64 per i binari. Per i dati parametrici ci sono i resource template, con un uriTemplate secondo RFC 6570 (per esempio file:///{path}), elencati da resources/templates/list.
Ecco perché un server con quattordici tool spesso è un server che avrebbe dovuto esporre tre resources. Lo schema del database, la documentazione interna e la configurazione corrente sono tutte cose da leggere, e passarle come tool significa chiedere al modello di decidere ogni volta se chiamarle. Esposte come resources, è l’applicazione a decidere come includerle: la specifica cita un selettore nell’interfaccia, una ricerca, o l’inclusione automatica. Il modello non spreca un turno per scoprire cosa c’è.
Il tool resta la risposta giusta quando c’è un’azione o un calcolo con argomenti che cambiano a ogni chiamata. Una regola pratica: se il “tool” non ha argomenti e il risultato cambia solo quando cambia il dato, quasi sempre è una resource.
Le resources supportano anche delle annotations facoltative: audience ("user", "assistant" o entrambi), priority da 0.0 a 1.0 e lastModified in ISO 8601. Sono indicazioni al client su cosa includere nel contesto per primo.
Prompts: modelli che l’utente richiama
Un prompt è un template di messaggi che il server offre e che l’utente sceglie esplicitamente. L’esempio tipico, citato dalla specifica, sono gli slash command. Il client li elenca con prompts/list e ne ottiene uno con prompts/get, passando gli argomenti:
{
"jsonrpc": "2.0",
"id": 2,
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": {
"code": "def hello():\n print('world')"
}
}
}
La risposta è una lista di messages, ciascuno con un role ("user" o "assistant") e un content: testo, immagine, audio, un resource_link oppure una resource incorporata ("type": "resource"). Quest’ultima è il punto in cui le due primitive si incontrano: un prompt “prepara la revisione del modulo X” può portarsi dietro il contenuto del modulo senza che il modello debba chiamare nulla.
Nello schema (schema.ts) gli arguments di prompts/get sono una mappa di stringhe. Niente oggetti annidati: se un prompt ha bisogno di input strutturato, probabilmente è un tool.
Il prompt è la risposta giusta quando si vede ripetere la stessa richiesta ben formulata: “analizza questo log con questi criteri”, “scrivi il changelog in questo formato”. Invece di sperare che il modello indovini quando usarla, la si dà all’utente come comando.
Le notifiche: cosa è cambiato nella 2026-07-28
Ogni primitiva può dichiarare listChanged nelle capability, e il server notifica quando l’elenco cambia (notifications/resources/list_changed, notifications/prompts/list_changed). Le resources hanno anche subscribe, per sapere quando cambia una resource specifica.
Qui la revisione corrente cambia le cose. Le notifiche viaggiano su uno stream aperto con subscriptions/listen, che secondo la specifica “replaces the former resources/subscribe RPC and the HTTP GET endpoint”. Il client dichiara cosa vuole ricevere in un filtro notifications:
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscriptions/listen",
"params": {
"notifications": {
"promptsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}
(Nell’esempio manca il _meta che ogni richiesta deve avere, come fa la specifica stessa per brevità.) I campi sono quattro: toolsListChanged, promptsListChanged, resourcesListChanged e resourceSubscriptions. Il server “MUST NOT send notification types the client has not explicitly requested”, deve rispondere per prima cosa con notifications/subscriptions/acknowledged e lì indica il sottoinsieme che ha accettato davvero. Il client dovrebbe confrontarlo con quello che ha chiesto. Su stdio, dopo una riconnessione, il client “MUST re-send subscriptions/listen“: il server non conserva lo stato delle sottoscrizioni.
Se avete codice scritto su una revisione precedente che chiama resources/subscribe, è questo il punto da rivedere.
Errori da evitare
- Restituire
contentsvuoto per una resource che non esiste. La specifica lo vieta: “Servers MUST NOT return an emptycontentsarray for a non-existent resource”. L’errore giusto è un JSON-RPC-32602(Invalid Params). I client dovrebbero accettare anche-32002, il codice delle versioni precedenti, ma un server nuovo deve usare-32602. - Usare
https://come URI di una resource che si legge solo dal server. La specifica chiede di usare quello schema solo quando il client può scaricare la risorsa da solo. Negli altri casi meglio un altro schema o uno personalizzato, purché conforme a RFC 3986. - Non validare gli URI. Tra le Security Considerations: “Servers MUST validate all resource URIs” e, per le
file://, “MUST sanitize file paths to prevent directory traversal attacks”. Un resource templatefile:///{path}senza controlli è un modo elegante di esporre l’intero disco. - Un elenco che dipende dalla connessione. Come per i tools,
resources/listeprompts/list“MUST NOT vary per-connection”. Possono cambiare in base all’autorizzazione presentata sulla richiesta, non in base a uno stato tenuto sul canale. - Fidarsi del contenuto di un prompt. La sezione Security è una riga sola, ma è una
MUST: validare input e output dei prompt “to prevent injection attacks or unauthorized access to resources”.
In sintesi
Prima di aggiungere il quindicesimo tool vale la pena chiedersi chi deve decidere quando usarlo. Se è il modello, è un tool. Se è un dato da mettere nel contesto, è una resource. Se è una richiesta che l’utente ripete sempre uguale, è un prompt. Il server ne esce più piccolo, e il modello smette di sprecare turni per scoprire cosa c’è.
Se state progettando un server MCP sui dati della vostra azienda e volete ragionare su cosa esporre e come, scriveteci.
