142 lines
9.8 KiB
Markdown
142 lines
9.8 KiB
Markdown
# 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 <schlüssel>` | 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/<typ>/` 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.<typ>`. 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 `<kurzname>__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.
|