Regeln für Python-MCP-Server
Diese Seite fasst zusammen, wie wir auf ki.karlkratz.com MCP-Server in Python bauen. Grundlage sind die Architektur-Prinzipien aus /var/www/code/projektcharakteristika/ (DDD, Hexagonal, SRP, SoC, DRY, SOLID, Clean Code, Max-LoC), übersetzt auf die Eigenheiten des Model-Context-Protocol-stdio-Transports.
1. SDK und Transport
- Verwende das offizielle PyPI-Paket mcp, in der Praxis die FastMCP-Klasse für deklarative Tool-Registrierung.
- Default-Transport ist stdio, eine Subprocess-Instanz pro Client. HTTP-/SSE-Transport nur, wenn Remote-Zugriff explizit gewünscht ist.
- SDK-Version in einer Lockfile fixieren, keine "latest"-Floats in Production.
2. Goldene stdio-Regel
Keine ungewollten Schreibvorgänge auf stdout. Der stdio-Transport nutzt stdout exklusiv für JSON-RPC-Frames; jedes print() oder unbuffered Logging zerstört das Protokoll. Konsequenz:
- Logging immer auf stderr oder in eine Datei.
- Bibliotheken, die per Default auf stdout loggen (httpx-Logger, urllib3-Logger), im Composition-Root einmal explizit konfigurieren.
- Auch Banner, "Loading"-Anzeigen, Tracebacks gehören auf stderr.
3. Tool-Definition
Jedes Tool ist eine Funktion mit dem mcp.tool()-Decorator. Vier Pflichten:
- Name in snake_case, fachlich gewählt, ohne technische Präfixe (kein get_, do_).
- Docstring, der das Tool für das LLM beschreibt. Erster Satz ist die Kurzfassung; danach Argumentbedingungen, Wertgrenzen, Fehlersignale.
- Type-Hints für alle Parameter und den Return-Typ. Daraus generiert das SDK das JSON-Schema, das die KI zur Argument-Erzeugung sieht.
- Determinismus dokumentiert: List/Get sind seiteneffektfrei; Schreibvorgänge sind im Docstring klar als solche markiert.
4. Schichten: DDD und Hexagonal
Wie in /var/www/code/projektcharakteristika/architecture_layers.yaml beschrieben, übertragen auf Python:
| Schicht | Inhalt | Darf NICHT enthalten |
|---|---|---|
| domain/ | Dataclasses (frozen, slots), Domain-Errors, Ports als typing.Protocol | HTTP-Calls, DB-Treiber, JSON-Parsing, mcp-SDK-Imports |
| application/ | Use Cases, orchestrieren Ports, kein I/O | httpx, psycopg, Dateipfade hartcodiert |
| infrastructure/ | Adapter: HTTP-Clients, DB-Connections, Filesystem, externe APIs, Config-Loader | fachliche Regeln, Validierungs-Algorithmen |
| presentation/ | server.py, Tool-Funktionen, JSON-Mapper | fachliche Wahrheit, Domain-Regeln |
Abhängigkeiten zeigen nur nach innen, Infrastructure kennt Application, Application kennt Domain. Niemals umgekehrt.
5. Quality Principles (aus quality_principles.yaml)
- SRP, jedes Tool macht eine fachliche Sache. Kein update_or_create, kein list_with_optional_delete.
- SoC, die Tool-Funktion enthält keine SQL, kein Body-Mapping, keine Validation. Sie ruft eine Use-Case-Methode auf und reicht das Ergebnis weiter.
- DRY, Validierung (Längen, Regex, Whitelist) lebt an genau einer Stelle, in der Regel im Backend hinter der API. Der MCP-Server validiert NICHT zusätzlich.
- SOLID, Use Cases bekommen Ports via Konstruktor injiziert. Konkrete Adapter werden im Composition-Root (server.py) verdrahtet.
- Clean Code, explizite Namen, kurze Funktionen, keine Cleverness, Type-Hints überall.
6. Maximale Dateigröße (hart)
| Datei-Typ | Hartes Limit (LoC) |
|---|---|
| domain (Dataclasses, Errors, Ports) | 80 |
| application (Use Cases) | 120 |
| infrastructure (Adapter) | 200 |
| server.py (Composition + Tools) | 150 |
| Test-Dateien | 300 |
Überschreitung ist Blocker, bevor das Limit angehoben wird, prüfen ob die Datei mehrere Verantwortungen mischt.
7. Forbidden Dependencies
- Kein Application-Framework als Treiber des MCP, kein FastAPI, kein Flask, kein Django zum Tool-Routing. Diese Pakete sind in Infrastructure-Adaptern erlaubt, wenn das Backend sie spricht.
- Keine ORM-Magie in Use Cases, psycopg oder sqlalchemy.core sind OK, das Mapping bleibt in einer Repository-Klasse.
- Keine globalen Singletons, keine Module-Level-Seiteneffekte außer im Composition-Root.
8. Sicherheit und Sandbox
- Niemals Pfade aus Tool-Argumenten ohne Whitelist auf Dateioperationen anwenden, Path-Traversal-Schutz via realpath-Vergleich gegen einen erlaubten Root.
- Niemals subprocess oder eval mit ungeprüftem User-Input.
- Secrets liegen in /var/www/_credentials/<dienst>/credentials.json oder in ENV-Variablen, niemals im Code.
- Destruktive Tools (delete_, drop_, exec_) verlangen explizite Argumente, keine "all"-Variante per Default, kein implizites Cascade.
9. Fehlerbehandlung
Use Cases werfen Domain-Errors. Die Tool-Funktion fängt sie ab und mappt sie auf MCP-Fehlerantworten. Eine generische except Exception ist nur an genau einer Stelle erlaubt: am äußersten Rand. Stack-Traces gehen auf stderr, NICHT in die Tool-Antwort, damit das LLM keine internen Pfade sieht.
10. Schema-Akkuratesse
Das vom SDK aus den Type-Hints generierte JSON-Schema wird dem LLM gezeigt und definiert, welche Argumente es schicken darf. Konsequenz:
- Optionale Parameter mit sinnvollen Defaults annotieren, sonst kann das LLM sie nicht zuverlässig auslassen.
- Enum-Werte als Literal[...] kennzeichnen, damit das LLM nur gültige Optionen sieht.
- Return-Typen so konkret wie möglich, kein Any als Default, sonst bleiben Antworten unlesbar.
11. Tests
- Use Cases werden mit Fake-Adaptern getestet, keine echten HTTP- oder DB-Calls.
- Schema-Tests prüfen, dass tools/list das erwartete Schema liefert.
- Integration-Test fährt den Server als Subprocess hoch und schickt einen echten initialize+tools/call-Roundtrip.
12. Verzeichnis-Layout (Referenz)
| Pfad | Inhalt |
|---|---|
| server.py | Composition-Root: instanziiert Config, Adapter, Use Cases; registriert Tools |
| domain/ | Dataclasses, Errors, Ports (typing.Protocol) |
| application/ | Use Cases, dünne Orchestrierung |
| infrastructure/ | HTTP-Clients, DB-Adapter, Config-Loader |
| tests/ | pytest mit Fakes |
| pyproject.toml | Metadaten, Versionen via Lockfile fixiert |
13. Quellen
- modelcontextprotocol.io, Spec, Reference Implementations
- github.com/modelcontextprotocol/python-sdk, offizielles Python-SDK
- /var/www/code/projektcharakteristika/quality_principles.yaml, SRP, SoC, DRY, SOLID, Clean Code, MaxLoC
- /var/www/code/projektcharakteristika/rules.yaml, projektweite Architektur-Hardregeln
- /var/www/code/projektcharakteristika/architecture_layers.yaml, Schicht-Definitionen
- /var/www/code/projektcharakteristika/forbidden_dependencies.yaml, verbotene Pakete und Framework-Kategorien