Was es brauchte, um einen sicheren DynamoDB-MCP-Server zu bauen
Das macht es trivial, einem deine Datenbank zu überlassen. Das ist das Problem. Die meisten DynamoDB-MCP-Server sind ein dünner Prozess, der deine rohen AWS-Zugangsdaten hält, mit einem Schreibpfad und ohne Prüfschritt — wir haben aus Sicht der Nutzer darüber geschrieben, warum dieses Muster riskant ist und wie man sicher wählt.
Dieser Beitrag ist die andere Seite: was es tatsächlich brauchte, um die sichere Version zu bauen. DynoTable ist ein Desktop-DynamoDB-Client, dessen MCP-Server sein abgesichertes KI-Toolkit für externe Agenten — Claude Code, Cursor, Codex — bereitstellt, ohne dass der Agent je AWS-Zugangsdaten berührt. Dorthin zu kommen bedeutete, einen -Authorization-Server in einer Electron-App auszuliefern, einen Identitätstyp zu entwerfen, der Verwechslungen von Zugangsdaten undarstellbar macht, und mehr als einmal zu entscheiden, dass die bequeme Voreinstellung die falsche war.
Wenn du einen MCP-Server um irgendetwas Sensibles herum baust, wirst du diesen Entscheidungen ebenfalls begegnen.
Loopback-HTTP ist nicht automatisch sicher
Der Server bindet an 127.0.0.1, was sicher klingt und nicht ausreicht. Jede
Webseite, die du besuchst, kann Requests an localhost versuchen, und DNS-Rebinding
lässt eine bösartige Seite diese mit einem plausiblen Host verkleiden. Die
erste Schicht ist daher ein Origin-Guard: jeden Request mit einem echten
Webseiten-Origin ablehnen, Nicht-Loopback-Host-Header ablehnen — aber
Requests ganz ohne Origin zulassen, weil legitime CLI-Clients wie
Claude Code keinen senden.
Dieser letzte Punkt ist der ehrliche Teil: Ein Origin-Guard blockiert nur die Angriffsklasse aus dem Browser. Er kann niemanden authentifizieren. Das eigentliche Tor ist der Bearer Token — was die Frage aufwirft, woher die Token kommen.
Wir haben einen OAuth-2.1-Authorization-Server in der App ausgeliefert
MCPs Auth-Konzept für Remote-Server ist OAuth 2.1, und für einen lokalen Desktop-Server gibt es keinen vorgelagerten Identity Provider, auf den man sich stützen kann. Also ist die App der Authorization-Server: dynamische Client-Registrierung, (S256), Authorization Codes, Refresh Tokens, Widerruf und die Metadaten-Dokumente nach RFC 8414 / RFC 9728, über die Clients das alles entdecken.
Regeln, die wir früh gesetzt und beibehalten haben:
- Das Protokoll nicht selbst nachbauen. Das MCP-SDK liefert die komplette AS-Oberfläche als Router; wir implementieren nur den Storage-/Policy-Provider darunter. Eine Konsequenz, die wir akzeptiert haben: Unser MCP-Framework spricht intern einen HTTP-Stack und der SDK-Router einen anderen, deshalb läuft der AS als zweiter Loopback-Listener, wobei der Resource-Server die Clients per Protected-Resource-Metadaten dorthin verweist. Zwei Listener sind ein klar umrissener, expliziter Preis; PKCE und Token-Grammatik von Hand nachzubauen ein unbegrenzter.
- Redirects sind nur Loopback (gemäß RFC 8252 für native Apps), und ein ungültiger Redirect erhält nie einen Error-Redirect — er bekommt ein direktes 400, sodass die URI eines Angreifers nie ein Ziel ist.
- Authorization Codes sind einmal verwendbar — ein erneut abgespielter Code ist ein ungültiger Grant, Punkt.
- Refresh kann den Scope verengen, aber nie erweitern, und es gibt dieselbe Identity-Payload wortgetreu erneut aus — ein aufgefrischter Token kann sich nicht klammheimlich eine andere Region aneignen.
- Der Widerruf wird bei jedem Request neu geprüft, nicht beim Verbinden gecacht — ein Klick auf „Widerrufen“ in den Einstellungen killt den nächsten Aufruf des Clients.
Entwicklungs-Builds nutzen stattdessen einen einfacheren Auth-Pfad, der so gebaut ist, dass er in einem Release-Binary nicht existieren kann — ein nicht authentifizierter Full-Scope-Token in ausgelieferter Software wäre eine lokale Hintertür hinter einem Origin-Guard, der bewusst Requests ohne Origin zulässt.
Eine Verbindung, eine AWS-Identität — als Ganzes getragen, nie geraten
Die zentrale Sicherheitseigenschaft: Ein externer Agent verbindet sich mit einem Profil, nicht mit „DynoTable“. Jede Verbindung ist an ein AWS-Profil und eine Region gebunden, gestützt auf einen eigenen, nicht geteilten DynamoDB-Client, sodass zwei Agenten auf verschiedenen Profilen nicht ineinander überlaufen können.
Die Bindung begann als lose Felder — awsProfile, region, profileId, ein
optionaler lokaler Port —, die über jeden Datensatz auf dem Pfad neu deklariert
wurden. Sie ist jetzt ein Identitätstyp als Discriminated Union mit zwei
Zweigen: online (Profil + Region + ID) und offline (lokaler Port + ID, für
DynamoDB Local-Profile). Das Offline-Sein ist der
Diskriminant selbst, sodass eine Online-Identität ohne Zugangsdaten oder eine
Offline-Identität mit Region ein Typfehler ist und keine Laufzeitüberraschung.
Dieser eine Typ wandert unversehrt durch Consent → Token → Grant → jeden Request.
Zwei Konsequenzen, die wir als tragend betrachten:
- Die Region wird beim Consent eingefroren. Der Nutzer genehmigt „Profil X in
Region Y“, und dieses Tupel wird auf dem Token fixiert — es übersteht den
Refresh und wird auf dem Hot Path nie erneut aus veränderlichem Zustand
aufgelöst. Die Region des Profils zu ändern, erfordert erneuten Consent. Die
Alternative — die Region pro Request aufzulösen oder auf einen Standard
zurückzufallen — ist der Weg, auf dem ein Agent klammheimlich
us-east-1abfragt. - Fehlerhafte Token sind ein 401, nie eine Erzwingung. Wenn die
Identity-Claims eines Tokens nicht zu einem gültigen Zweig parsen, ist der
Request nicht autorisiert. Kein
'default'-Profil, keine Standard-Region, kein leerer String. Überall, wo der alte Code geraten hätte, gibt es jetzt einen harten Fehler.
Consent ist eine Produktfläche, kein Dialogfenster
Die erste Consent-Abfrage war die plattformeigene Message-Box. Sie fror den Electron-Main-Prozess ein, solange sie offen war, passte nicht zum Design-System der App und ließ sich für die Doku nicht einmal per Screenshot festhalten. Jetzt ist es ein In-App-Modal: der Name des sich verbindenden Clients, ein Profil-Picker über genau die Profile, die der Nutzer freigeben wollte, und drei Scopes, angeboten vom engsten zuerst — schreibgeschützt, lesen + stagen, voll. Abgelaufene oder doppelte Consent-Anfragen enden als abgelehnt, statt den OAuth-Flow hängen zu lassen.

Der Picker hat eine kleine eigene Vertrauensgrenze: Ein Client darf ein Profil in der Authorize-URL andeuten, aber der Grant wird aus dem geprägt, was der Nutzer tatsächlich ausgewählt hat, geprüft gegen den Snapshot, der ihm gezeigt wurde — ein gefälschter Hint kann kein Profil binden, das nie auf dem Bildschirm war.
Wir sind ebenso deutlich darin, was Consent dir nicht gibt: Die Autorisierung ist scope-only und wird einmal pro Verbindung genehmigt. Die Per-Aufruf-Berechtigungsabfragen des In-App-Assistenten gelten nicht für MCP-Traffic — ein geleakter Full-Scope-Token kann schreiben (ins Staging), ohne dass ein Mensch jeden Aufruf sieht. Kurze Token-Lebensdauern, Widerruf pro Client, ein stets aktives Audit-Log, das jede von MCP ausgehende Aktion markiert, und schreibgeschützt als Standardangebot sind die Gegenmaßnahmen. Das als Restrisiko aufzuschreiben war nützlicher, als so zu tun, als hätte das Consent-Modal es geschlossen.
Leitplanken für Headless-Betrieb
Ein externer Agent hat keinen Renderer, keine Bestätigungs-Popups, keinen zuschauenden Nutzer. Alles Interaktive brauchte ein Headless-Äquivalent:
- Live-Lizenz-Clamping, pro Aufruf. Der Grant speichert den vom Nutzer genehmigten Scope; der effektive Scope wird bei jedem Tool-Aufruf aus dem aktuellen Lizenzstatus der App abgeleitet — eine schreibgeschützte Lizenz klemmt einen vollen Grant herunter, ein ausgeloggter Status lehnt mit 401 ab, bevor irgendein Token-Zweig greift (sodass der Dev-Token ihn nicht umgehen kann), und eine Reaktivierung mitten in der Sitzung weitet wieder auf, ohne erneuten Consent. Eine bewusste Asymmetrie: Der In-App-Export bleibt unter einer schreibgeschützten Lizenz verfügbar (deine Daten sind deine Daten), aber der Headless-Massenexport über MCP wird abgeklemmt — ein interaktives Portabilitätsrecht und ein agentengetriebener Egress-Kanal sind unterschiedliche Vertrauensflächen.
- Scan-Budgets statt Bestätigungs-Popups. In der App fragt ein Read über die ganze Tabelle zuerst nach. Headless begrenzt ein Items-/Bytes-/Dauer-Budget jeden Scan, und SQL über Tabellen streamt durch eine plattengestützte Engine statt über ein In-Memory-Limit.
- Ein Verbindungsabbruch bricht ehrlich ab. Wenn der Client mitten im Scan abbricht, bricht der Server an der nächsten Seitengrenze ab — nie mitten in einem Statement — und verwirft den temporären Zustand.
- wird über die Leitung abgeschlossen. Wenn AWS-Zugangsdaten einen MFA-Code brauchen und der Client MCP-Elicitation unterstützt, holt der Server den 6-stelligen Code über die eigene UI des Agenten ein — Claude Code fragt den Menschen — und versucht es erneut. SSO ist von diesem Pfad bewusst ausgenommen: Es braucht einen Browser-Device-Flow, und etwas anderes vorzugeben wäre nur eine schlechtere Fehlermeldung. Gleichzeitige Aufrufe, die dieselben Zugangsdaten brauchen, verschmelzen zu einer Abfrage.
Ist es überhaupt sicher, eine Datenbank über MCP bereitzustellen?
Unsere ehrliche Antwort: nur wenn der Server auf der Annahme aufgebaut ist, dass der Agent bestenfalls verwirrt und schlimmstenfalls feindselig ist. macht die Absicht eines Agenten nicht vertrauenswürdig — deshalb verlässt sich die Architektur nie darauf, dass sich der Agent benimmt: Zugangsdaten bleiben in der App, Schreibvorgänge landen ausschließlich in einem überprüfbaren Staging-Bereich, Scopes begrenzen, was ein Aufruf tun kann, unabhängig davon, was das Modell verlangt, und jede Aktion wird auditiert. Der Leitfaden für die Verbindungsseite behandelt, was das in der Praxis für jeden Client bedeutet.
Was du von jedem MCP-Server verlangen solltest, der deine Datenbank berührt
- Wo liegen die Zugangsdaten — im Prozess des Agenten oder hinter dem Server?
- Gibt es Consent pro Verbindung mit echten Scopes, oder gewährt eine einzige Konfigurationsdatei alles?
- Kann ein Schreibvorgang ohne menschlichen Prüfschritt ausgeführt werden?
- Wird die Identität (Profil, Region) beim Consent fixiert oder zur Aufrufzeit aus Standardwerten aufgelöst?
- Was passiert bei einem fehlerhaften oder widerrufenen Token — harter Fehler oder ein Fallback?
- Gibt es ein Audit-Log, das von Agenten ausgehende Aktionen unterscheidet?
- Was begrenzt einen ausufernden Scan, und was passiert, wenn der Client mitten in einer Operation die Verbindung trennt?
Wenn ein Server diese Fragen nicht beantworten kann, ist die bequeme Integration die riskante. Um abzuschätzen, was dich ein unbegrenzter Scan tatsächlich kosten würde, ist der Preis-Rechner ernüchternd — und wenn du die sichere Version lieber laufen sehen als darüber lesen willst, lade DynoTable herunter, schalte den MCP-Server ein und verbinde einen Client mit schreibgeschütztem Scope in unter einer Minute.


