mcp-calculator/README.md
2026-09-15 20:38:16 +02:00

523 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.