MCP für Code-Dokumentation: Ein Nachschlagewerk für Legacy-Projekte
Wie ich mir eine große Perl-Codebase erschlossen habe
Ich arbeite seit einiger Zeit an einem größeren Projekt. Die Codebase ist in Perl geschrieben, gewachsen über viele Jahre, komplex und umfangreich. Die Herausforderung: Über 1.300 Module, sehr viele Datenbanktabellen, gewachsene Geschäftslogik. So sieht Software aus, die lange lebt und sich bewährt hat. Aber wie arbeitet man sich da ein?
Daher habe ich mir im Lauf der Zeit ein paar Tools gebaut, um diesen Code besser zu verstehen und effektiver mit ihm arbeiten zu können. Und nun verbinde ich diese einzelnen Werkzeuge mit einer KI, um das Optimum aus ihnen herauszuholen.
Das Problem
Die üblichen Wege:
- Code lesen – geht, aber bei der Menge verliert man schnell den Überblick
- Kollegen fragen – hilft, aber nicht für jede Detailfrage
- Dokumentation lesen – existiert teilweise, aber lückenhaft und manchmal veraltet
Was mir fehlte: Ein persönliches Nachschlagewerk. Etwas, das ich selbst aufbaue, in meinen Worten, das mit meinem Verständnis wächst.
Die Idee: Obsidian als Wissensbasis
Ich nutze Obsidian für Notizen. Markdown-Dateien, lokal, vernetzt durch Links. Perfekt für eine Code-Dokumentation:
- Portabel – nur Textdateien, kein Lock-in
- Verlinkbar – Module verweisen aufeinander
- Durchsuchbar – Volltextsuche über alles
- Versionierbar – kann ins Git
Die Struktur, die mir vorschwebt:
docs/
├── modules/
│ ├── User.md
│ ├── Order.md
│ └── ...
├── tables/
│ ├── users.md
│ ├── orders.md
│ └── ...
├── flows/
│ ├── checkout.md
│ └── ...
└── glossar.md
Jedes Modul bekommt eine Seite. Jede wichtige Tabelle. Wichtige Abläufe als eigene Dokumente. Alles verlinkt.
Warum MCP?
Das händisch zu machen wäre Sisyphusarbeit. Bei 1.300 Modulen würde ich Jahre brauchen – und wäre nie fertig, weil sich der Code weiterentwickelt.
Die Idee: Ein MCP-Tool, das mir hilft. Nicht automatisch generierte Dokumentation (die ist meist wertlos), sondern ein Assistent beim Dokumentieren:
- Ich frage: “Was macht das Modul
Order::Validation?” - Claude liest den Code, erklärt ihn mir
- Ich formuliere das in meinen Worten
- Das Tool schreibt es als Markdown-Datei
Später kann ich fragen: “Hat sich Order::Validation seit meiner letzten Dokumentation geändert?” – und das Tool vergleicht.
Der erste Ansatz
Ein MCP-Server mit diesen Fähigkeiten:
Code lesen und analysieren
from pathlib import Path
PROJECT_ROOT = Path("/path/to/project")
DOCS_ROOT = Path.home() / "Documents" / "project-docs"
@mcp.tool()
def read_module(module_name: str) -> str:
"""Liest ein Perl-Modul und gibt den Inhalt zurück."""
# Module::Name -> Module/Name.pm
path = module_name.replace("::", "/") + ".pm"
full_path = PROJECT_ROOT / "lib" / path
if not full_path.exists():
return f"Modul nicht gefunden: {module_name}"
content = full_path.read_text()
# Bei sehr langen Dateien kürzen
if len(content) > 15000:
content = content[:15000] + "\n\n... (gekürzt, Datei hat {len(content)} Zeichen)"
return content
@mcp.tool()
def find_modules(pattern: str) -> str:
"""Findet Module die einem Muster entsprechen."""
lib_path = PROJECT_ROOT / "lib"
matches = list(lib_path.rglob(f"*{pattern}*.pm"))
if not matches:
return f"Keine Module gefunden für: {pattern}"
# Pfade zu Modulnamen konvertieren
modules = []
for m in sorted(matches)[:30]:
rel = m.relative_to(lib_path)
name = str(rel.with_suffix("")).replace("/", "::")
modules.append(name)
return "\n".join(modules)
@mcp.tool()
def module_dependencies(module_name: str) -> str:
"""Zeigt welche Module ein Modul verwendet (use/require)."""
path = module_name.replace("::", "/") + ".pm"
full_path = PROJECT_ROOT / "lib" / path
if not full_path.exists():
return f"Modul nicht gefunden: {module_name}"
content = full_path.read_text()
# Einfaches Regex für use/require Statements
import re
uses = re.findall(r'^use\s+([\w:]+)', content, re.MULTILINE)
requires = re.findall(r'^require\s+([\w:]+)', content, re.MULTILINE)
deps = sorted(set(uses + requires))
if not deps:
return "Keine Abhängigkeiten gefunden"
return "\n".join(deps)
Dokumentation schreiben
@mcp.tool()
def write_doc(doc_type: str, name: str, content: str) -> str:
"""Schreibt eine Dokumentations-Datei.
doc_type: 'module', 'table', 'flow' oder 'note'
name: Name der Datei (ohne .md)
content: Markdown-Inhalt
"""
valid_types = ["module", "table", "flow", "note"]
if doc_type not in valid_types:
return f"Ungültiger Typ. Erlaubt: {valid_types}"
folder = DOCS_ROOT / f"{doc_type}s"
folder.mkdir(parents=True, exist_ok=True)
# Dateiname bereinigen
safe_name = name.replace("::", "_").replace("/", "_")
filepath = folder / f"{safe_name}.md"
filepath.write_text(content)
return f"Geschrieben: {filepath}"
@mcp.tool()
def read_doc(doc_type: str, name: str) -> str:
"""Liest eine existierende Dokumentations-Datei."""
safe_name = name.replace("::", "_").replace("/", "_")
filepath = DOCS_ROOT / f"{doc_type}s" / f"{safe_name}.md"
if not filepath.exists():
return f"Dokumentation nicht gefunden: {filepath}"
return filepath.read_text()
@mcp.tool()
def list_docs(doc_type: str = None) -> str:
"""Listet vorhandene Dokumentation auf."""
if doc_type:
folder = DOCS_ROOT / f"{doc_type}s"
if not folder.exists():
return f"Keine Dokumentation vom Typ: {doc_type}"
files = sorted(folder.glob("*.md"))
else:
files = sorted(DOCS_ROOT.rglob("*.md"))
if not files:
return "Keine Dokumentation vorhanden"
return "\n".join(f.stem for f in files[:50])
Änderungen erkennen
import hashlib
import json
HASH_FILE = DOCS_ROOT / ".module_hashes.json"
@mcp.tool()
def check_changes(module_name: str) -> str:
"""Prüft ob sich ein Modul seit der letzten Dokumentation geändert hat."""
path = module_name.replace("::", "/") + ".pm"
full_path = PROJECT_ROOT / "lib" / path
if not full_path.exists():
return f"Modul nicht gefunden: {module_name}"
# Aktuellen Hash berechnen
content = full_path.read_text()
current_hash = hashlib.md5(content.encode()).hexdigest()
# Gespeicherten Hash laden
hashes = {}
if HASH_FILE.exists():
hashes = json.loads(HASH_FILE.read_text())
stored_hash = hashes.get(module_name)
if stored_hash is None:
return f"{module_name}: Noch nie dokumentiert"
elif stored_hash != current_hash:
return f"{module_name}: GEÄNDERT seit letzter Dokumentation"
else:
return f"{module_name}: Unverändert"
@mcp.tool()
def mark_documented(module_name: str) -> str:
"""Markiert ein Modul als dokumentiert (speichert Hash)."""
path = module_name.replace("::", "/") + ".pm"
full_path = PROJECT_ROOT / "lib" / path
if not full_path.exists():
return f"Modul nicht gefunden: {module_name}"
content = full_path.read_text()
current_hash = hashlib.md5(content.encode()).hexdigest()
hashes = {}
if HASH_FILE.exists():
hashes = json.loads(HASH_FILE.read_text())
hashes[module_name] = current_hash
HASH_FILE.write_text(json.dumps(hashes, indent=2))
return f"{module_name}: Als dokumentiert markiert"
Der Workflow
So stelle ich mir die tägliche Arbeit vor:
Neues Modul erschließen:
“Zeig mir das Modul
Order::Validation”
Claude liest den Code und erklärt die Struktur, die wichtigsten Funktionen, die Abhängigkeiten.
“Was macht die Funktion
validate_payment?”
Detailliertere Erklärung.
“Schreib das als Dokumentation”
Claude erstellt ein Markdown-Dokument mit Zusammenfassung, wichtigen Funktionen, Abhängigkeiten, Beispielen.
Später, nach Code-Änderungen:
“Welche Module haben sich seit meiner Dokumentation geändert?”
Das Tool vergleicht Hashes und zeigt, wo ich nacharbeiten sollte.
Template für Module
Damit die Dokumentation einheitlich aussieht, ein Template:
# {Modulname}
**Pfad:** `lib/Order/Validation.pm`
**Letzte Dokumentation:** 2025-12-29
**Status:** Aktuell
## Zweck
Kurze Beschreibung, was das Modul macht.
## Wichtige Funktionen
### `validate_order($order)`
Validiert eine Bestellung vor dem Checkout.
- **Parameter:** `$order` - Order-Objekt
- **Rückgabe:** `1` bei Erfolg, `0` bei Fehlern
- **Seiteneffekte:** Setzt `$order->errors` bei Validierungsfehlern
### `validate_payment($payment_data)`
...
## Abhängigkeiten
- [[Order::Base]] - Basisklasse
- [[Payment::Gateway]] - Zahlungsanbindung
- [[Database::Orders]] - Datenbankzugriff
## Verwendet von
- [[Checkout::Process]]
- [[API::Orders]]
## Notizen
- Achtung: Validierung für Sonderfälle (Gutscheine > Warenwert) ist in `_handle_edge_cases`
- Die Funktion `_legacy_validate` wird noch für alte API-Clients gebraucht
Die [[...]] sind Obsidian-Links. So entsteht ein Netz aus verlinkten Dokumenten.
Was noch fehlt
Das ist der erste Ansatz. Was ich noch bauen will:
- Datenbank-Schema – Tabellen dokumentieren, Beziehungen visualisieren
- Suche über Dokumentation – “Wo ist die Gutschein-Logik dokumentiert?”
- Diff-Ansicht – Was genau hat sich geändert?
Bulk-Check – Alle Module auf Änderungen prüfen✓ implementiert
Erfahrungen bisher
Nach den ersten Wochen:
Was funktioniert:
- Claude versteht Perl erstaunlich gut
- Die Erklärungen sind meist korrekt und hilfreich
- Die Verlinkung in Obsidian macht Navigation einfach
Was schwierig ist:
- Sehr lange Module (>2000 Zeilen) – Context-Limit
- Implizite Abhängigkeiten (dynamische Modul-Ladung)
- Business-Kontext – den muss ich selbst einbringen
Wichtig: Claude erklärt den Code, aber ich muss verstehen und formulieren. Die Dokumentation ist meine, nicht generiert. Das Tool hilft, ersetzt aber nicht das eigene Denken.
Der Aufwand für den Server war überschaubar – ein Nachmittag. Der Nutzen ist täglich spürbar. Und das Beste: Die Dokumentation gehört mir, liegt als Markdown lokal, und kann bei Projektende übergeben werden.
Disclaimer: Der Code ist ein Proof of Concept aus meiner täglichen Arbeit. Funktioniert für meine Zwecke, keine Garantie für andere Setups. Die Pfade und Strukturen müssen natürlich angepasst werden.
GitHub: cuber-it/mcp_doku_tool
Aktualisiert: 2025-01-03