Skip to main content
Glama
AndiAtom

MariaDB MCP Server

by AndiAtom
README.md
# MariaDB MCP Server mit streamable HTTP für Open-WebUI

Ein **read-only** MCP Server, der als Schnittstelle zwischen einer MariaDB Datenbank und Open-WebUI dient. Der Server erlaubt **ausschließlich lesende Abfragen** und blockiert alle Schreiboperationen wie INSERT, UPDATE, DELETE, CREATE, ALTER, DROP usw.

> **Hinweis:** Der Server verwendet `mysql-connector-python`, der vollständig mit MariaDB kompatibel ist und keine externen Systembibliotheken benötigt. Der Server ist **MCP-kompatibel** und implementiert die notwendigen Endpunkte für Open-WebUI. Aktuellste Version verwendet Python 3.12 als Base Image.

---

## :rocket: Schnellstart

### Mit Docker (empfohlen)

```bash
# Klone das Repository
git clone https://github.com/AndiAtom/mariadb-mcp-strhttp.git
cd mariadb-mcp-strhttp

# Starte mit Docker Compose (enthält MariaDB + MCP Server)
docker-compose up -d

# Der Server ist jetzt unter http://localhost:8000 verfügbar
```

### Ohne Docker

```bash
# Installiere Abhängigkeiten
pip install -r requirements.txt

# Starte den Server
python -m src.server

# Oder direkt
python src/server.py
```

---

## :gear: Konfiguration

### Umgebungsvariablen

Der Server wird über **Umgebungsvariablen** konfiguriert. Die mitgelieferte `config.json` dient als Referenz für die möglichen Werte und wird nicht automatisch vom Server eingelesen.

#### Datenbank-Konfiguration

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `DB_HOST` | MariaDB Hostname | `localhost` | `192.168.1.100` |
| `DB_PORT` | MariaDB Port | `3306` | `3306` |
| `DB_USER` | MariaDB Benutzername | `mcpuser` | `mcp_user` |
| `ALLOW_DB_ROOT` | `DB_USER=root` beim Start erlauben (nur lokale Entwicklung) | `false` | `true` |
| `DB_PASSWORD` | MariaDB Passwort | `""` | `securepassword` |
| `DB_DATABASE` | Standard-Datenbank | `None` | `mydatabase` |
| `DB_TIMEOUT` | Timeout für Datenbankabfragen (Sekunden) | `30` | `60` |

#### Server-Konfiguration

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `SERVER_HOST` | Server Host | `0.0.0.0` | `0.0.0.0` |
| `SERVER_PORT` | Server Port | `8000` | `8000` |
| `LOG_LEVEL` | Log-Level | `info` | `debug` |

#### API-Token-Authentifizierung

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `API_TOKEN` | Einzelner API-Token | `None` | `my-secret-token` |
| `API_TOKENS` | Mehrere API-Tokens (komma-separiert) | `None` | `token1,token2` |
| `API_TOKEN_FILE` | Pfad zur Token-Datei | `None` | `/app/tokens.json` |
| `DISABLE_API_AUTH` | Authentifizierung deaktivieren | `false` | `true` |
| `API_HEADER_NAME` | Name des Authorization Headers | `Authorization` | `X-API-Key` |
| `API_QUERY_PARAM` | Name des Query-Parameters | `api_key` | `token` |

> **Hinweis:** Der Token wird **nicht** mehr aus dem Request-Body extrahiert. Dies verhindert einen Doppelkonsum des Bodies durch die Auth-Middleware. Verwende stattdessen den `Authorization`-Header oder den `api_key`-Query-Parameter.

#### CORS

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `CORS_ALLOWED_ORIGINS` | Erlaubte CORS-Origins (komma-separiert) | `""` (keine) | `https://openwebui.example.com` |

> **Sicherheit:** `allow_origins=["*"]` mit `allow_credentials=True` ist eine bekannte Fehlkonfiguration. Ohne Konfiguration von `CORS_ALLOWED_ORIGINS` sind keine Cross-Origin-Requests mit Credentials möglich.

#### Datenbank-Zugriffskontrolle

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `ALLOWED_DATABASES` | Positiv-Liste erlaubter Datenbanken (komma-separiert) | `""` (nicht gesetzt) | `testdb,analytics` |

> **Sicherheit:** System-Schemata (`mysql`, `information_schema`, `performance_schema`, `sys`) sind immer gesperrt. Ohne `ALLOWED_DATABASES` sind alle nicht-System-Schemata erlaubt; mit gesetzter Variable nur die gelisteten.

#### API-Dokumentation

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `PUBLIC_DOCS` | `/docs` und `/redoc` ohne Auth freischalten | `false` | `true` |

> **Sicherheit:** Die API-Dokumentation ist standardmäßig auth-pflichtig, um kein Informationsleck zu erzeugen. Nur in vertrauenswürdigen internen Umgebungen auf `true` setzen.

#### Request-Limitierung & Security-Header

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `MAX_REQUEST_BODY_BYTES` | Maximale Request-Body-Größe in Bytes (DoS-Schutz) | `1048576` (1 MiB) | `2097152` |

> **Sicherheit:** Der Server setzt zusätzlich Standard-Security-Header auf jede Antwort: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, `Cache-Control: no-store`. `Strict-Transport-Security` (HSTS) wird nur bei HTTPS-Requests gesetzt. Ein `DB_USER=root` wird beim Start abgewiesen (außer `ALLOW_DB_ROOT=true`); Auth aktiviert ohne konfigurierte Tokens führt zu einem Startabbruch (Fail-Closed).

#### Rate Limiting

| Variable | Beschreibung | Standardwert | Beispiel |
|----------|--------------|--------------|----------|
| `RATE_LIMITING_ENABLED` | Rate Limiting aktivieren | `true` | `false` |
| `RATE_LIMIT_REQUESTS_PER_MINUTE` | Anfragen pro Minute | `100` | `200` |
| `RATE_LIMIT_BURST_REQUESTS` | Burst-Anfragen | `10` | `20` |
| `RATE_LIMIT_WHITELIST` | Whitelisted Pfade | `/health,/`, etc. | `/health,/info` |

### Referenzkonfiguration (`config.json`)

Die folgende `config.json` zeigt die Struktur der Konfigurationswerte. Sie wird **nicht** vom Server automatisch geladen; alle Werte werden stattdessen über die oben genannten Umgebungsvariablen gesetzt.

```json
{
  "server": {
    "host": "0.0.0.0",
    "port": 8000,
    "log_level": "info"
  },
  "database": {
    "host": "localhost",
    "port": 3306,
    "user": "mcpuser",
    "password": "securepassword",
    "database": "mydatabase",
    "timeout": 30
  },
  "security": {
    "read_only": true,
    "block_write_operations": true,
    "validate_queries": true
  },
  "authentication": {
    "enabled": true,
    "type": "api_token",
    "tokens": ["token1", "token2"],
    "token_file": null,
    "header_name": "Authorization",
    "query_param_name": "api_key"
  },
  "rate_limiting": {
    "enabled": true,
    "requests_per_minute": 100,
    "burst_requests": 10,
    "whitelist": ["/health", "/", "/docs", "/openapi.json", "/redoc"]
  }
}
```

> **Hinweis:** Maßgeblich sind die Umgebungsvariablen aus den Tabellen oben. Diese `config.json` ist lediglich eine Referenz.

---

## :key: API-Token-Authentifizierung

Der Server unterstützt optionale API-Token-Authentifizierung, um den Zugriff auf die API zu schützen.

### Token-Generierung

Das Projekt enthält ein Skript `generate_token.py` zur Generierung sicherer API-Tokens:

```bash
# Einzelnen Token generieren
python generate_token.py

# Mehrere Tokens generieren
python generate_token.py --num 5

# Token mit bestimmter Länge generieren (Standard: 32 Zeichen)
python generate_token.py --length 64

# Token in Datei speichern
python generate_token.py --file tokens.json

# Tokens nur anzeigen (nicht speichern)
python generate_token.py --no-file
```

### Authentifizierung aktivieren

Es gibt mehrere Möglichkeiten, die Authentifizierung zu konfigurieren:

#### 1. Einzelner Token über Umgebungsvariable

```bash
# In docker-compose.yml oder beim Starten
API_TOKEN=your-secure-token-here
```

#### 2. Mehrere Tokens über Umgebungsvariable (komma-separiert)

```bash
API_TOKENS=token1,token2,token3
```

#### 3. Token aus Datei laden

Erstelle eine JSON-Datei `tokens.json`:

```json
{
  "tokens": [
    "your-secure-token-1",
    "your-secure-token-2"
  ]
}
```

Oder eine einfache Textdatei (ein Token pro Zeile):
```
token1
token2
token3
```

Dann in docker-compose.yml:
```yaml
environment:
  - API_TOKEN_FILE=/app/tokens.json
volumes:
  - ./tokens.json:/app/tokens.json:ro
```

> **Hot-Reload:** Eine über `API_TOKEN_FILE` eingebundene Token-Datei wird bei Änderung (mtime) automatisch beim nächsten Request neu geladen. Token-Rotation ist damit ohne Server-Restart möglich. Die Prüfung erfolgt über `stat` und ist sehr billig; nur bei tatsächlicher Änderung wird die Datei gelesen.

> **Fail-Closed-Startup:** Ist die Authentifizierung aktiviert (Standard), aber es sind keine Tokens konfiguriert (`API_TOKEN`/`API_TOKENS`/`API_TOKEN_FILE`), bricht der Server den Start mit einem Fehler ab. Das verhindert sowohl einen versehentlich offenen als auch einen "abgeriegelten" (Silent-Death) Server. Für lokale Entwicklung `DISABLE_API_AUTH=true` setzen.

> **Konstanter Token-Vergleich:** Tokens werden über `secrets.compare_digest` validiert (konstante Zeit), um Timing-Seitenkanäle bei der Token-Enumeration zu vermeiden.

#### 4. Authentifizierung deaktivieren (nur für lokale Entwicklung)

```bash
DISABLE_API_AUTH=true
```

### Token verwenden

Es gibt drei Möglichkeiten, den Token zu übergeben:

#### 1. Authorization Header (empfohlen)

```bash
curl -X POST http://localhost:8000/query \
  -H "Authorization: Bearer your-secure-token" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT * FROM customers LIMIT 10"}'
```

#### 2. Query Parameter

```bash
curl -X POST http://localhost:8000/query?api_key=your-secure-token \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT * FROM customers LIMIT 10"}'
```

> **Hinweis:** Die Token-Übertragung im Request-Body wird aus Sicherheitsgründen nicht mehr unterstützt (Doppelkonsum des Bodies). Verwende Header oder Query-Parameter.

### Öffentliche Endpunkte

Die folgenden Endpunkte benötigen **keine** Authentifizierung:
- `GET /` - Server-Informationen
- `GET /health` - Health-Check
- `GET /openapi.json` - OpenAPI-Spezifikation (für Clients wie Open-WebUI)

> **Hinweis:** `/docs` und `/redoc` sind standardmäßig **auth-pflichtig** und können über `PUBLIC_DOCS=true` freigeschaltet werden.

Alle anderen Endpunkte erfordern einen gültigen API-Token, wenn die Authentifizierung aktiviert ist.

---

## :plug_socket: Standard-MCP-Clients (Spec 2026-07-28)

Seit v1.4.0 spricht `POST /mcp` zusätzlich zum Open-WebUI-Legacy-Format das
offizielle MCP-Protokoll nach **Spec 2026-07-28** (stateless core, JSON-RPC 2.0).
Beide Generationen bedient derselbe Endpunkt — ein valider JSON-RPC-2.0-Envelope
wandert in den Spec-Pfad, alles andere in den Legacy-Pfad.

Unterstützte RPCs: `server/discover`, `tools/list`, `tools/call`, `ping`.
Header-Pflichten: `MCP-Protocol-Version` (muss mit
`_meta['io.modelcontextprotocol/protocolVersion']` übereinstimmen) und
`Mcp-Method`; für `tools/call` zusätzlich `Mcp-Name` (Base64-Sentinel
`=?base64?...?=` wird dekodiert). Fehler: `-32020` HeaderMismatch,
`-32022` UnsupportedProtocolVersion, `-32601` mit HTTP 404 für unbekannte
Methoden, `202 Accepted` für Notifications. Tool-Fehler kommen als
Tool-Result mit `isError: true` (HTTP 200), Parameterfehler als `-32602`.

Beispiel mit dem offiziellen Python-SDK:

```python
from mcp.client.streamable_http import streamable_http_client
from mcp.client.session import ClientSession

async with streamable_http_client("http://localhost:8000/mcp") as streams:
    async with ClientSession(*streams) as session:
        result = await session.discover()   # statt initialize()
        session.adopt(result)
        tools = await session.list_tools()
        r = await session.call_tool("execute_query", {"query": "SELECT 1"})
```

REST-Endpunkte (`/query`, `/tables`, `/databases`, `/schema/{table}`, ...)
bleiben unverändert; die Authentifizierung (API-Token via Header/Query)
gilt weiterhin für alle Pfade.

---

## :desktop_computer: Open-WebUI Integration

### MCP Server in Open-WebUI hinzufügen

1. **Öffne Open-WebUI** (z.B. `http://localhost:8080`)
2. **Gehe zu Einstellungen** --> **MCP Server** oder **Externe Tool-Server**
3. **Klicke auf "Add MCP Server"** oder **"Neuer Server"**
4. **Füge folgende Konfiguration ein:**

```json
{
  "name": "MariaDB Read-Only",
  "type": "http",
  "url": "http://localhost:8000",
  "readOnly": true,
  "headers": {
    "Authorization": "Bearer your-api-token-here"
  },
  "capabilities": {
    "query": true,
    "stream": true,
    "validate": true,
    "list_resources": false,
    "read_resource": false
  },
  "timeout": 60
}
```

> **:bulb: Hinweis:** Open-WebUI erkennt automatisch den MCP-kompatiblen Endpunkt. Die URL kann einfach `http://localhost:8000` sein, der Server hat sowohl den Standard- als auch den `/mcp`-Endpunkt.

### Verbindung testen

Frage Open-WebUI:
```
"Was sind die Tabellen in der Datenbank?"
```

Erwartete Antwort: Eine Liste aller Tabellen aus deiner MariaDB.

---

## :shield: Sicherheitsfeatures

### Read-Only Implementierung

Der Server implementiert **mehrere Ebenen** von Read-Only-Schutz:

1. **DB-User-Ebene (primär):** Verwende einen dedizierten DB-User ohne Schreibrechte (`GRANT SELECT ON ...`). Dies ist die wichtigste Schutzmaßnahme.
2. **Session-Ebene:** `SET SESSION read_only=ON` wird auf **jeder Verbindung** des Pools gesetzt (Defense-in-Depth)
3. **Abfrage-Ebene:** Jede Abfrage wird vor der Ausführung auf Schreiboperationen geprüft (`is_read_only_query`)
4. **Identifier-Validierung:** Tabellen- und Datenbanknamen werden per Regex (`^[A-Za-z0-9_]+$`) validiert, bevor sie in SQL eingefügt werden (SQL-Injection-Schutz)
5. **Kommentar-Stripping:** Vor der Prüfung werden SQL-Kommentare entfernt. Dabei kommt ein **stack-basiertes** Verfahren zum Einsatz, das auch verschachtelte/gestaffelte Blockkommentare (``/* a /* b */ INSERT ... */``) korrekt nach MariaDB-Semantik entfernt. Ein nicht-greedy Regex würde hier das `INSERT` übersehen.

> **Hinweis:** Die frühere einzelne, global geteilte Verbindung wurde durch einen Connection-Pool ersetzt, der pro Request eine isolierte Verbindung öffnet. Das verhindert Race Conditions durch `USE`-Wechsel auf geteilten Verbindungen.

### Query Timeout Schutz

- **Standard-Timeout:** Alle Datenbankabfragen haben einen Standard-Timeout von 30 Sekunden
- **Individueller Timeout:** Kann pro Abfrage über den `timeout`-Parameter angepasst werden
- **Streaming-Limit:** Streaming-Abfragen sind auf 10.000 Zeilen begrenzt, um sehr große Resultsets zu verhindern
- **Konfigurierbar:** Timeout kann über die Umgebungsvariable `DB_TIMEOUT` oder in der Konfigurationsdatei angepasst werden

### Audit-Logging

Der Server schreibt strukturierte Audit-Ereignisse in den separaten Logger `audit` (unabhängig vom Anwendungs- und Access-Log, z. B. in eine Datei oder ein SIEM weiterleitbar). Jedes Ereignis ist eine JSON-Zeile mit:

- `event`: Art (`query.executed`, `query.denied`, `query.error`)
- `client_ip`: direkter Peer (nicht `X-Forwarded-For`, vertraut nur direkter Verbindung)
- `token_index`: Index des Tokens in der konfigurierten Menge (kein Token-Wert!)
- `database`, `query_preview` (max. 80 Zeichen), `valid`, `row_count`, `status_code`, `error`

> **Sicherheit:** Es werden keine vollständigen Queries und keine Token-Werte protokolliert. Der `token_index` ermöglicht eine eindeutige Client-Zuordnung ohne Token-Leak. Audit-Ereignisse werden im `/query`-Endpunkt bei Erlaubnis, Ablehnung und Fehler geschrieben.

### Blockierte Befehle

Der Server blockiert **alle** Schreiboperationen, einschließlich:

#### DDL (Data Definition Language)
- `CREATE` - Tabellen, Datenbanken, Indizes erstellen
- `ALTER` - Objekte ändern
- `DROP` - Objekte löschen
- `TRUNCATE` - Tabellen leeren
- `RENAME` - Objekte umbenennen

#### DML (Data Manipulation Language)
- `INSERT` - Daten einfügen
- `UPDATE` - Daten aktualisieren
- `DELETE` - Daten löschen
- `REPLACE` - Daten ersetzen
- `LOAD` - Daten laden
- `MERGE` - Daten zusammenführen

#### DCL (Data Control Language)
- `GRANT` - Berechtigungen erteilen
- `REVOKE` - Berechtigungen entziehen
- `DENY` - Berechtigungen verweigern

#### Transaktionssteuerung
- `COMMIT` - Transaktionen bestätigen
- `ROLLBACK` - Transaktionen zurücksetzen
- `SAVEPOINT` - Speicherpunkte erstellen
- `RELEASE` - Speicherpunkte freigeben

#### Administrative Befehle
- `SHUTDOWN` - Server herunterfahren
- `KILL` - Verbindungen beenden
- `PURGE` - Logs bereinigen
- `RESET` - Zurücksetzen
- `FLUSH` - Caches leeren
- `SET PASSWORD` - Passwort ändern
- `SET GLOBAL` - Globale Variablen setzen
- `SET SESSION` - Sitzungsvariablen setzen (außer read_only)

#### Replikation
- `CHANGE MASTER` - Master ändern
- `START SLAVE` - Slave starten
- `STOP SLAVE` - Slave stoppen

#### MariaDB/MySQL-spezifische Befehle
- `OPTIMIZE TABLE` - Tabellen defragmentieren (Schreiboperation)
- `REPAIR TABLE` - Tabellen reparieren (Schreiboperation)
- `ANALYZE TABLE` - Statistiken aktualisieren (Schreiboperation)
- `CHECK TABLE` - Tabellen prüfen (kann Reparaturen auslösen)
- `CHECKSUM TABLE` - Prüfsummen berechnen

### Erlaubte Befehle

Nur folgende Befehle sind erlaubt:

#### Datenabfragen
- `SELECT` - Daten abfragen
- `WITH` / `CTE` - Common Table Expressions

#### Metadaten-Abfragen
- `SHOW` - Informationen anzeigen (TABLES, DATABASES, COLUMNS, INDEX, etc.)
- `DESCRIBE` / `DESC` - Tabellenstruktur anzeigen
- `EXPLAIN` - Ausführungsplan anzeigen

#### Informationsschema
- `INFORMATION_SCHEMA` - Metadaten abfragen

#### Transaktionssteuerung (nur lesend)
- `START TRANSACTION READ ONLY` - Read-Only Transaktion starten
- `BEGIN READ ONLY` - Read-Only Transaktion beginnen
- `SET TRANSACTION READ ONLY` - Transaktion als read-only setzen

#### Sonstige
- `HELP` - Hilfe anzeigen

> **Hinweis:** `USE` ist nicht mehr als direkte Abfrage erlaubt. Ein Datenbankwechsel erfolgt über den `database`-Parameter der Endpunkte (z. B. `{"query": "...", "database": "mydb"}`). System-Schemata wie `mysql` oder `information_schema` sind gesperrt.

---

## :satellite: API Endpunkte

### MCP Endpunkte (für Open-WebUI)

| Methode | Endpunkt | Beschreibung |
|---------|----------|--------------|
| GET | `/mcp` | MCP Server Information (Tools, Capabilities) |
| POST | `/mcp` | MCP Anfragen verarbeiten |

### Standard API Endpunkte (für direkte Nutzung)

| Methode | Endpunkt | Beschreibung | Parameter |
|---------|----------|--------------|-----------|
| GET | `/` | Server-Informationen | - |
| GET | `/health` | Health-Check | - |
| POST | `/query` | SQL-Abfrage ausführen | `query`, `database` (optional), `timeout` (optional) |
| GET | `/query/validate` | SQL-Abfrage validieren | `query` |
| POST | `/query/validate` | SQL-Abfrage validieren | `query` |
| GET | `/query/stream` | SQL-Abfrage mit Streaming | `query`, `timeout` (optional) |
| GET | `/tables` | Alle Tabellen auflisten | `database` (optional) |
| GET | `/databases` | Alle Datenbanken auflisten | - |
| GET | `/schema/{table}` | Schema einer Tabelle abrufen | `table` |
| GET | `/columns/{table}` | Spalten einer Tabelle abrufen | `table` |
| GET | `/query/examples` | Beispiele für erlaubte Abfragen | - |
| GET | `/openapi.json` | OpenAPI-Spezifikation (öffentlich) | - |
| GET | `/docs` | Swagger UI Dokumentation (auth-pflichtig¹) | - |
| GET | `/redoc` | ReDoc Dokumentation (auth-pflichtig¹) | - |

> ¹ Auth-pflichtig, außer `PUBLIC_DOCS=true` ist gesetzt.

---

## :computer: API Beispiele

### Einfache Abfrage mit Authentifizierung

```bash
# Mit Authorization Header
curl -X POST http://localhost:8000/query \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT * FROM customers LIMIT 10"}'

# Mit Query Parameter
curl -X POST http://localhost:8000/query?api_key=your-api-token \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT * FROM customers LIMIT 10"}'

# Mit Datenbank-Angabe
curl -X POST http://localhost:8000/query \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT * FROM mails LIMIT 5", "database": "tanss"}'
```

### MCP Endpunkt testen

```bash
# MCP Server Info abrufen (keine Authentifizierung nötig)
curl http://localhost:8000/mcp

# MCP Anfrage ausführen (mit Authentifizierung)
curl -X POST http://localhost:8000/mcp \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"method": "execute_query", "params": {"query": "SELECT * FROM customers LIMIT 5"}}'
```

### Streaming Abfrage

```bash
curl -H "Authorization: Bearer your-api-token" \
  http://localhost:8000/query/stream?query=SELECT%20*%20FROM%20large_table
```

Antwort (Server-Sent Events):
```
data: {"type": "metadata", "columns": ["id", "name"], "query": "SELECT * FROM large_table"}

data: {"type": "row", "data": {"id": 1, "name": "Row 1"}}

data: {"type": "row", "data": {"id": 2, "name": "Row 2"}}

data: {"type": "complete", "total_rows": 1000}
```

### Abfrage validieren

```bash
curl -X POST http://localhost:8000/query/validate \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"query": "INSERT INTO users VALUES (1, \"test\")"}'
```

Antwort:
```json
{
  "valid": false,
  "error": "Abfrage enthält Schreiboperationen. Nur lesende Abfragen sind erlaubt.",
  "blocked_keywords": ["INSERT"]
}
```

### Tabellen auflisten

```bash
# Alle Tabellen in der aktuellen Datenbank
curl -H "Authorization: Bearer your-api-token" \
  http://localhost:8000/tables

# Tabellen in einer bestimmten Datenbank
curl -H "Authorization: Bearer your-api-token" \
  http://localhost:8000/tables?database=tanss
```

### Schema einer Tabelle abrufen

```bash
curl -H "Authorization: Bearer your-api-token" \
  http://localhost:8000/schema/customers
```

---

## :hourglass: Rate Limiting

Der Server implementiert Rate Limiting, um die API vor übermäßiger Nutzung zu schützen.

### Standard-Konfiguration

- **Aktiviert:** Ja (standardmäßig)
- **Anfragen pro Minute:** 100
- **Burst-Anfragen:** 10
- **Whitelist:** `/`, `/health`, `/docs`, `/openapi.json`, `/redoc`

### Rate Limit Header

Jede Antwort enthält folgende Header:
- `X-RateLimit-Limit`: Maximale Anfragen pro Minute
- `X-RateLimit-Remaining`: Verbleibende Anfragen
- `X-RateLimit-Reset`: Zeitstempel, wann das Limit zurückgesetzt wird

### Fehlerbehandlung

Bei Überschreitung des Rate Limits:
- **HTTP Status:** 429 Too Many Requests
- **Header:** `Retry-After: 60` (Sekunden bis zum nächsten Versuch)
- **Body:**
  ```json
  {
    "error": "Too Many Requests",
    "detail": "Rate Limit überschritten. Maximale Anfragen: 100 pro Minute",
    "retry_after": 60
  }
  ```

---

## :test_tube: Testen

### Automatisierte Tests

```bash
# Installiere Test-Abhängigkeiten
pip install pytest httpx

# Führe Tests aus
pytest tests/
```

### Manuelles Testen

1. **Verbindung testen:**
   ```bash
   curl http://localhost:8000/health
   ```

2. **MCP Endpunkt testen:**
   ```bash
   curl http://localhost:8000/mcp
   ```

3. **Erlaubte Abfrage testen:**
   ```bash
   curl -X POST http://localhost:8000/query \
     -H "Authorization: Bearer your-token" \
     -d '{"query": "SELECT 1"}'
   ```

4. **Blockierte Abfrage testen:**
   ```bash
   curl -X POST http://localhost:8000/query \
     -H "Authorization: Bearer your-token" \
     -d '{"query": "INSERT INTO test VALUES (1)"}'
   ```
   --> Sollte Fehler 403 zurückgeben

5. **Authentifizierung testen:**
   ```bash
   # Ohne Token (sollte 401 zurückgeben, wenn Auth aktiviert)
   curl -X POST http://localhost:8000/query \
     -d '{"query": "SELECT 1"}'
   
   # Mit falschem Token (sollte 401 zurückgeben)
   curl -X POST http://localhost:8000/query \
     -H "Authorization: Bearer wrong-token" \
     -d '{"query": "SELECT 1"}'
   
   # Mit richtigem Token (sollte funktionieren)
   curl -X POST http://localhost:8000/query \
     -H "Authorization: Bearer your-correct-token" \
     -d '{"query": "SELECT 1"}'
   ```

---

## :wrench: Fehlerbehebung

### Häufige Probleme und Lösungen

#### 1. Open-WebUI erkennt den MCP Server nicht
- **Ursache:** Falsche URL oder Server nicht erreichbar
- **Lösung:** 
  - URL in Open-WebUI auf `http://localhost:8000` setzen
  - Server-Status prüfen: `curl http://localhost:8000/health`
  - MCP-Endpunkt testen: `curl http://localhost:8000/mcp`

#### 2. "Leere Abfrage" Fehler
- **Ursache:** Open-WebUI sendet die Abfrage in einem anderen Format
- **Lösung:** Der Server unterstützt jetzt:
  - JSON Body: `{"query": "SELECT ..."}`
  - Formular-Daten: `query=SELECT ...`
  - Alternative Feldnamen: `query`, `sql`, `q`
  - Datenbank-Angabe: `{"query": "...", "database": "tanss"}`

#### 3. Verbindung zur Datenbank scheitert
- **Ursache:** Falsche Credentials oder MariaDB nicht für Remote-Zugriff konfiguriert
- **Lösung:**
  - Prüfe `DB_HOST`, `DB_USER`, `DB_PASSWORD` in `docker-compose.yml`
  - MariaDB für Remote-Zugriff konfigurieren:
    ```ini
    # In /etc/mysql/mariadb.conf.d/50-server.cnf
    bind-address = 0.0.0.0
    ```
  - Benutzer berechtigen:
    ```sql
    GRANT SELECT ON *.* TO 'mcp_user'@'%';
    FLUSH PRIVILEGES;
    ```
  - `network_mode: host` in `docker-compose.yml` verwenden

#### 4. Server nicht erreichbar
- **Ursache:** Port Konflikt oder Firewall
- **Lösung:**
  - Prüfe mit `curl http://localhost:8000/health`
  - Port 8000 freigeben: `sudo ufw allow 8000`
  - Andere Dienste auf Port 8000 beenden: `sudo lsof -i :8000`

#### 5. Docker-Container startet nicht
- **Ursache:** Berechtigungsprobleme oder fehlende Abhängigkeiten
- **Lösung:**
  ```bash
  docker-compose down
  docker-compose up -d --build
  docker-compose logs mariadb-mcp-server
  ```

#### 6. Abfragen werden blockiert
- **Ursache:** Abfrage enthält Schreiboperationen
- **Lösung:**
  - Validierung prüfen: `curl -X POST http://localhost:8000/query/validate -d '{"query": "DEINE_ABFRAGE"}'`
  - Nur lesende Abfragen verwenden (SELECT, SHOW, DESCRIBE, etc.)

#### 7. 401 Unauthorized Fehler
- **Ursache:** API-Token-Authentifizierung aktiviert, aber kein oder falscher Token angegeben
- **Lösung:**
  - Token in Authorization Header angeben: `-H "Authorization: Bearer your-token"`
  - Token als Query Parameter angeben: `?api_key=your-token`
  - Authentifizierung deaktivieren: `DISABLE_API_AUTH=true`

> **Hinweis:** Die Token-Übertragung im Request-Body wird nicht mehr unterstützt (kein Doppelkonsum des Bodies). Verwende Header oder Query-Parameter.

---

## :package: Abhängigkeiten

Der Server verwendet folgende Python-Pakete:

| Paket | Version | Zweck |
|-------|---------|-------|
| fastapi | >=0.104.0 | Web-Framework für die API |
| uvicorn | >=0.24.0 | ASGI-Server |
| mysql-connector-python | >=8.0.0 | MariaDB/MySQL Connector |
| sse-starlette | >=1.6.0 | Server-Sent Events Unterstützung |
| pydantic | >=2.5.0 | Datenvalidierung |
| python-multipart | >=0.0.6 | Formular-Daten Unterstützung |

> **:bulb: Hinweis:** Wir verwenden `mysql-connector-python` statt `mariadb`, da dieser Connector keine externen Systembibliotheken benötigt und damit Docker-freundlicher ist. Er ist vollständig kompatibel mit MariaDB.

---

## :bookmark: Versionshistorie

| Version | Datum | Änderungen |
|---------|-------|------------|
| v1.0.0 | 2026-07-30 | Erste stabile Version |
| | | Read-only SQL-Validierung |
| | | Streaming-Unterstützung |
| | | Docker-Unterstützung |
| | | Wechsel zu mysql-connector-python |
| | | MCP-kompatibler Endpunkt |
| v1.0.1 | 2026-08-03 | Bugfixes |
| | | Behebe "Leere Abfrage" Fehler |
| | | Behebe TRANSACTION READ ONLY Fehler |
| | | Unterstützung für alternative Anfrage-Formate |
| | | Verbesserte Docker-Netzwerk-Konfiguration |
| v1.1.0 | 2026-08-05 | API-Token-Authentifizierung |
| | | Unterstützung für einzelne und mehrere Tokens |
| | | Token aus Datei laden |
| | | Flexible Token-Übertragung (Header, Query) |
| | | Benutzerdefinierte Header/Parameter Namen |
| | | Öffentliche Endpunkte ohne Authentifizierung |
| v1.2.0 | 2026-08-13 | Erweiterte Sicherheit |
| | | Thread-sicheres Rate Limiting |
| | | Korrigierte asyncio-Probleme |
| | | Verbesserte Fehlerbehandlung |
| | | Aktualisierte Dokumentation |
| v1.3.0 | 2026-08-14 | Sicherheits-Härtung |
| | | SQL-Injection-Schutz: Identifier-Validierung, parametrisierte Queries |
| | | USE blockiert, Datenbank-Allow-Liste (ALLOWED_DATABASES) |
| | | Connection-Pool statt globaler Verbindung (Race Condition) |
| | | Auth-Middleware liest Request-Body nicht mehr (kein Doppelkonsum) |
| | | CORS restriktiviert (CORS_ALLOWED_ORIGINS) |
| | | /docs, /redoc auth-pflichtig (PUBLIC_DOCS); /openapi.json öffentlich |
| | | Fehlermeldungen leaken keine DB-Interna |
| | | Testsuite repariert (119 Tests) |
| v1.3.1 | 2026-08-14 | Weitere Sicherheits-Mechanismen |
| | | Konstanter Token-Vergleich (Timing-Seitenkanal) |
| | | Stack-basiertes Kommentar-Stripping (verschachtelte/gestaffelte Kommentare) |
| | | Request-Body-Größenbegrenzung (`MAX_REQUEST_BODY_BYTES`) |
| | | Security-Headers (`nosniff`, `DENY`, `no-store`, HSTS bei HTTPS) |
| | | Fail-Closed-Startup: Auth ohne Tokens bricht den Start ab |
| | | Token-Datei-Hot-Reload (Rotation ohne Restart) |
| | | `DB_USER=root`-Startup-Guard (`ALLOW_DB_ROOT`) |
| | | Strukturiertes Audit-Logging (Token-Index statt Token-Wert) |
| | | `.dockerignore` + Container-Härtung (`cap_drop`, `read_only`, `no-new-privileges`) |
| | | `config.json`-Passwort-Feld als Platzhalter |
| v1.4.0 | 2026-09-17 | MCP-Spec 2026-07-28 (stateless core) |
| | | JSON-RPC 2.0 auf `POST /mcp` (Dual-Era: Spec + Open-WebUI-Legacy) |
| | | `server/discover` (Pflicht-Methode, ersetzt initialize-Handshake) |
| | | `tools/list` mit ttlMs/cacheScope, deterministische Tool-Reihenfolge |
| | | `tools/call` mit `content` + `structuredContent`, `isError`-Semantik |
| | | Header-Routing/Validierung: `MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name` (-32020) |
| | | Base64-Sentinel-Dekodierung für `Mcp-Name` |
| | | `-32022` UnsupportedProtocolVersion, `404`+`-32601` für unbekannte Methoden |
| | | Notifications: `202 Accepted` |
| | | Mit offiziellem Python-MCP-SDK verifiziert (discover→adopt→tools) |
| | | 48 neue Tests (167 gesamt) |

---

## :busts_in_silhouette: Mitwirken

1. Fork das Repository
2. Erstelle einen Feature-Branch (`git checkout -b feature/AmazingFeature`)
3. Commit deine Änderungen (`git commit -m 'Add some AmazingFeature'`)
4. Push zum Branch (`git push origin feature/AmazingFeature`)
5. Öffne einen Pull Request

---

## :memo: Lizenz

Dieses Projekt ist unter der MIT-Lizenz lizenziert - siehe [LICENSE](LICENSE) für Details.

---

## :email: Kontakt

- **GitHub:** [AndiAtom/mariadb-mcp-strhttp](https://github.com/AndiAtom/mariadb-mcp-strhttp)
- **Issues:** [GitHub Issues](https://github.com/AndiAtom/mariadb-mcp-strhttp/issues)

---

**Hinweis:** Dieser Server ist **ausschließlich für lesende Abfragen** konzipiert. Alle Versuche, Schreiboperationen auszuführen, werden blockiert und führen zu einem Fehler.

**Technischer Hinweis:** Der Server verwendet `mysql-connector-python`, der vollständig mit MariaDB kompatibel ist und keine externen C-Bibliotheken benötigt, was die Docker-Installation deutlich vereinfacht. Der Server implementiert einen MCP-kompatiblen Endpunkt (`/mcp`) für nahtlose Integration mit Open-WebUI und unterstützt sowohl JSON- als auch Formular-Daten-Anfragen. Die Read-Only-Funktionalität wird auf Session-Ebene (`SET SESSION read_only=ON`) und auf Abfrage-Ebene (Validierung) sichergestellt. Die API-Token-Authentifizierung bietet eine zusätzliche Sicherheitsebene für den Zugriff auf die API.