# Tooling Server - Design Document
**Datum:** 2025-12-03
**Status:** Entwurf validiert
---
## 1. Übersicht
### Problem
Claude Code lädt alle Tool-Definitionen von MCP-Servern in den Context. Bei 100+ Tools führt das zu:
- ~50.000 Token Verbrauch bevor die Arbeit beginnt
- Schlechtere Tool-Auswahl durch Überladung
- Langsame Responses
### Lösung
Ein **Tooling Server** der als Meta-MCP-Server fungiert:
- Claude Code sieht nur 1 Tool: `execute_task`
- Der Tooling Server verwaltet 100+ Tools intern
- Hierarchische Navigation mit günstigem/schnellem LLM (Cerebras/Gemini)
- Nur Ergebnisse werden zurückgegeben
```
┌─────────────┐ 1 Tool ┌──────────────────┐ 100+ Tools ┌─────────────┐
│ Claude Code │ ◄──────────────► │ Tooling Server │ ◄──────────────► │ MCP Servers │
└─────────────┘ (nur Ergebnis) └──────────────────┘ (intern) └─────────────┘
```
---
## 2. Architektur
### Komponenten
```
┌─────────────────────────────────────────────────────────────────┐
│ TOOLING SERVER │
│ (Python MCP-Server) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CATALOG │ │ NAVIGATOR │ │ EXECUTOR │ │
│ │ │ │ │ │ │ │
│ │ - Hierarchie│ ──► │ - LLM Calls │ ──► │ - MCP Client│ │
│ │ - Verzeichnis │ - Schritt- │ │ - Subprocess│ │
│ │ - Validation│ │ weise │ │ - Response │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
| Komponente | Verantwortung |
|------------|---------------|
| **Catalog** | Lädt Tool-Hierarchie aus Verzeichnisstruktur |
| **Navigator** | LLM-gesteuerte Navigation durch die Hierarchie |
| **Executor** | Verbindet zu MCP-Servern und führt Tools aus |
### Schnittstellen
- **MCP-Server:** Claude Code ruft `execute_task(task)` auf
- **CLI:** Direktes Testen via `tooling-server run "..."`
---
## 3. Ablauf
```
👤 User: "Schick eine Nachricht an #general in Slack: Hallo Team!"
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLAUDE CODE │
│ Ruft auf: execute_task("Schick Nachricht an #general...") │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ TOOLING SERVER │
│ │
│ Schritt 1: Kategorie wählen (LLM) │
│ → "communication" │
│ │
│ Schritt 2: Service wählen (LLM) │
│ → "slack" │
│ │
│ Schritt 3: Tool wählen (LLM) │
│ → "send_message" │
│ │
│ Schritt 4: Parameter extrahieren (LLM) │
│ → {channel: "#general", text: "Hallo Team!"} │
│ │
│ Schritt 5: Tool ausführen (MCP) │
│ → slack-mcp.send_message(...) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ZURÜCK ZU CLAUDE CODE │
│ {success: true, tool_used: "slack.send_message", result: {...}} │
└─────────────────────────────────────────────────────────────────┘
```
### Adaptive Tiefe
Ebenen werden übersprungen wenn nur 1 Option existiert:
```
Beispiel: "PostgreSQL Query ausführen"
├── Kategorien: 5 Optionen → LLM wählt "database"
├── Services: 1 Option (postgres) → AUTOMATISCH
├── Tools: 1 Option (query) → AUTOMATISCH
└── Gesamt: 1 LLM-Call statt 3
```
---
## 4. Katalog-Struktur
Die Verzeichnisstruktur IST der Katalog:
```
catalog/
├── _config.yaml # Globale Einstellungen
│
├── communication/
│ ├── _category.yaml # description: "Messaging, Chat..."
│ │
│ ├── slack/
│ │ ├── _service.yaml # mcp_server: "slack-mcp"
│ │ ├── send_message.yaml
│ │ ├── list_channels.yaml
│ │ └── search_messages.yaml
│ │
│ └── email/
│ ├── _service.yaml
│ └── send_email.yaml
│
├── database/
│ ├── _category.yaml
│ └── postgres/
│ ├── _service.yaml
│ └── query.yaml
│
└── files/
├── _category.yaml
└── gdrive/
├── _service.yaml
├── upload.yaml
└── download.yaml
```
### Tool-Definition (Beispiel)
```yaml
# catalog/communication/slack/send_message.yaml
description: "Nachricht an Channel oder User senden"
parameters:
channel:
type: string
required: true
description: "Channel-Name (#general) oder User-ID"
text:
type: string
required: true
description: "Nachrichtentext"
thread_ts:
type: string
required: false
description: "Thread-ID für Antworten"
```
### Service-Definition (Beispiel)
```yaml
# catalog/communication/slack/_service.yaml
description: "Team-Kommunikation via Slack"
mcp_server: slack-mcp
```
### MCP-Server-Konfiguration
```yaml
# catalog/_config.yaml
mcp_servers:
slack-mcp:
command: "npx"
args: ["-y", "@anthropic/slack-mcp"]
env:
SLACK_TOKEN: "${SLACK_TOKEN}"
gmail-mcp:
command: "python"
args: ["-m", "gmail_mcp_server"]
postgres-mcp:
command: "postgres-mcp-server"
args: ["--connection-string", "${DATABASE_URL}"]
```
---
## 5. Technische Details
### Navigator
```python
class Navigator:
async def navigate(self, task: str) -> SelectedTool:
category = await self._select_level(task, "category")
service = await self._select_level(task, "service", category)
tool = await self._select_level(task, "tool", category, service)
parameters = await self._extract_parameters(task, tool)
return SelectedTool(category, service, tool, parameters)
async def _select_level(self, task, level_type, *path) -> str:
options = self._read_options(path)
# Nur 1 Option? Automatisch wählen
if len(options) == 1:
return options[0].name
# LLM fragen
return await self.llm.ask(
f"Aufgabe: {task}\n"
f"Welche {level_type}?\n"
f"{self._format_options(options)}"
)
```
### Executor
```python
class Executor:
connections: dict[str, MCPConnection] = {}
async def execute(self, selected: SelectedTool) -> ExecutionResult:
connection = await self._get_connection(selected.mcp_server)
result = await connection.call_tool(
name=selected.tool,
arguments=selected.parameters
)
return ExecutionResult(success=True, result=result)
async def _get_connection(self, server_name: str) -> MCPConnection:
# Lazy Loading: Server nur starten wenn nötig
if server_name not in self.connections:
config = self.mcp_configs[server_name]
self.connections[server_name] = await MCPConnection.create(
command=config["command"],
args=config["args"]
)
return self.connections[server_name]
```
### MCP-Schnittstelle
```python
from mcp import Server
server = Server("tooling-server")
@server.tool()
async def execute_task(task: str) -> dict:
"""
Führt eine Aufgabe mit dem passenden Tool aus.
Args:
task: Natürlichsprachliche Beschreibung
"""
selected = await navigator.navigate(task)
result = await executor.execute(selected)
return result.to_dict()
```
---
## 6. Fehlerbehandlung
| Fehler | Reaktion |
|--------|----------|
| LLM wählt ungültige Option | Retry mit Hinweis |
| Parameter unvollständig | Rückfrage an Claude Code |
| MCP-Server antwortet nicht | Retry mit Backoff, dann Fehler |
| Tool-Ausführung schlägt fehl | Fehler + Kontext zurück |
### Fehler-Response
```json
{
"success": false,
"tool_used": "slack.send_message",
"error": {
"type": "tool_execution_error",
"message": "Channel #general-old nicht gefunden",
"suggestion": "Verfügbare Channels: #general, #random, #dev"
}
}
```
---
## 7. Technologie-Stack
| Komponente | Technologie |
|------------|-------------|
| Sprache | Python 3.11+ |
| MCP SDK | `mcp` (Python) |
| LLM Provider | Cerebras / Gemini Flash |
| Konfiguration | YAML (Verzeichnisstruktur) |
| CLI | `click` oder `typer` |
| Async | `asyncio` |
---
## 8. Entscheidungen
| Entscheidung | Begründung |
|--------------|------------|
| **Verzeichnis statt YAML** | Übersichtlicher, Lazy Loading, isolierte Änderungen |
| **Subprocess für MCP** | Standard-Ansatz, einfach, später Docker möglich |
| **Stateless** | Claude Code hat Gedächtnis, Server braucht keins |
| **Adaptive Tiefe** | Schneller bei eindeutigen Fällen |
| **Cerebras/Gemini** | 10-100x günstiger als Claude für Selection |
---
## 9. Nächste Schritte
1. Projekt-Struktur aufsetzen
2. Catalog-Loader implementieren
3. Navigator mit LLM-Integration
4. Executor mit MCP-Client
5. MCP-Server-Interface
6. CLI für Testing
7. Beispiel-Katalog erstellen