MCP in produzione: checklist
Prima che un server MCP tocchi dati veri controllo sette cose: hint, destinatario del token, scope, discovery OAuth, server locali, handle e log.
Perché conta
«Voglio collegare il gestionale all’assistente AI che uso già. Se passa da MCP, è sicuro?»
MCP (Model Context Protocol) è lo standard con cui un assistente AI usa strumenti e dati esterni, ma il protocollo non risponde al posto tuo. La specifica MCP lo scrive chiaramente: MCP non può imporre questi principi di sicurezza a livello di protocollo. Stabilisce come client e server si parlano. Se un certo server si può collegare in sicurezza ai tuoi dati dipende da come sono fatti il server, il client e il login.
Le ultime settimane mostrano dove si sbaglia. Tra il 28 settembre e il 5 ottobre 2026 l’SDK Python ufficiale di MCP (la libreria con cui si costruiscono server e client) ha pubblicato advisory su credenziali OAuth spedite a un server di autorizzazione scelto dal server MCP e su token accettati anche se emessi per un altro servizio. Nessuno dei due richiede una prompt injection (istruzioni nascoste nei contenuti che il modello legge). Riguardano entrambi il login e i permessi.
Qui sotto ci sono i sette controlli che faccio prima che un server MCP tocchi dati veri. Per ciascuno riporto cosa chiede la revisione corrente della specifica, la 2026-07-28, e come lo verifico. Ogni requisito ha il link alla sua fonte.
1. Un hint non è un permesso
Un tool può dichiarare delle annotations, per esempio readOnlyHint. Nello schema, readOnlyHint significa che il tool non modifica il suo ambiente. Nella stessa pagina c’è scritto che tutte le annotations sono indicazioni, che non è garantito che descrivano il tool in modo fedele e che il client non dovrebbe mai decidere l’uso di un tool in base alle annotations di un server non fidato. La pagina sui tool è ancora più netta: il client deve considerare le annotations non fidate, a meno che arrivino da server fidati.
Quindi un server può etichettare un tool come «sola lettura» e poi scrivere lo stesso. L’etichetta la scrive proprio chi stai cercando di limitare.
# Anti-pattern: decide l'etichetta scritta dal server
if tool.annotations.get("readOnlyHint" ):
auto_approve(call)
# Better: decide la mia policy, per server, tool e argomenti
if policy.allows(server.id , tool.name, call.arguments):
run(call)
else :
ask_person(call)
Il permesso vero sta in due posti. Sul server, che deve avere controlli di accesso adeguati. E nel client, dove una persona dovrebbe poter negare la chiamata a un tool.
Come lo verifico: elenco i tool che scrivono, inviano o cancellano. Per ognuno chiedo cosa lo ferma sul server se l’hint è sbagliato.
2. Il token deve essere emesso per il tuo server
Un server MCP remoto è un resource server OAuth. La specifica gli chiede di verificare che ogni access token sia stato emesso proprio per lui, come destinatario, e di rispondere 401 quando non lo è. Non deve accettare né far transitare altri token. Se il server chiama un’API a valle, usa un token separato, emesso dal server di autorizzazione di quell’API, e non deve inoltrare il token ricevuto dal client. La documentazione di sicurezza chiama il contrario token passthrough e spiega perché è vietato.
L’advisory dell’SDK Python del 5 ottobre 2026 è proprio questo caso. L’autenticazione bearer lato server non aveva modo di controllare il destinatario del token, quindi un token valido per un altro servizio dello stesso server di autorizzazione passava. La correzione è in mcp 1.30.0 e 2.2.0, e l’advisory precisa che aggiornare da solo non cambia nulla: il controllo resta spento finché non imposti validate_token_resource=True e non restituisci il destinatario del token dal tuo verifier.
Come lo verifico: prendo un token valido per un altro servizio, emesso dallo stesso server di autorizzazione, e lo mando al server MCP. La risposta deve essere 401.
3. Prima gli scope minimi
Il client dovrebbe chiedere solo gli scope (i permessi che un token concede) che gli servono. L’elenco scopes_supported del server dovrebbe contenere il minimo per l’uso di base; gli altri scope si chiedono dopo, con una richiesta di step-up, quando un tool ne ha bisogno.
Sul lato server c’è un dettaglio da non perdere. Se la prima risposta 401 non indica uno scope, il client ripiega su tutto quello che c’è in scopes_supported. Quindi quello che il server pubblica lì è quello che verrà chiesto all’utente. La documentazione di sicurezza elenca gli errori più comuni: pubblicare tutti gli scope possibili, usare scope jolly come * o full-access, e considerare sufficienti gli scope del token senza una logica di autorizzazione sul server.
Come lo verifico: leggo i metadati della risorsa protetta e la prima risposta 401. Poi confronto gli scope con quello che serve davvero al primo flusso.
4. Quello che il server manda durante il login non è fidato
Durante la discovery OAuth è il server MCP a dire al client dov’è il suo server di autorizzazione. Ogni campo di quei documenti arriva quindi da chi si sta autenticando.
La specifica chiede al client di registrare l’issuer atteso e verificare il parametro iss nella risposta di autorizzazione prima di mandare il codice a qualunque token endpoint. Richiede PKCE con S256 e redirect URI in HTTPS o su localhost. L’advisory ad alta gravità dell’SDK Python del 28 settembre 2026 mostra cosa succede senza il controllo dell’issuer: un server MCP malevolo poteva ricevere il client secret, il codice di autorizzazione e il verifier PKCE destinati al vero server di autorizzazione. Corretto in 1.30.0 e 2.2.0. Per i provider che girano senza un utente, la correzione funziona solo se passi issuer=.
L’altra metà è l’URL di autorizzazione. La documentazione di sicurezza dice che il client deve accettare solo http:// e https:// e non deve aprire gli URL tramite una shell. Delle due, è la seconda a proteggere davvero. Pillar Security ha descritto un caso in opencode, su Windows e WSL, in cui un normale URL https:// inviato da un server remoto poteva arrivare a PowerShell ed eseguire codice durante il login. Indicano come vulnerabili le versioni dalla 1.0.137 alla 1.18.32 e come corretta la 1.18.33, uscita il 28 settembre 2026. Un controllo sullo schema quell’URL lo lascia passare. Lo ferma non costruire mai un comando di shell con quell’URL.
Come lo verifico: annoto le versioni di client e SDK, controllo se l’issuer è fissato e guardo come il client apre l’URL di login.
5. Server locali: localhost, Origin, isolamento
Un server che gira sulla tua macchina ha rischi diversi. Con Streamable HTTP il server deve validare l’header Origin e rispondere 403 se non è valido, per bloccare il DNS rebinding (un trucco con cui una pagina web raggiunge servizi sul tuo computer), e dovrebbe ascoltare su 127.0.0.1, non su 0.0.0.0. Nell’SDK Python questa protezione era spenta per default prima della 1.23.0. Con stdio la specifica dice di non usare il flusso OAuth e di prendere le credenziali dall’ambiente.
La checklist ufficiale per i server locali copre il resto: un autore identificabile e una versione fissata, l’isolamento in un container, una sandbox o una VM, credenziali ristrette e separate per ogni server, accesso al filesystem limitato a cartelle precise e un elenco noto di destinazioni di rete.
Come lo verifico: leggo per intero il comando di avvio, controllo l’indirizzo su cui ascolta ed elenco le credenziali che il processo può raggiungere.
6. Un handle di stato non è un’identità
La revisione 2026-07-28 ha tolto le sessioni dal protocollo. Un server che deve mantenere uno stato tra una chiamata e l’altra restituisce un handle, per esempio l’ID di un carrello o di un flusso, e lo riceve indietro come normale argomento del tool. La pagina sui tool dice che un handle è un nome, non una capacità: a ogni chiamata il server dovrebbe verificare che chi chiama sia autorizzato su quell’handle. La documentazione di sicurezza aggiunge che il server non deve considerare il possesso di un handle come autenticazione e dovrebbe legarlo all’utente ricavato dal token verificato.
Le revisioni precedenti avevano lo stesso problema con gli ID di sessione. L’SDK Python instradava le richieste verso una sessione solo in base all’ID, senza verificare chi si era autenticato, fino alla 1.27.2.
Come lo verifico: creo uno stato come utente A e lo richiamo come utente B con lo stesso handle. La chiamata deve fallire.
7. Una persona che può dire no, e un log
È il modello a decidere quando chiamare un tool. La specifica dice comunque che dovrebbe esserci sempre una persona in grado di negare la chiamata a un tool. Il suo elenco di sicurezza chiede al server di validare gli input, applicare i controlli di accesso, limitare la frequenza delle chiamate e ripulire gli output, e al client di mostrare gli input prima della chiamata, validare i risultati, impostare dei timeout e registrare l’uso dei tool per l’audit.
Anche le descrizioni dei tool sono input: il modello le legge come istruzioni. La guida ai server locali consiglia di leggere l’elenco dei tool prima di affidarsi a un server, e di preferire client che chiedono di nuovo conferma quando la definizione di un tool cambia. L’MCP Inspector mostra i tool di un server senza un modello in mezzo.
Come lo verifico: scelgo un’azione di scrittura e cerco la sua traccia. Chi l’ha chiesta, quali argomenti sono usciti, chi l’ha approvata, cosa è tornato.
La checklist in una pagina
Se non sei tecnico, puoi girare questa lista a chi ti installa il server e chiedere una risposta per ogni riga.
- Nessun tool viene approvato per il suo
readOnlyHinto per altre annotations. - Il server risponde 401 a un token emesso per un altro servizio.
- Il server non inoltra mai a valle il token del client.
scopes_supportedcontiene solo quello che serve al primo flusso.- Il client fissa l’issuer atteso e usa PKCE con
S256. - Il client apre gli URL di login senza passare da una shell.
- I server HTTP locali controllano
Origine ascoltano su localhost; i server stdio hanno credenziali proprie e ristrette. - Ogni handle di stato viene verificato rispetto a chi chiama, a ogni chiamata.
- Le azioni di scrittura richiedono la conferma di una persona e lasciano una riga nel log.
- Le versioni di SDK e client sono fissate e includono le correzioni citate sopra.
Prima di collegarlo
Sono Gianluca Mazza, consulente AI indipendente. Se il tuo problema è quello dell’inizio, «voglio collegare il gestionale all’assistente che uso già e non so se è sicuro», scrivimi a info@gianlucamazza.it indicando il server e il client che vuoi usare, o usa il secondo parere scritto se hai già un preventivo per l’integrazione.
FAQ
readOnlyHint impedisce a un tool MCP di scrivere dati?
No. La specifica definisce tutte le annotations dei tool come indicazioni che possono non descrivere il tool in modo fedele. Il client deve considerarle non fidate, a meno che il server sia fidato. Una scrittura la fermano i controlli di accesso sul server e una persona che può negare la chiamata.
OAuth è obbligatorio per ogni server MCP?
No. Nella specifica l’autorizzazione è facoltativa. Quando la si usa su HTTP, vale la specifica di autorizzazione. Con stdio la specifica dice di prendere le credenziali dall’ambiente invece di usare il flusso OAuth.
Basta controllare lo schema dell’URL durante il login MCP?
No. Le linee guida di sicurezza MCP chiedono sia di accettare solo http e https sia di non aprire mai l’URL tramite una shell. Nel caso opencode descritto da Pillar Security l’URL era un normale https: il problema era la shell.
Quale revisione della specifica segue questa checklist?
La revisione 2026-07-28 della specifica MCP, con le pagine su autorizzazione e tool, e la documentazione ufficiale di sicurezza della stessa revisione. Gli advisory dell’SDK Python citati sono nella pagina di sicurezza dell’SDK su GitHub.
Vuoi qualcosa di simile? Vedi servizi e prezzi
Articoli correlati
I dati non fidati non devono decidere il flusso di controllo
Mostro come reasoning-kernel tiene i dati non fidati fuori dal percorso degli effetti: due reasoner non fidati, un gate deterministico. Non è un certificato.
2 ott 20268 min di lettura#Agenti#Sicurezza#LLM#Python#ProduzioneUna risposta rifiutata deve comunque addebitare i token
Se il runtime rifiuta la risposta, addebito i token della chiamata a produce che il provider ha già fatturato. Esattezza del ledger, mklang 1.3.7 / pull 111.
4 ott 20268 min di lettura#mklang#LLM#Costi#Agenti#ProduzioneRAG in produzione: prima chunking e re-ranking, poi gli embedding
Quando il retrieval è debole, cambiare gli embedding raramente lo sistema. Diagnostico prima chunking e re-ranking.
20 dic 202414 min di lettura#RAG#Retrieval#LLM#Produzione