27 KiB
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 2.x,
SymPy, mpmath, NumPy, SciPy, Pint, Pebble, prometheus-client. Tests mit unittest (Standardbibliothek).
Offene Ideen und geplante Features: BACKLOG.md.
Inhalt
- Schnellstart
- Tools im Überblick
- Transporte
- Clients anbinden
- Konfiguration
- Sicherheit
- Betrieb
- Monitoring
- Architektur
- Entwicklung
- Grenzen
Die ausführliche Referenz aller Parameter steht in docs/tools.md.
Schnellstart
Alles wird ausschließlich im virtuellen Environment des Projekts installiert, exakt in den Versionen aus dem Lockfile.
# 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:
# 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-
instructionserklären beim Verbindungsaufbau, welches Tool wofür gedacht ist, und beschreiben die Syntax. - Verkettbare Ergebnisse:
resulthat dieselbe Syntax wie die Eingabe. Eine Ableitung lässt sich also direkt ansolveweitergeben. - Exakt statt Gleitkomma:
0.1 + 0.2 = 3/10. Dezimalwerte kommen als String mit der gewünschten Stellenzahl zurück. - Selbsterklärende Ergebnisse:
meaningbei Wahrscheinlichkeiten (P(X > 7) = P(X >= 8)),conclusionbei Tests,notebei 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 |
| 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.
# 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:
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.
opencode
opencode.json (Projekt) oder ~/.config/opencode/opencode.json (global):
{
"$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:
{
"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):
{
"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:
{
"mcpServers": {
"calculator": {
"command": "/pfad/zu/mcp-calculator/.venv/bin/mcp-calculator",
"args": []
}
}
}
Claude Code
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:
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:
list_tools()ausführen undname,descriptionundinput_schemajedes Tools als Tool-Definition an das Modell geben.- Fordert das Modell einen Tool-Call an,
call_tool(name, arguments)aufrufen. result.content[0].textzurückgeben. Beiresult.is_errorden 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. 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 | |
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 |
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:
- Die Eingabe wird vorab geprüft: Zeichen-Whitelist (keine Quotes,
[],{},;,:,\), kein__, kein Attributzugriff (.name), keine Python-Keywords, keine Namen mit_. - 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. - 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^9blockiert also nichts. - Unter Linux darf eine Berechnung pro Worker höchstens
MCP_CALC_MEMORY_LIMIT_MBzusätzlich belegen (RLIMIT_AS, gesetzt nach dem Laden der Bibliotheken). Ein Ausreißer endet mitMemoryError, 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.
- 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,/readyzund/metricsbleiben 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.1automatisch aktiv, bei0.0.0.0überMCP_CALC_ALLOWED_HOSTSeinschaltbar. - 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 |
| 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 |
| 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 |
| 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:
# 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) 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
- Eine reine Funktion in
ops/<bereich>.pyschreiben: Parameter rein, JSON-fähigesdictraus. Erwartbare Fehler alsMathError("verständliche Meldung")werfen. Bei einem neuen Modul dieses inexecutor.TASK_MODULESeintragen. - Enums/Literal-Typen für die Parameter in
ops/spec.pydefinieren, nicht im Rechenmodul.server.pydarf nurspecimportieren, sonst landen SymPy/SciPy wieder im Server-Prozess (tests/test_footprint.pyschlägt dann fehl). - In
server.pyein@mcp.tool(annotations=READ_ONLY)mit typisierten, perField(description=…)beschriebenen Parametern anlegen, dasawait run("<name>", "<modul>.<funktion>", **kwargs)aufruft. - Das Tool in
INSTRUCTIONS(Abschnitt „Which tool“) und indocs/tools.mdergänzen. - Tests schreiben: Fachlogik direkt in
tests/test_<bereich>.py, Aufruf über MCP intests/test_server.py(dort auch die ListeEXPECTED_TOOLSanpassen).
Entwicklung
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:
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_PYTHONgesetzt (z. B.UV_PYTHON=3.13), hat das bei uv Vorrang vor.python-version. Deshalb steht in den Befehlen oben--python 3.12explizit.
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.pynachmessen. - Keine Plots, keine Finanzmathematik-Spezialfunktionen (IRR, Annuitäten lassen sich über
evaluate/solveabbilden). Geplant siehe BACKLOG.md. - Warteschlange pro Replica.
MAX_QUEUEbegrenzt nur den einzelnen Pod; Lastverteilung über mehrere Replicas übernimmt der Service/Router.