523 lines
27 KiB
Markdown
523 lines
27 KiB
Markdown
# mcp-calculator
|
||
|
||
MCP-Server, der LLMs **exakte, deterministische Mathematik** gibt. LLMs sagen Tokens voraus und
|
||
verrechnen sich deshalb bei großen Zahlen, vielen Rechenschritten, Wahrscheinlichkeiten oder
|
||
Einheiten. Dieser Server übernimmt das Rechnen. Das LLM formuliert nur den Aufruf und deutet das Ergebnis.
|
||
|
||
- **13 Tools**: Arithmetik mit beliebiger Präzision, Algebra, Gleichungen, Analysis, Kurvendiskussion,
|
||
Differentialgleichungen, lineare Algebra, Statistik, Regression, Verteilungen, Hypothesentests,
|
||
Zahlentheorie, Einheiten
|
||
- **Beide Transporte**: `stdio` (lokal) und **Streamable HTTP** (zentral, zustandslos, skalierbar)
|
||
- **Sicher**: Parser mit Whitelist statt `eval`, Worker-Prozesse mit hartem Timeout und
|
||
Speicherlimit
|
||
- **Client-Verwaltung**: API-Tokens (nur als Hash gespeichert) und Netz-Freigaben in einer Datei auf dem
|
||
gemeinsamen Volume, änderbar zur Laufzeit per CLI oder Admin-API, mit Audit-Log
|
||
- **Überlastschutz**: Verbindungslimit, Limit pro Client, Kapazitätsgrenze, Wartezeit-Limit, Timeout,
|
||
Abbruch bei Verbindungsende
|
||
- **Betriebsbereit**: Health/Readiness, Prometheus-Metriken, Graceful Shutdown, JSON-Logging,
|
||
Konfiguration komplett über Env-Variablen, festgeschriebene Abhängigkeiten
|
||
|
||
Stack: Python ≥ 3.12, [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) 2.x,
|
||
SymPy, mpmath, NumPy, SciPy, Pint, Pebble, prometheus-client. Tests mit `unittest` (Standardbibliothek).
|
||
|
||
Offene Ideen und geplante Features: [BACKLOG.md](BACKLOG.md).
|
||
|
||
---
|
||
|
||
## Inhalt
|
||
|
||
1. [Schnellstart](#schnellstart)
|
||
2. [Tools im Überblick](#tools-im-überblick)
|
||
3. [Transporte](#transporte)
|
||
4. [Clients anbinden](#clients-anbinden)
|
||
5. [Konfiguration](#konfiguration)
|
||
6. [Sicherheit](#sicherheit)
|
||
7. [Betrieb](#betrieb)
|
||
8. [Monitoring](#monitoring)
|
||
9. [Architektur](#architektur)
|
||
10. [Entwicklung](#entwicklung)
|
||
11. [Grenzen](#grenzen)
|
||
|
||
Die ausführliche Referenz aller Parameter steht in [docs/tools.md](docs/tools.md).
|
||
|
||
---
|
||
|
||
## Schnellstart
|
||
|
||
Alles wird ausschließlich im virtuellen Environment des Projekts installiert, exakt in den Versionen
|
||
aus dem Lockfile.
|
||
|
||
```bash
|
||
# venv anlegen und exakt nach uv.lock installieren (inkl. Dev-Werkzeug ruff)
|
||
uv sync --frozen --extra dev --python 3.12
|
||
source .venv/bin/activate
|
||
|
||
# alternativ ohne uv, mit pip (Hashes werden geprüft)
|
||
python3.12 -m venv .venv && source .venv/bin/activate
|
||
pip install --require-hashes -r requirements.lock
|
||
pip install --no-deps -e .
|
||
|
||
# Tests (unittest aus der Standardbibliothek)
|
||
python -m unittest # alle Tests (~50 s)
|
||
MCP_CALC_SKIP_SLOW=1 python -m unittest # ohne Subprozess-/Server-Tests (~1 s)
|
||
|
||
# Server starten
|
||
mcp-calculator # stdio (Standard)
|
||
mcp-calculator --transport http --port 8080 # HTTP auf 127.0.0.1:8080/mcp
|
||
python -m mcp_calculator --help # alle Optionen
|
||
```
|
||
|
||
Ausprobieren:
|
||
|
||
```bash
|
||
# Beispiel-Client (startet den Server selbst über stdio)
|
||
python examples/client_example.py
|
||
|
||
# gegen einen laufenden HTTP-Server
|
||
python examples/client_example.py http://localhost:8080/mcp [TOKEN]
|
||
|
||
# interaktiv im Browser mit dem MCP Inspector (braucht Node.js)
|
||
npx @modelcontextprotocol/inspector .venv/bin/mcp-calculator
|
||
```
|
||
|
||
---
|
||
|
||
## Tools im Überblick
|
||
|
||
| Tool | Zweck | Beispiel |
|
||
|---|---|---|
|
||
| `evaluate` | Wert exakt und dezimal, beliebige Präzision | `1000*(1+0.05)^10` → `1628.89462678` |
|
||
| `transform` | simplify, expand, factor, apart, substitute, nsimplify … | `x^2-5x+6` → `(x-3)(x-2)` |
|
||
| `solve` | Gleichungen, Systeme, Ungleichungen, numerisch | `["x+y=10","x-y=2"]` → `x=6, y=4` |
|
||
| `calculus` | Ableitung, Integral, Grenzwert, Reihe, Summe, Gradient, Hesse, Jacobi | `∫e^(-x²)` von -∞ bis ∞ → `sqrt(pi)` |
|
||
| `analyze_function` | komplette Kurvendiskussion | Nullstellen, Extrema, Wendepunkte, Asymptoten … |
|
||
| `solve_ode` | Differentialgleichungen mit Anfangswerten | `y''+y=0, y(0)=0, y'(0)=1` → `sin(x)` |
|
||
| `matrix` | Determinante, Inverse, Eigenwerte, LGS, SVD … (exakt oder numerisch) | `[[1,2],[3,4]]` → det `-2` |
|
||
| `statistics` | deskriptive Statistik, Korrelation | Mittel, Median, Quartile, Schiefe … |
|
||
| `regression` | linear, polynomial, exponentiell, logarithmisch, Potenz | Koeffizienten, R², Vorhersage |
|
||
| `probability` | 14 Verteilungen: pdf/cdf/sf/Quantil/Intervall | `P(85 < X ≤ 115)` bei N(100, 15) |
|
||
| `hypothesis_test` | t-Tests, ANOVA, χ², Mann-Whitney, Wilcoxon, Shapiro, Binomial | p-Wert und Entscheidung im Klartext |
|
||
| `number_theory` | Primzahlen, ggT/kgV, Modular, CRT, Kombinatorik, Basen | `factorize 360` → `2^3 * 3^2 * 5` |
|
||
| `convert_units` | Einheiten umrechnen und mit Einheiten rechnen | `100 km/h` → `27.78 m/s` |
|
||
|
||
**Designentscheidungen, die die Treffsicherheit des LLMs erhöhen:**
|
||
|
||
- *Wenige Tools, klar abgegrenzt.* Verwandte Operationen hängen an einem `operation`-Enum. Das LLM wählt zuverlässiger aus 13 Tools als aus 60.
|
||
- *Server-`instructions`* erklären beim Verbindungsaufbau, welches Tool wofür gedacht ist, und beschreiben die Syntax.
|
||
- *Verkettbare Ergebnisse*: `result` hat dieselbe Syntax wie die Eingabe. Eine Ableitung lässt sich also direkt an `solve` weitergeben.
|
||
- *Exakt statt Gleitkomma*: `0.1 + 0.2 = 3/10`. Dezimalwerte kommen als String mit der gewünschten Stellenzahl zurück.
|
||
- *Selbsterklärende Ergebnisse*: `meaning` bei Wahrscheinlichkeiten (`P(X > 7) = P(X >= 8)`), `conclusion` bei Tests, `note` bei Sonderfällen.
|
||
- *Hilfreiche Fehler*: Tool-Fehler mit konkretem Hinweis (`matrix is singular …; try 'pseudo_inverse'`), damit das LLM den Aufruf selbst korrigieren kann.
|
||
|
||
---
|
||
|
||
## Transporte
|
||
|
||
| | stdio | Streamable HTTP |
|
||
|---|---|---|
|
||
| Start | Client startet den Prozess | Server läuft dauerhaft |
|
||
| Einsatz | lokal, Einzelplatz, Entwicklung | zentral fürs Team, Pod |
|
||
| Auth | keine | API-Token pro Client oder Netz-Freigabe, siehe [docs/auth.md](docs/auth.md) |
|
||
| Endpoint | – | `POST http://<host>:8080/mcp` |
|
||
|
||
Der HTTP-Transport läuft **zustandslos** (`stateless_http`, JSON-Antworten). Es gibt keine
|
||
MCP-Sessions, deshalb braucht es keine Sticky Sessions, und beliebig viele Replicas sind möglich.
|
||
|
||
```bash
|
||
# Client anlegen (Token wird einmalig angezeigt)
|
||
mcp-calculator-admin --file ./data/clients.json create mein-client
|
||
|
||
# HTTP, wie im Container
|
||
MCP_CALC_TRANSPORT=http \
|
||
MCP_CALC_HOST=0.0.0.0 \
|
||
MCP_CALC_CLIENTS_FILE=./data/clients.json \
|
||
MCP_CALC_LOG_FORMAT=json \
|
||
mcp-calculator
|
||
```
|
||
|
||
Rohtest mit curl:
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:8080/mcp \
|
||
-H "Content-Type: application/json" \
|
||
-H "Accept: application/json, text/event-stream" \
|
||
-H "Authorization: Bearer mcpc_…" \
|
||
-H "MCP-Protocol-Version: 2025-06-18" \
|
||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"evaluate","arguments":{"expression":"2^100"}}}'
|
||
```
|
||
|
||
---
|
||
|
||
## Clients anbinden
|
||
|
||
> Die Konfigurationsformate der Clients ändern sich gelegentlich. Die Beispiele entsprechen dem
|
||
> Stand bei der Erstellung. Bitte gegen die Doku der eingesetzten Client-Version prüfen.
|
||
|
||
Jeder Client (Team, IDE-Installation, Dienst) bekommt ein **eigenes Token**:
|
||
`mcp-calculator-admin create <name>`. Das Token wird im Header `Authorization: Bearer <token>`
|
||
mitgeschickt, alternativ als `X-API-Key: <token>`. Verwaltung und Sperren: [docs/auth.md](docs/auth.md).
|
||
|
||
### opencode
|
||
|
||
`opencode.json` (Projekt) oder `~/.config/opencode/opencode.json` (global):
|
||
|
||
```json
|
||
{
|
||
"$schema": "https://opencode.ai/config.json",
|
||
"mcp": {
|
||
"calculator": {
|
||
"type": "remote",
|
||
"url": "https://calculator.intern.example/mcp",
|
||
"enabled": true,
|
||
"headers": {
|
||
"Authorization": "Bearer {env:MCP_CALC_TOKEN}"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Lokal über stdio:
|
||
|
||
```json
|
||
{
|
||
"mcp": {
|
||
"calculator": {
|
||
"type": "local",
|
||
"command": ["/pfad/zu/mcp-calculator/.venv/bin/mcp-calculator"],
|
||
"enabled": true
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Cline
|
||
|
||
`cline_mcp_settings.json` (in Cline über „MCP Servers → Configure“ erreichbar):
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"calculator": {
|
||
"type": "streamableHttp",
|
||
"url": "https://calculator.intern.example/mcp",
|
||
"headers": {
|
||
"Authorization": "Bearer <token>"
|
||
},
|
||
"disabled": false,
|
||
"autoApprove": [
|
||
"evaluate", "transform", "solve", "calculus", "analyze_function", "solve_ode", "matrix",
|
||
"statistics", "regression", "probability", "hypothesis_test", "number_theory", "convert_units"
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Alle Tools sind rein lesend, deshalb ist `autoApprove` unbedenklich. Lokal über stdio:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"calculator": {
|
||
"command": "/pfad/zu/mcp-calculator/.venv/bin/mcp-calculator",
|
||
"args": []
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Claude Code
|
||
|
||
```bash
|
||
claude mcp add --transport http calculator https://calculator.intern.example/mcp \
|
||
--header "Authorization: Bearer <token>"
|
||
# oder lokal
|
||
claude mcp add calculator -- /pfad/zu/mcp-calculator/.venv/bin/mcp-calculator
|
||
```
|
||
|
||
### Eigene Tools / Agenten
|
||
|
||
Jede Anwendung mit MCP-Client-SDK (Python, TypeScript, Java, Go …) kann den Server nutzen.
|
||
Ein lauffähiges Python-Beispiel liegt in [`examples/client_example.py`](examples/client_example.py):
|
||
|
||
```python
|
||
import httpx2
|
||
from mcp import Client
|
||
from mcp.client.streamable_http import streamable_http_client
|
||
|
||
async with httpx2.AsyncClient(headers={"Authorization": f"Bearer {token}"}) as http:
|
||
async with Client(streamable_http_client("https://calculator.intern.example/mcp", http_client=http)) as client:
|
||
tools = (await client.list_tools()).tools # -> als Tool-Definitionen ans LLM
|
||
result = await client.call_tool("solve", {"equations": "x^2 = 2", "domain": "real"})
|
||
print(result.structured_content)
|
||
```
|
||
|
||
Einbindung in eine eigene LLM-Schleife:
|
||
1. `list_tools()` ausführen und `name`, `description` und `input_schema` jedes Tools als Tool-Definition an das Modell geben.
|
||
2. Fordert das Modell einen Tool-Call an, `call_tool(name, arguments)` aufrufen.
|
||
3. `result.content[0].text` zurückgeben. Bei `result.is_error` den Rückgabewert als Fehler markieren.
|
||
|
||
> **Hinweis:** Serverseitige MCP-Connectoren von LLM-Anbietern rufen den MCP-Server aus der Cloud
|
||
> des Anbieters auf. Ein nur intern erreichbarer Server funktioniert damit nicht. Bei internem
|
||
> Betrieb muss der eigene Code MCP-Client sein (wie oben).
|
||
|
||
---
|
||
|
||
## Konfiguration
|
||
|
||
Jede Option ist per Umgebungsvariable `MCP_CALC_<NAME>` oder per CLI-Flag `--<name>` setzbar.
|
||
Die CLI hat Vorrang vor der Umgebung, die Umgebung vor den Defaults. Eine Vorlage liegt in
|
||
[`.env.example`](.env.example). Die Datei wird **nicht** automatisch geladen, sie dient als
|
||
Referenz für die Pod-Umgebung.
|
||
|
||
| Variable | Default | Beschreibung |
|
||
|---|---|---|
|
||
| `MCP_CALC_TRANSPORT` | `stdio` | `stdio` oder `http` |
|
||
| `MCP_CALC_HOST` | `127.0.0.1` | Bind-Adresse; im Container `0.0.0.0` |
|
||
| `MCP_CALC_PORT` | `8080` | HTTP-Port |
|
||
| `MCP_CALC_PATH` | `/mcp` | Pfad des MCP-Endpoints |
|
||
| `MCP_CALC_ALLOWED_HOSTS` | – | erlaubte `Host`-Header, kommagetrennt; aktiviert den DNS-Rebinding-Schutz |
|
||
| `MCP_CALC_ALLOWED_ORIGINS` | – | erlaubte `Origin`-Header (nur für Browser-Clients relevant) |
|
||
| `MCP_CALC_TRUSTED_PROXIES` | – | IPs/CIDRs von Proxys (OpenShift-Router), deren `X-Forwarded-For` ausgewertet wird |
|
||
| `MCP_CALC_MAX_REQUEST_BODY_BYTES` | `1048576` | maximale Request-Größe |
|
||
| `MCP_CALC_MAX_CONNECTIONS` | `200` | gleichzeitige HTTP-Verbindungen; darüber antwortet uvicorn mit `503` |
|
||
| `MCP_CALC_METRICS` | `true` | Prometheus-Metriken unter `/metrics` (nur HTTP) |
|
||
| **Authentifizierung** | | *nur HTTP; Details in [docs/auth.md](docs/auth.md)* |
|
||
| `MCP_CALC_CLIENTS_FILE` | – | Client-Datei (z. B. `/data/clients.json` auf RWX-PVC); aktiviert die Authentifizierung |
|
||
| `MCP_CALC_CLIENTS_RELOAD_SECONDS` | `5` | wie oft die Datei auf Änderungen geprüft wird |
|
||
| `MCP_CALC_AUTH_TOKENS` | – | statische Tokens `name:token,…` (einfache Alternative, Änderung nur per Neustart) |
|
||
| `MCP_CALC_AUDIT_FILE` | `audit.log` neben der Client-Datei | Änderungsprotokoll |
|
||
| `MCP_CALC_ADMIN_TOKENS` | – | Admin-Tokens `name:token,…`; nur wenn gesetzt, startet die Admin-API |
|
||
| `MCP_CALC_ADMIN_HOST` | `127.0.0.1` | Bind-Adresse der Admin-API (Default: nur per `oc port-forward`/`exec`) |
|
||
| `MCP_CALC_ADMIN_PORT` | `9090` | Port der Admin-API |
|
||
| `MCP_CALC_EXECUTOR` | `process` | `process` (isolierter Worker-Pool) oder `inline` (nur Entwicklung) |
|
||
| `MCP_CALC_WORKERS` | `2` | Anzahl Worker-Prozesse (≈ CPU-Kerne) |
|
||
| `MCP_CALC_MAX_QUEUE` | `20` | so viele Berechnungen dürfen auf einen freien Worker warten; darüber wird sofort mit „busy“ abgelehnt (0 = keine Warteschlange) |
|
||
| `MCP_CALC_MAX_QUEUE_WAIT_SECONDS` | `30` | maximale Wartezeit auf einen freien Worker, danach „busy“ |
|
||
| `MCP_CALC_CLIENT_MAX_CONCURRENT` | `10` | gleichzeitige Berechnungen pro Client (0 = unbegrenzt); pro Client überschreibbar |
|
||
| `MCP_CALC_TIMEOUT_SECONDS` | `10` | harter Timeout pro Berechnung |
|
||
| `MCP_CALC_MAX_TASKS_PER_WORKER` | `200` | Worker nach N Aufgaben recyceln (0 = nie) |
|
||
| `MCP_CALC_MEMORY_LIMIT_MB` | `512` | zusätzlicher Speicher, den eine Berechnung pro Worker belegen darf (nur Linux; 0 = aus), siehe [docs/memory.md](docs/memory.md) |
|
||
| `MCP_CALC_MAX_INPUT_LENGTH` | `5000` | max. Zeichen pro Ausdruck |
|
||
| `MCP_CALC_MAX_OUTPUT_LENGTH` | `20000` | längere Ergebnisse werden gekürzt |
|
||
| `MCP_CALC_MAX_PRECISION` | `1000` | max. signifikante Stellen |
|
||
| `MCP_CALC_MAX_MATRIX_SIZE` | `30` | max. Zeilen/Spalten |
|
||
| `MCP_CALC_MAX_DATA_POINTS` | `1000000` | max. Werte pro Datensatz |
|
||
| `MCP_CALC_MAX_LIST_ITEMS` | `10000` | max. Einträge sonstiger Listen |
|
||
| `MCP_CALC_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
||
| `MCP_CALC_LOG_FORMAT` | `text` | `text` oder `json` (eine Zeile pro Eintrag) |
|
||
|
||
---
|
||
|
||
## Sicherheit
|
||
|
||
**Ausdrucks-Parser (`ops/parsing.py`).** SymPys `parse_expr` nutzt intern `eval`. Abgesichert wird das in drei Schichten:
|
||
|
||
1. Die Eingabe wird vorab geprüft: Zeichen-Whitelist (keine Quotes, `[]`, `{}`, `;`, `:`, `\`),
|
||
kein `__`, kein Attributzugriff (`.name`), keine Python-Keywords, keine Namen mit `_`.
|
||
2. Ausgewertet wird in einem Namensraum, der **nur** freigegebene SymPy-Funktionen und ein leeres
|
||
`__builtins__` enthält. Unbekannte Namen werden zu mathematischen Symbolen, nie zu Python-Objekten.
|
||
3. Die Berechnung läuft isoliert in einem Worker-Prozess (siehe unten).
|
||
|
||
Die Tests in `tests/test_parsing.py` decken typische Injection-Versuche ab (`__import__`, `().__class__`,
|
||
`lambda`, `open(...)`, `getattr`, …).
|
||
|
||
**Worker-Pool (`executor.py`).** Jede Berechnung läuft in einem separaten Prozess (Pebble-Pool, `spawn`):
|
||
- Wird der Timeout überschritten, wird der Worker hart beendet und ersetzt. `9^9^9^9` blockiert also nichts.
|
||
- Unter Linux darf eine Berechnung pro Worker höchstens `MCP_CALC_MEMORY_LIMIT_MB` zusätzlich belegen (`RLIMIT_AS`, gesetzt nach dem Laden der Bibliotheken). Ein Ausreißer endet mit `MemoryError`, statt den Pod zu killen.
|
||
- BLAS/OpenMP laufen pro Worker einthreadig. Die Parallelität kommt aus der Worker-Anzahl, das CPU-Limit des Pods wird nicht überbucht.
|
||
- Worker werden nach N Aufgaben recycelt, damit die SymPy-Caches nicht unbegrenzt wachsen.
|
||
- Ist der Pool defekt, startet er sich selbst neu.
|
||
|
||
**HTTP.**
|
||
- Authentifizierung pro Client (API-Token oder Netz-Freigabe). Tokens sind 256 Bit zufällig und werden nur als SHA-256-Hash gespeichert; Sperren, Ablaufdatum, Netz-Einschränkung und Rotation sind zur Laufzeit möglich. Details: **[docs/auth.md](docs/auth.md)**.
|
||
- Die Admin-API läuft auf eigenem Port, standardmäßig nur auf `127.0.0.1`, mit eigenen Admin-Tokens; jede Änderung landet im Audit-Log.
|
||
- `/healthz`, `/readyz` und `/metrics` bleiben offen für Probes und Prometheus.
|
||
- Request-Body-Limit (Default 1 MiB), dazu Längenlimits für alle Eingaben.
|
||
- DNS-Rebinding-Schutz: bei `127.0.0.1` automatisch aktiv, bei `0.0.0.0` über `MCP_CALC_ALLOWED_HOSTS` einschaltbar.
|
||
- Ohne Client-Konfiguration und nicht an localhost gebunden, protokolliert der Server beim Start eine Warnung.
|
||
|
||
Die Einheiten-Umrechnung (Pint) nutzt einen eigenen Tokenizer ohne `eval`.
|
||
|
||
---
|
||
|
||
## Betrieb
|
||
|
||
Die Anbindung an OpenShift ist nicht Teil dieses Projekts. Für die eigene Integration relevant:
|
||
|
||
| Punkt | Wert |
|
||
|---|---|
|
||
| Start | `mcp-calculator` (Entry Point) oder `python -m mcp_calculator` |
|
||
| Pflicht-Env im Container | `MCP_CALC_TRANSPORT=http`, `MCP_CALC_HOST=0.0.0.0` |
|
||
| Ports | `8080` MCP; `9090` Admin-API (nur mit `MCP_CALC_ADMIN_TOKENS`, Default nur `127.0.0.1`). Unprivilegiert, beliebige UID möglich |
|
||
| Volume | RWX-PVC z. B. unter `/data` für `clients.json` und `audit.log`, sonst wird nichts geschrieben |
|
||
| Liveness | `GET /healthz` → `200 {"status":"ok","version":…}` |
|
||
| Readiness | `GET /readyz` → `200` erst, wenn die Worker gestartet sind und ihre Bibliotheken geladen haben, sonst `503` |
|
||
| Metriken | `GET /metrics` (Prometheus, ohne Token), siehe [Monitoring](#monitoring) |
|
||
| Shutdown | `SIGTERM`: keine neuen Verbindungen, laufende Requests werden fertig gerechnet, danach wird der Worker-Pool geordnet gestoppt (Log: `worker pool stopped`) |
|
||
| Überlast | Sechs Schutzschichten (Verbindungen, pro Client, Kapazität, Wartezeit, Timeout, Abbruch bei Verbindungsende); Ablehnungen sind Tool-Fehler mit „retry“-Hinweis. Siehe [docs/auth.md → Überlastschutz](docs/auth.md#überlastschutz) |
|
||
| Skalierung | zustandslos, horizontal skalierbar, keine Sticky Sessions |
|
||
| Ressourcen | `MCP_CALC_WORKERS` ≈ CPU-Limit. Leerlauf gemessen (macOS, RSS): ~90 MB + ~140 MB je Worker, mit 2 Workern ~360 MB. Pod-Limit ≥ Leerlauf + `WORKERS` × `MEMORY_LIMIT_MB`. Analyse, Profile und Messskript: **[docs/memory.md](docs/memory.md)** |
|
||
| Logs | stderr; `MCP_CALC_LOG_FORMAT=json` für Log-Aggregation. Pro Tool-Aufruf: `tool=<name> client=<client> status=… duration_ms=…`; abgelehnte Anfragen: `auth rejected: reason=… client=… ip=…` |
|
||
| Startzeit | `/readyz` nach ~1,2 s (Worker vorgewärmt); der erste Aufruf ist danach sofort schnell |
|
||
|
||
---
|
||
|
||
## Monitoring
|
||
|
||
`GET /metrics` liefert Prometheus-Textformat. Wie `/healthz` und `/readyz` ist der Endpoint ohne Token
|
||
erreichbar, damit Prometheus im Cluster scrapen kann. Er enthält Zähler und Client-Namen, keine Rechen-Eingaben.
|
||
Wer das nicht möchte, schränkt den Zugriff per NetworkPolicy ein oder setzt `MCP_CALC_METRICS=false`.
|
||
|
||
| Metrik | Typ | Bedeutung |
|
||
|---|---|---|
|
||
| `mcp_calc_tool_calls_total{tool, client, status}` | Counter | Aufrufe je Tool, Client und Ergebnis: `ok`, `error` (Mathe-/Eingabefehler), `timeout`, `busy` (Kapazität, Wartezeit oder Client-Limit), `cancelled` (Client hat Verbindung getrennt), `crashed` (Worker gestorben). `client` ist `local` bei stdio, `anonymous` ohne Client-Konfiguration |
|
||
| `mcp_calc_tool_duration_seconds{tool}` | Histogram | Dauer inkl. Wartezeit in der Queue |
|
||
| `mcp_calc_inflight_computations` | Gauge | laufende + wartende Berechnungen |
|
||
| `mcp_calc_capacity` | Gauge | `WORKERS + MAX_QUEUE`; darüber wird abgelehnt |
|
||
| `mcp_calc_workers` | Gauge | konfigurierte Worker |
|
||
| `mcp_calc_pool_restarts_total` | Counter | Neustarts des Pools nach internem Fehler |
|
||
| `mcp_calc_client_inflight_computations{client}` | Gauge | laufende + wartende Berechnungen je Client |
|
||
| `mcp_calc_auth_failures_total{reason}` | Counter | abgelehnte Anfragen: `missing_credentials`, `invalid_token`, `disabled`, `expired`, `network_denied` |
|
||
| `mcp_calc_clients{status}` | Gauge | konfigurierte Clients: `active`, `disabled`, `expired` |
|
||
| `mcp_calc_build_info{version}` | Info | Version |
|
||
| `process_*`, `python_*` | Standard | CPU, RSS, Dateien des **Server**-Prozesses (Worker nicht enthalten) |
|
||
|
||
Nicht gezählt werden Aufrufe, die schon an der Schema-Validierung scheitern (falscher Parametertyp).
|
||
Die beantwortet das MCP SDK, bevor der Tool-Code läuft.
|
||
|
||
Nützliche Abfragen:
|
||
|
||
```promql
|
||
# Fehler- und Timeout-Quote je Tool (5 min)
|
||
sum by (tool, status) (rate(mcp_calc_tool_calls_total{status!="ok"}[5m]))
|
||
/ ignoring(status) group_left sum by (tool) (rate(mcp_calc_tool_calls_total[5m]))
|
||
|
||
# p95-Dauer je Tool
|
||
histogram_quantile(0.95, sum by (tool, le) (rate(mcp_calc_tool_duration_seconds_bucket[5m])))
|
||
|
||
# Auslastung (Alarm z. B. > 0.8 über 5 min → mehr Worker oder Replicas)
|
||
mcp_calc_inflight_computations / mcp_calc_capacity
|
||
|
||
# Überlast-Ablehnungen, nach Client
|
||
sum by (client) (rate(mcp_calc_tool_calls_total{status="busy"}[5m]))
|
||
|
||
# Wer belegt gerade die Kapazität?
|
||
topk(5, mcp_calc_client_inflight_computations)
|
||
|
||
# Auffällige Auth-Fehler (falsch konfigurierte Clients oder Rateversuche)
|
||
sum by (reason) (rate(mcp_calc_auth_failures_total[5m]))
|
||
```
|
||
|
||
---
|
||
|
||
## Architektur
|
||
|
||
```
|
||
┌────────────────────────── Server-Prozess ──────────────────────────┐
|
||
Client ─stdio──►│ MCPServer (server.py) │
|
||
─HTTP───►│ └ Starlette/uvicorn (http_app.py): Bearer-Auth, /healthz, /readyz │
|
||
│ Tool-Funktionen: Validierung (Pydantic-Schemas) → Executor │
|
||
└───────────────────────────────┬─────────────────────────────────────┘
|
||
│ Task-Name + Argumente, Timeout
|
||
┌──────────────────▼──────────────────┐
|
||
│ Worker-Pool (executor.py, Pebble) │
|
||
│ ops/*.py – reine Mathe-Funktionen │
|
||
│ SymPy · mpmath · NumPy · SciPy · Pint
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
Der Server-Prozess lädt nur MCP SDK und HTTP-Stack, die Worker nur die Mathe-Bibliotheken.
|
||
Das halbiert den Speicherbedarf ungefähr (Details in [docs/memory.md](docs/memory.md)) und wird von
|
||
`tests/test_footprint.py` abgesichert.
|
||
|
||
```
|
||
src/mcp_calculator/
|
||
├── __main__.py Entry Point: Konfiguration laden, Logging, Transport starten
|
||
├── config.py Settings aus Env/CLI, Validierung, Limits
|
||
├── server.py MCP-Tools: Schemas, Beschreibungen, Server-Instructions
|
||
├── http_app.py HTTP-Transport: ASGI-App, Abbruch bei Verbindungsende, Health, Admin-Server
|
||
├── auth.py Authentifizierung (Token, Netz) und Client-Identität je Request
|
||
├── clients.py Client-Modell, Client-Datei (Lock, atomar), Hot-Reload
|
||
├── admin.py Client-Verwaltung (gemeinsame Logik), Audit-Log, Admin-API
|
||
├── admin_cli.py CLI mcp-calculator-admin
|
||
├── ratelimit.py Limit gleichzeitiger Berechnungen pro Client
|
||
├── executor.py Worker-Pool: Timeout, Speicherlimit, Backpressure, Vorwärmen, Selbstheilung
|
||
├── metrics.py Prometheus-Metriken
|
||
├── logging_setup.py Text-/JSON-Logging nach stderr
|
||
└── ops/ reine, synchrone Mathe-Funktionen (dict rein, dict raus)
|
||
├── spec.py Typen/Enums/Hilfetexte für die Tool-Schemas (import-leicht, auch im Server)
|
||
├── context.py Fehlertyp und Limits (import-leicht)
|
||
├── parsing.py sicherer Ausdrucks-Parser
|
||
├── common.py Ergebnis-Formatierung, Limit-Prüfungen
|
||
├── algebra.py evaluate, transform, solve
|
||
├── calculus.py calculus, analyze_function, solve_ode
|
||
├── linalg.py matrix
|
||
├── stats.py statistics, regression, probability, hypothesis_test
|
||
├── number_theory.py
|
||
└── units.py
|
||
tests/ unittest: pro Modul + End-to-End (in-process, HTTP, stdio, Pool, Footprint)
|
||
examples/ lauffähiger MCP-Client
|
||
scripts/ measure_memory.py – Speicher messen
|
||
docs/tools.md vollständige Tool-Referenz
|
||
docs/memory.md Speicheranalyse und Dimensionierung
|
||
docs/auth.md Authentifizierung, Client-Verwaltung, Überlastschutz (Betriebshandbuch)
|
||
uv.lock Lockfile (uv), Quelle der Wahrheit für Versionen
|
||
requirements.lock daraus exportiert, mit Hashes, für pip/Container-Builds
|
||
BACKLOG.md geplante Features und offene Themen
|
||
```
|
||
|
||
### Neues Tool hinzufügen
|
||
|
||
1. Eine reine Funktion in `ops/<bereich>.py` schreiben: Parameter rein, JSON-fähiges `dict` raus.
|
||
Erwartbare Fehler als `MathError("verständliche Meldung")` werfen. Bei einem neuen Modul dieses in
|
||
`executor.TASK_MODULES` eintragen.
|
||
2. Enums/Literal-Typen für die Parameter in `ops/spec.py` definieren, **nicht** im Rechenmodul.
|
||
`server.py` darf nur `spec` importieren, sonst landen SymPy/SciPy wieder im Server-Prozess
|
||
(`tests/test_footprint.py` schlägt dann fehl).
|
||
3. In `server.py` ein `@mcp.tool(annotations=READ_ONLY)` mit typisierten, per `Field(description=…)`
|
||
beschriebenen Parametern anlegen, das `await run("<name>", "<modul>.<funktion>", **kwargs)` aufruft.
|
||
4. Das Tool in `INSTRUCTIONS` (Abschnitt „Which tool“) und in `docs/tools.md` ergänzen.
|
||
5. Tests schreiben: Fachlogik direkt in `tests/test_<bereich>.py`, Aufruf über MCP in `tests/test_server.py`
|
||
(dort auch die Liste `EXPECTED_TOOLS` anpassen).
|
||
|
||
---
|
||
|
||
## Entwicklung
|
||
|
||
```bash
|
||
source .venv/bin/activate
|
||
python -m unittest # alle Tests
|
||
MCP_CALC_SKIP_SLOW=1 python -m unittest # schnelle Tests ohne Subprozesse
|
||
python -m unittest tests.test_parsing -v # nur Parser/Sicherheit
|
||
python -m unittest tests.test_algebra.SolveTest.test_quadratic # ein einzelner Test
|
||
ruff check src tests examples scripts # Linting (Konfiguration in pyproject.toml)
|
||
MCP_CALC_EXECUTOR=inline MCP_CALC_LOG_LEVEL=DEBUG mcp-calculator # Debugging ohne Worker-Pool
|
||
```
|
||
|
||
**Abhängigkeiten aktualisieren.** `pyproject.toml` enthält die erlaubten Bereiche, `uv.lock` die exakt
|
||
getesteten Versionen. Nach einer Aktualisierung immer beide Lock-Dateien committen:
|
||
|
||
```bash
|
||
uv lock --upgrade # oder gezielt: uv lock --upgrade-package mcp
|
||
uv export --frozen --format requirements-txt --no-emit-project --no-header -o requirements.lock
|
||
uv sync --frozen --extra dev --python 3.12 && python -m unittest
|
||
```
|
||
|
||
> **Hinweis zur Python-Version:** Ist in der Shell `UV_PYTHON` gesetzt (z. B. `UV_PYTHON=3.13`), hat das bei
|
||
> uv Vorrang vor `.python-version`. Deshalb steht in den Befehlen oben `--python 3.12` explizit.
|
||
|
||
Code, Docstrings und Tool-Beschreibungen sind auf Englisch. Das ist die Sprache, mit der LLMs
|
||
Tool-Schemas am zuverlässigsten interpretieren. Die Projektdokumentation ist auf Deutsch.
|
||
|
||
---
|
||
|
||
## Grenzen
|
||
|
||
- **Formulierung bleibt beim LLM.** Der Server rechnet korrekt, aber das Modell muss die Aufgabe richtig in einen Aufruf übersetzen. Diese Fehlerquelle bleibt.
|
||
- **Symbolik hat Grenzen.** Nicht jedes Integral, jede Gleichung und jede DGL hat eine geschlossene Form. Der Server fällt dann auf numerische Verfahren zurück (Integral, Summe) oder gibt einen Hinweis (`numeric_only`). Aufwendige Probleme laufen in den Timeout.
|
||
- **Numerisches Lösen** (`numeric_only`) liefert bei nicht-polynomialen Gleichungen eine Wurzel nahe dem Startwert, nicht alle.
|
||
- **Speicherlimit pro Worker** greift nur unter Linux. Unter macOS gilt nur der Timeout. Die Speicherwerte in der Doku sind unter macOS gemessen; im Container bitte mit `scripts/measure_memory.py` nachmessen.
|
||
- **Keine Plots**, keine Finanzmathematik-Spezialfunktionen (IRR, Annuitäten lassen sich über `evaluate`/`solve` abbilden). Geplant siehe [BACKLOG.md](BACKLOG.md).
|
||
- **Warteschlange pro Replica.** `MAX_QUEUE` begrenzt nur den einzelnen Pod; Lastverteilung über mehrere Replicas übernimmt der Service/Router.
|