# db-mcp – lesende MCP-Server für Datenbanken Ein gemeinsamer Kern (`dbmcp.core`), darauf je Datenbanktyp ein eigener Dienst mit eigenem Image: **`db-mcp-postgres`** (`dbmcp.postgres`) und **`db-mcp-oracle`** (`dbmcp.oracle`), weitere wie MS SQL mit derselben Schnittstelle. Der Name ist bewusst neutral; jeder MCP-Client kann die Dienste nutzen, Bifröst ist einer davon. Stand: 25.10.2026. In Bifröst sind Datenbanken MCP-Verbindungen eines Workspace über diese Dienste (siehe [Einbindung in Bifröst](#einbindung-in-bifröst)). ## Aufteilung | Bifröst (bzw. ein anderer Client) | DB-Dienst | |---|---| | Verbindungen je Workspace, Zugangsdaten (verschlüsselt), Bestätigung der Sichtbarkeit, Rechte, Regeln, Freigaben | Datenbanktreiber, SQL-Dialekt in der Anleitung (`instructions`) | | Audit-Log der Nutzer, Tabellenansicht, CSV, Diagramm | Nur lesen durchsetzen, Grenzen, Audit-Log der Abfragen | | Schickt Zugangsdaten je Aufruf als Header | Speichert keine Zugangsdaten | Andere MCP-Clients (OpenCode, Claude Desktop, eigene Agenten) können den Dienst direkt nutzen. Sie tragen Schlüssel und Zugangsdaten als Header in ihrer Konfiguration ein und sind selbst für die Kontrolle zuständig. ## Tools Alle Dienste bieten dieselben Tools mit denselben Parametern, alle gekennzeichnet als nur lesend (`readOnlyHint`): | Tool | Parameter | Ergebnis | |---|---|---| | `check_connection` | – | Version, sichtbare Tabellen, Zahl der Objekte mit Schreibrecht (empfohlen: 0) | | `list_tables` | – | `schema`, `name`, `type` (`table`/`view`), `rows_estimate`, `comment` | | `describe_table` | `table` (gern `schema.tabelle`) | Spalten (`name`, `type`, `nullable`, `comment`) und unter `sample` die ersten Zeilen | | `query` | `sql` | Genau eine lesende Abfrage, Zeilen begrenzt | Jedes Ergebnis kommt als Text für das Modell (Markdown-Tabelle) und strukturiert (`structuredContent`): ```json {"columns": ["id", "name"], "rows": [[1, "Kunde 1"]], "row_count": 1, "truncated": false} ``` Werte sind JSON: Zahlen (auch `numeric`, außer bei mehr als 15 Stellen als Text), Datum/Zeit als ISO-Text, Binärdaten als `base64:…` (gekürzt). ## Zugangsdaten je Aufruf (Header) | Header | Pflicht | Bedeutung | |---|---|---| | `Authorization: Bearer ` | ja | Schlüssel des Clients (`DBMCP_API_KEYS`) | | `X-DB-Host` | ja | Muss in `DBMCP_ALLOWED_HOSTS` stehen | | `X-DB-Port` | nein | Standard des Treibers (PostgreSQL 5432, Oracle 1521) | | `X-DB-Database`, `X-DB-User` | ja | | | `X-DB-Password` | nein | | | `X-DB-SSLMode` | nein | `verify-full` (Standard), `require`; `prefer`/`disable` nur für lokale Hosts. Bei Oracle: `require`/`verify-full` = TCPS, `verify-full` prüft zusätzlich den Hostnamen im Zertifikat | Bei Oracle steht in `X-DB-Database` der **Service-Name** (z. B. `ORCLPDB1`). Die Zugangsdaten gehören nie in Tool-Argumente: Sonst stünden sie im Prompt und im Verlauf eines Chats. ## Sicherheit - **Nur lesen, mehrfach:** DB-Nutzer mit reinen Leserechten (prüft `check_connection`), Verbindung mit `default_transaction_read_only`, schreibgeschützte Transaktion mit Rollback, genau eine Anweisung (serverseitiger Cursor), Prüfung des ersten Schlüsselworts. - **Oracle:** schreibgeschützte Transaktion (`SET TRANSACTION READ ONLY`) mit Rollback; oracledb führt je Aufruf nur eine Anweisung aus; PL/SQL-Blöcke schließt das erste Schlüsselwort aus, PL/SQL im `WITH` (`WITH FUNCTION`) wird abgewiesen, weil eine Funktion mit autonomer Transaktion sonst schreiben könnte. Vorhandene Funktionen mit EXECUTE-Recht kann eine Abfrage aufrufen; darum ist ein Nutzer nur mit `CREATE SESSION` und SELECT-Rechten hier besonders wichtig. `check_connection` meldet auch systemweite Rechte (`INSERT ANY TABLE` …). - **Grenzen:** Zeitlimit in der Datenbank und zusätzlich vom Dienst, Zeilenlimit, Länge der Abfrage. - **Nur freigegebene Hosts** (`DBMCP_ALLOWED_HOSTS`, exakt oder als Endung `.db.intern`): Der Dienst ist kein Sprungbrett ins Netz (SSRF). - **Anmeldung:** jeder Aufruf mit Schlüssel; nur `/live` und `/ready` ohne. - **Audit-Log:** eine JSON-Zeile je Aufruf auf stdout mit Client, Datenbank (ohne Passwort), Tool, Abfrage, Zeilen, Dauer, Ergebnis. - **Netz auf OpenShift:** Ingress nur von Bifröst, Egress nur zu den Datenbank-Servern (siehe `deploy/`). ## Einstellungen | Variable | Standard | Bedeutung | |---|---|---| | `DBMCP_API_KEYS` | – (Pflicht) | `name:geheimnis,…`; Geheimnis mindestens 16 Zeichen; der Name erscheint im Audit-Log | | `DBMCP_ALLOWED_HOSTS` | leer | Erlaubte Datenbank-Hosts, kommagetrennt | | `DBMCP_ALLOW_LOCAL` | `false` | `localhost` erlauben (nur Entwicklung) | | `DBMCP_QUERY_TIMEOUT_SECONDS` | 15 | Zeitlimit je Abfrage | | `DBMCP_CONNECT_TIMEOUT_SECONDS` | 5 | Zeitlimit für den Verbindungsaufbau | | `DBMCP_MAX_ROWS` | 500 | Höchstzahl Zeilen je Ergebnis | | `DBMCP_HTTP_HOSTS` | leer | Erlaubte Host-Header (Schutz vor DNS-Rebinding); leer = keine Prüfung | | `DBMCP_BIND`, `DBMCP_PORT` | `0.0.0.0`, 8080 | Adresse des Dienstes | | `DBMCP_INSECURE_NO_AUTH` | `false` | Nur Entwicklung: ohne Schlüssel starten | ## Lokal starten und testen Der Dienst läuft in der Entwicklung mit der Umgebung von Bifröst (alle Pakete sind dort schon installiert): ```bash cd services/db-mcp DBMCP_API_KEYS="dev:dev-schluessel-12345678" DBMCP_ALLOW_LOCAL=true DBMCP_PORT=8791 \ ../../.venv/bin/python -m dbmcp.postgres ../../.venv/bin/python -m pytest -q -p no:cacheprovider # braucht das Test-Postgres auf Port 55433 ``` Oracle zum Testen (Oracle Database Free im Container, ca. 6 GB; die Tests nutzen nur SQL, das es in 19c schon gibt, und werden übersprungen, wenn der Container nicht läuft): ```bash cd ../.. && docker compose --profile oracle up -d oracle # 127.0.0.1:55434, Service FREEPDB1, system/aiw cd services/db-mcp DBMCP_API_KEYS="dev:dev-schluessel-12345678" DBMCP_ALLOW_LOCAL=true DBMCP_PORT=8792 \ ../../.venv/bin/python -m dbmcp.oracle URL=http://127.0.0.1:8792/mcp DBPORT=55434 DBNAME=FREEPDB1 DBUSER=system scripts/dbcall.sh check ``` Bequemer mit `scripts/dbcall.sh` (fragt standardmäßig die lokale Bifröst-Datenbank ab, Zugangsdaten über Umgebungsvariablen überschreibbar): ```bash scripts/dbcall.sh check scripts/dbcall.sh tables scripts/dbcall.sh describe public.workspaces scripts/dbcall.sh query "SELECT count(*) FROM users" DBPASS=falsch scripts/dbcall.sh check # Fehlerfall ``` Aufruf ohne MCP-Client, z. B. mit curl (der Dienst ist zustandslos, eine Sitzung ist nicht nötig): ```bash curl -s -X POST http://127.0.0.1:8791/mcp \ -H "Authorization: Bearer dev-schluessel-12345678" -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: 2025-06-18" \ -H "X-DB-Host: 127.0.0.1" -H "X-DB-Port: 55433" -H "X-DB-Database: aiw" -H "X-DB-User: aiw" \ -H "X-DB-Password: aiw" -H "X-DB-SSLMode: disable" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query","arguments":{"sql":"SELECT 1"}}}' ``` ## Image und OpenShift - `Containerfile.postgres`, `Containerfile.oracle`: UBI-Image mit Python 3.12 und nur dem jeweiligen Treiber, läuft ohne root mit zufälliger UID. Oracle braucht keinen Oracle-Client (Thin-Modus). - `deploy/openshift-postgres.yaml`, `deploy/openshift-oracle.yaml`: Beispiel mit Secret, Deployment (zwei Replikas, Probes, Limits, eingeschränkter `securityContext`), Service und NetworkPolicies für Ingress und Egress. Braucht keine Cluster-Admin-Rechte. Der Dienst ist zustandslos (`stateless_http`): Mehrere Pods brauchen keine gemeinsame Sitzung. ## Weitere Datenbanktypen Neues Paket `dbmcp//` mit einem Treiber nach `dbmcp.core.driver` (`session()` mit `check`, `tables`, `describe`, `query`, dazu `name`, `key`, `default_port`, `starts`), einem `__main__.py`, einer Extra-Abhängigkeit in `pyproject.toml` und einem eigenen `Containerfile.`. Kern, Tools, Header, Ergebnisformat und Audit-Log bleiben gleich. Tests nach dem Muster von `tests/test_oracle.py` mit einer Test-Datenbank im Container. ## Offene Punkte Oracle - **Native Network Encryption:** Verlangt der Server die Oracle-eigene Verschlüsselung (`SQLNET.ENCRYPTION_SERVER = required`) statt TLS, reicht der Thin-Modus nicht (Fehler DPY-3001). Dann TCPS am Server oder ein Image mit Oracle Instant Client (Thick-Modus). - **Synonyme:** `list_tables` zeigt Tabellen und Views, keine Synonyme. Abfragen über Synonyme funktionieren. ## Einbindung in Bifröst - In Bifröst heißt ein Dienst *Datenbank-Treiber*. Ein Plattform-Admin registriert ihn einmal unter *Administration → Datenbank-Treiber*: URL `http://db-mcp-oracle:8080/mcp`, Schlüssel des Dienstes aus `DBMCP_API_KEYS` (öffnet nur den Dienst, keine Datenbank). Dann *Tools abrufen* und die vier Tools einstufen (Vorschlag: lesend, erlaubt). - Workspace-Admins legen unter *Einstellungen → MCP-Server → Datenbank hinzufügen* Verbindungen an: Datenbanktyp (einer der Treiber), Kurzname, Host, Port, Datenbank, Nutzer, Passwort, TLS. Bifröst prüft den Host zusätzlich gegen `AIW_DB_ALLOWED_HOSTS` und speichert das Passwort verschlüsselt. - Die Tools der Verbindung heißen `__list_tables`, `…__describe_table`, `…__query`; sie haben die Einstufung des Treibers (gleicher Fingerabdruck) und werden nicht einzeln eingestuft. Speichern und *Testen* rufen `check_connection`. - Bei jedem Aufruf hängt Bifröst Schlüssel und Zugangsdaten als Header an (`Authorization`, `X-DB-*`); das Modell sieht sie nicht. Den SQL-Dialekt liest das Modell aus der Anleitung des Dienstes. - `structuredContent` mit `columns` und `rows` wird zur Tabellenansicht (Tabelle, CSV, Diagramm); das gilt für jedes MCP-Tool, nicht nur für Datenbanken. - Bestätigung der Sichtbarkeit, Vorgaben je Verbindung (z. B. `prod__query` nur mit Freigabe) und Audit-Log bleiben in Bifröst. - Lokal startet `scripts/dev.sh` den Dienst für PostgreSQL auf Port 8791, mit `--oracle` den für Oracle auf Port 8792, und zeigt URL und Token.