Cosa ci è voluto per costruire un server MCP per DynamoDB sicuro
Il rende banale affidare a un il tuo database. È proprio questo il problema. La maggior parte dei server MCP per DynamoDB è un processo sottile che custodisce le tue credenziali AWS grezze con un percorso di scrittura e nessun passaggio di revisione — abbiamo scritto del perché quel pattern è rischioso e come scegliere in sicurezza dal lato dell'utente.
Questo articolo racconta l'altra faccia: cosa ci è voluto davvero per costruire la versione sicura. DynoTable è un client desktop per DynamoDB il cui server MCP espone il suo toolkit AI protetto ad agenti esterni — Claude Code, Cursor, Codex — senza che l'agente tocchi mai le credenziali AWS. Arrivarci ha significato pubblicare un server di autorizzazione dentro un'app Electron, progettare un tipo di identità che rende i mescolamenti di credenziali irrappresentabili e decidere, più di una volta, che il default conveniente era quello sbagliato.
Se stai costruendo un server MCP attorno a qualcosa di sensibile, sono decisioni che incontrerai anche tu.
L'HTTP di loopback non è automaticamente sicuro
Il server si lega a 127.0.0.1, il che suona sicuro ma non è sufficiente. Qualsiasi
pagina web che visiti può tentare richieste verso localhost, e il DNS
rebinding permette a una pagina ostile di camuffarle con un Host plausibile. Perciò
il primo strato è un origin guard: rifiuta qualsiasi richiesta che porti un vero
Origin di pagina web, rifiuta gli header Host non di loopback — ma consenti
le richieste senza alcun Origin, perché i client CLI legittimi come
Claude Code non ne inviano uno.
Quest'ultima clausola è la parte onesta: un origin guard blocca soltanto la classe di attacchi via browser. Non può autenticare nessuno. Il bearer token è il vero cancello, il che solleva la domanda su da dove arrivino i token.
Abbiamo pubblicato un server di autorizzazione OAuth 2.1 dentro l'app
La storia dell'autenticazione per i server remoti di MCP è OAuth 2.1, e per un server desktop locale non c'è alcun identity provider a monte su cui appoggiarsi. Quindi l'app è il server di autorizzazione: registrazione dinamica dei client, (S256), codici di autorizzazione, refresh token, revoca e i documenti di metadati RFC 8414 / RFC 9728 che permettono ai client di scoprire il tutto.
Regole che abbiamo fissato presto e mantenuto:
- Non implementare a mano il protocollo. L'SDK di MCP fornisce l'intera superficie AS come router; noi implementiamo solo il provider di storage/policy sottostante. Una conseguenza che abbiamo accettato: il nostro framework MCP parla uno stack HTTP internamente e il router dell'SDK ne parla un altro, così l'AS gira come un secondo listener di loopback, con il resource server che indirizza i client ad esso tramite i protected-resource metadata. Due listener sono un costo circoscritto ed esplicito; ri-derivare a mano PKCE e la grammatica dei token è un costo illimitato.
- I redirect sono solo di loopback (secondo l'RFC 8252 per le app native), e un redirect non valido non riceve mai un redirect di errore — riceve un 400 diretto, così l'URI di un attaccante non è mai una destinazione.
- I codici di autorizzazione sono monouso — un codice riprodotto è un grant non valido, punto e basta.
- Il refresh può restringere lo scope ma mai allargarlo, e riemette lo stesso payload di identità alla lettera — un token rinnovato non può acquisire di nascosto una region diversa.
- La revoca viene ri-verificata a ogni richiesta, non messa in cache alla connessione — fare clic su «Revoca» nelle Impostazioni uccide la prossima chiamata del client.
Le build di sviluppo usano invece un percorso di autenticazione più semplice, costruito in modo da non poter esistere in un binario di release — un token non autenticato a scope pieno in un software distribuito sarebbe una backdoor locale dietro un origin guard che ammette deliberatamente le richieste senza Origin.
Una connessione, un'identità AWS — trasportata intera, mai indovinata
La proprietà di sicurezza centrale: un agente esterno si connette a un profilo, non a «DynoTable». Ogni connessione è legata a un profilo AWS e a una region, sostenuta dal proprio client DynamoDB non condiviso, così due agenti su profili diversi non possono sconfinare l'uno nell'altro.
Il binding è nato come campi sciolti — awsProfile, region,
profileId, una porta locale opzionale — ridichiarati in ogni record lungo
il percorso. Ora è un unico tipo di identità a unione discriminata con due
rami: online (profilo + region + id) e offline (porta locale + id,
per i profili DynamoDB Local). L'essere offline è
il discriminante stesso, così un'identità online senza credenziali o una
offline che porta una region è un errore di tipo, non una sorpresa a runtime.
Quell'unico tipo viaggia intatto attraverso consenso → token → grant → ogni
richiesta.
Due conseguenze che consideriamo portanti:
- La region è congelata al consenso. L'utente approva «profilo X nella
region Y», e quella coppia è fissata sul token — sopravvivendo al refresh,
mai ri-risolta da stato mutabile sul percorso critico. Modificare la
region del profilo richiede un nuovo consenso. L'alternativa — risolvere la
region a ogni richiesta o ripiegare su un default — è il modo in cui un agente
finisce di nascosto a interrogare
us-east-1. - I token malformati sono un 401, mai una coercizione. Se le claim di identità
di un token non si parsano in un ramo valido, la richiesta non è autorizzata. Nessun
profilo
'default', nessuna region di default, nessuna stringa vuota. Ogni punto in cui il vecchio codice avrebbe indovinato è ora un fallimento netto.
Il consenso è una superficie di prodotto, non una finestra di dialogo
Il primo prompt di consenso era la message box nativa della piattaforma. Congelava il processo main di Electron mentre era aperta, non poteva rispettare il design system dell'app e non poteva nemmeno essere catturata come screenshot per la documentazione. Ora è un modale dentro l'app: il nome del client che si connette, un selettore di profilo su esattamente i profili che l'utente ha scelto di esporre, e tre scope offerti dal più ristretto al più ampio — read-only, read + stage, full. Le richieste di consenso scadute o duplicate si risolvono come negate anziché bloccare il flusso OAuth.

Il selettore ha un piccolo confine di fiducia tutto suo: un client può suggerire un profilo nell'URL di autorizzazione, ma il grant è coniato da ciò che l'utente ha effettivamente selezionato, validato rispetto allo snapshot che gli è stato mostrato — un suggerimento falsificato non può legare un profilo che non è mai comparso sullo schermo.
Siamo altrettanto espliciti su ciò che il consenso non ti concede: l'autorizzazione è solo di scope, approvata una volta per connessione. I prompt di permesso per chiamata dell'assistente dentro l'app non si applicano al traffico MCP — un token a scope pieno trafugato può scrivere (nello staging) senza che un umano veda ogni chiamata. Durate brevi dei token, revoca per client, una traccia di audit sempre attiva che marca ogni azione originata da MCP e il read-only come offerta di default sono le mitigazioni. Metterlo nero su bianco come rischio residuo è stato più utile che fingere che il modale di consenso lo avesse chiuso.
Guardrail per il lavoro headless
Un agente esterno non ha renderer, nessun popup di conferma, nessun utente che guarda. Tutto ciò che è interattivo aveva bisogno di un equivalente headless:
- Clamp della licenza in tempo reale, a ogni chiamata. Il grant memorizza lo scope che l'utente ha approvato; lo scope effettivo è derivato a ogni chiamata di strumento dallo stato attuale della licenza dell'app — una licenza read-only restringe un grant pieno, uno stato disconnesso rifiuta con 401 prima di qualsiasi ramo del token (così il dev token non può aggirarlo), e una riattivazione a metà sessione riallarga senza un nuovo consenso. Un'asimmetria deliberata: l'esportazione dentro l'app resta disponibile sotto una licenza read-only (i tuoi dati sono i tuoi dati), ma l'esportazione di massa headless tramite MCP è ristretta — un diritto di portabilità interattivo e un canale di uscita guidato da un agente sono superfici di fiducia diverse.
- Budget di scansione invece di popup di conferma. Nell'app, una lettura dell'intera tabella chiede prima. In modalità headless, un budget di elementi/byte/durata limita ogni scansione, e l'SQL sulle tabelle scorre attraverso un motore su disco anziché un limite in memoria.
- La disconnessione interrompe onestamente. Se il client si stacca a metà scansione, il server interrompe al confine della pagina successiva — mai a metà istruzione — e smaltisce lo stato temporaneo.
- L' si completa via cavo. Quando le credenziali AWS richiedono un codice MFA e il client supporta l'MCP elicitation, il server elicita il codice a 6 cifre attraverso l'interfaccia dell'agente stesso — Claude Code chiede all'umano — e riprova. L'SSO è escluso deliberatamente da questo percorso: ha bisogno di un device flow via browser, e fingere il contrario sarebbe solo un messaggio di errore peggiore. Le chiamate concorrenti che necessitano delle stesse credenziali si coalescono in un unico prompt.
Esporre un database tramite MCP è sicuro, in generale?
La nostra risposta onesta: solo se il server è progettato attorno all'assunzione che l'agente sia nel migliore dei casi confuso e nel peggiore avversario. La rende l'intento di un agente inaffidabile, ed è per questo che l'architettura non fa mai affidamento sul buon comportamento dell'agente: le credenziali restano nell'app, le scritture atterrano sempre e solo in una area di staging revisionabile, gli scope limitano ciò che una chiamata può fare a prescindere da ciò che il modello chiede, e ogni azione è sottoposta ad audit. La guida lato connessione copre cosa significa nella pratica per ciascun client.
Cosa pretendere da qualsiasi server MCP che tocca il tuo database
- Dove risiedono le credenziali — nel processo dell'agente, o dietro il server?
- C'è un consenso per connessione con scope reali, o un unico file di configurazione concede tutto?
- Una scrittura può essere eseguita senza un passaggio di revisione umana?
- L'identità (profilo, region) è fissata al consenso, o risolta dai default al momento della chiamata?
- Cosa succede con un token malformato o revocato — fallimento netto, o un ripiego?
- C'è una traccia di audit che distingue le azioni originate dall'agente?
- Cosa limita una scansione fuori controllo, e cosa succede quando il client si disconnette a metà operazione?
Se un server non sa rispondere a questo, l'integrazione conveniente è quella rischiosa. Per dimensionare quanto ti costerebbe davvero una scansione illimitata, il calcolatore dei prezzi fa riflettere — e se preferisci vedere la versione sicura in funzione piuttosto che leggerne, scarica DynoTable, attiva il server MCP e connetti un client a scope read-only in meno di un minuto.


