Master/Slave-System mit MCP-Gateway und LM Studio

Zusammenfassung

Ein Min-Master/Slave-Setup, das die teure, in der Wolke laufende Agenten-Instanz („Master”) entlastet: Der Master plant, delegiert, verifiziert und committet; ein günstiger lokaler Slave (Muse Glimmer 28B in LM Studio) erledigt die eigentliche Implementierungsarbeit nach Möglichkeit selbst. Die Kommunikation läuft über ein MCP-Gateway auf Port 3000, das den Slave über LM Studios OpenAI-kompatible API (localhost:1234) adressiert. Kern der Architektur: eine aufgabengebundene Tool-Freigabe und ein Reasoning-fester Tool-Loop – beide entscheidend, damit der Slave Werkzeuge tatsächlich zuverlässig aufruft.

Kernregeln (Cheat-Sheet)

Damit die Delegation den Ablauf nicht stört — die wichtigsten Regeln:

  1. Nie parallel delegieren. Mehrere gleichzeitige delegate_to_slave-Calls teilen sich dieselben GPU-Slots, bremsen sich gegenseitig und führen zu Timeouts (-32001). Immer sequenziell, ein Call nach dem anderen.
  2. Jeder Aufruf klein halten (≈ 1–3 Tool-Calls, eine konkrete Datei bzw. Aufgabe). Große mehrschrittige Implementierungen slice-weise delegieren.
  3. Keine künstlichen Token-Limits. max_tokens großzügig (Standard 8192, bei umfangreichen Tasks höher), Timeout nicht unnötig senken. Zu knappe Zuteilung bricht die Abarbeitung ab → unvollständiges Ergebnis → doppelte Kosten durch Neustart. Lieber ein Aufruf mehr als ein abgebrochener.
  4. Gateway-Neustart nur bei nachgewiesenem Hänger. Kein Neustart als „Re-Init” bei jedem Problem — erst diagnostizieren (Service-Status, Health-Checks, Log, LM-Studio-Modelle). Jeder Neustart invalidiert die Client-SSE-Session (-32602 → opencode neu verbinden).

Stand 2026-08-30: Endlosschleifen-Schutz im Slave-Tool-Loop + Wiki-Freigabe

Nach einem Vorfall, bei dem ein delegierter Slave nicht zur finalen Antwort fand, sondern immer wieder dieselben Tool-Calls emittierte (Stundenlauf bis zum 1-h-Gesamtlimit), wurde modules/orchestrator.py gehärtet:

  1. Wiki-Tools für general-Slave freigeschaltet. wiki_suche, wiki_lesen, wiki_uebersicht (read-only, SAFE_SLAVE_MODULES) stehen dem general-Typ jetzt in SLAVE_TOOLS_BY_TYPE zur Verfügung. Vorher gehörten sie in kein Slave-Tool-Set — ein Slave, der das Wiki lesen sollte, generierte ins Leere, weil er kein passendes Werkzeug sah.
  2. MAX_STALLED_ROUNDS=3 – identische Wiederholungen brechen sofort ab. Emittiert das Modell in Folge dieselben Tool-Calls (Name + Argumente), wird nach 3× abgebrochen („Tool-Schleife ohne Fortschritt"). Das ist das exakte Endlosschleifen-Bild von glimmer (immer wieder dasselbe nachschlagen).
  3. MAX_TOOL_ONLY_ROUNDS=12 – reine Tool-Folge ohne finale Antwort bricht ab. Läuft ein Slave über 12 aufeinanderfolgende Runden, deren einzige Ausgabe Tool-Calls sind (ohne je zu content zu kommen), wird abgebrochen („Tool-Only-Dauerschleife"). Verschiedene, legitime Tool-Sequenzen in Folge sind bis 12 Runden erlaubt; reasoning-/content-Runden resetteten die Zähler (stalled_rounds, prev_tool_signature, tool_only_rounds), damit Nicht-Tool-Runden die Direkt-Folge unterbrechen.

Alle drei Änderungen sind durch isolierte Loop-Simulationen verifiziert (identische Wiederholung → Abbruch nach 4 Requests; Tool-Only-Folge → Abbruch nach 12; ein Tool-Call + finale Antwort → normaler Erfolg in 2 Runden). Beeinflusste Datei: nur modules/orchestrator.pyreload_modules genügt, kein Gateway-Neustart, keine Client-Neuverbindung nötig.

Stand 2026-08-29: Härtung (alle Feinschliffe live in gateway.py/orchestrator.py/wiki.py)

Neue Garantien, umgesetzt nach den Fehlschlägen des Session-Tages — jede Meldung hier ist durch einen Testlauf belegt:

  1. Concurrency = 1 (hart). _CALL_LOCK in gateway.py lässt nur EINEN gleichzeitigen Tool-Call zu (über alle Tools/Sessions hinweg). Ein zweiter eingehender Call wird sofort abgelehnt („Gateway beschaeftigt …”), es wird nichts gestapelt. Verifiziert: zwei parallele Calls → genau einer läuft, der zweite wird abgelehnt.
  2. Automatischer Abbruch bei weggefallenem Client. Die SSE-Schleife setzt bei Verbindungsende set_cancel(True); der Orchestrator prüft zwischen den Tool-Runden auf cancel_requested() und stoppt inklusive hartem Gesamtlimit. Kein Weiterlaufen bis Runde 100 mehr, wenn der Auftraggeber schon weg ist.
  3. reload_modules überlebt Reloads. Die Registrierung liegt jetzt IN load_modules() (vorher nur einmal beim Start → Selbstzerstörung nach dem ersten Reload). Verifiziert: zwei aufeinanderfolgende Reloads, Tool bleibt da. Konsequenz: Änderungen an modules/*.py brauchen keinen Neustart mehr; nur gateway.py selbst.
  4. wiki_lesen liefert Artikel vollständig. Die 8000-Zeichen-Kappung ist entfernt — zu kleine Limits zerstören den Sinn des Artikels. Verifiziert: dieser Artikel kommt komplett (≈16 000 Zeichen) ohne Kürzungshinweis.
  5. Delegation ist nachvollziehbar. Der Orchestrator loggt jede Delegation mit Start/Ende in journalctl: [delegat] START slave_type=… tools=[…] task=… Zeichen und [delegat] ENDE status=OK/FEHLER dauer=…s rounds=… antwort=… Zeichen sowie pro Runde [delegat] Runde N: Tool '…' -> … Zeichen. Damit ist ein -32001-Ausgang (Client-Wattfenster) nachträglich diagnizierbar.
  6. Großzügige Zeitgrenzen. MAX_READ_SECONDS = 3600, MAX_DELEGATE_SECONDS = 3600 (vorher 900/1500). Bei ~3,5 Tokens/s ist 900 s schon bei einer mittleren write-Aufgabe unterschritten — ein zu kleines Fenster kappte den Call mitten in der Generierung, obwohl der Slave die Token bereits erzeugt hatte. Disziplin bleibt trotzdem: kleine Slices (~1–3 Tool-Calls), ein Element der Kernregeln.

Praxis-Regel aus dem Vorfall: Ein großer Aufgabe (z. B. komplette Datei schreiben, >~500 Generierungs-Token) kann dennoch an der Gesamtdauer scheitern und verbrennt die Slaven-Token ohne Ergebnis. Für zuverlässige Delegations-Ergebnisse gilt weiter: Aufgaben in kleine, einzeln verifizierbare Scheiben teilen und einen Call pro Scheibe machen.

Inhalt

Systemaufbau

Master (Agent in der Cloud / teuer)
   │  delegiert (MCP)
   ▼
MCP-Gateway  :3000/sse   (mcps/gateway.py, lädt modules/*.py)
   │  lädt Module / hält Tool-Handler
   ▼
orchestrator.py  (Registry, Tool-Sets, Tool-Loop)
   │  adressiert den Slave über seine Modell-ID
   ▼
LM Studio  :1234/v1/chat/completions   (Modell: meta/muse-glimmer 28B)
  • Gateway-Prozess: mcps/gateway.py, läuft als systemd-User-Service (mcp-gateway.service). Lädt modules/*.py einmal beim Start – Änderungen an Modulen erfordern deshalb einen Neustart des Services.
  • Registry: AVAILABLE_SLAVES mappt Slave-Typen (coder, math, general) auf Modell-IDs – aktuell alle drei auf Muse Glimmer 28B. Adressierung erfolgt stets über die Modell-ID (model_id) im Request.
  • Modell-Ladung: Glimmer mit -c 16384 --gpu 0.85 (teils CPU-Offload). Parameter, Kontext, GPU-Anteil beeinflussen die Zuverlässigkeit.

Erstinstallation & Schnellstart

Voraussetzungen: Python 3.10+, ausreichend VRAM/RAM für ein 28B-Modell, LM Studio mit aktivierter OpenAI-kompatibler Server-API auf 127.0.0.1:1234 und geladenem meta/muse-glimmer.

# 1. Quellen liegen unter ~/Dokumente/daten/sandbox/aux/mcps
cd ~/Dokumente/daten/sandbox/aux/mcps
 
# 2. Python-Umgebung
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
 
# (Alternativ läuft das Komplett-Setup inkl. Systemdeps über ./setup.sh
# bzw. der Paket-Nachinstall über ./install_python.sh.)
# 3. systemd-User-Unit anlegen, installieren & aktivieren
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/mcp-gateway.service <<'EOF'
[Unit]
Description=MCP Gateway (SSE, Port 3000)
After=network-online.target
 
[Service]
Type=simple
WorkingDirectory=%h/Dokumente/daten/sandbox/aux/mcps
ExecStart=%h/Dokumente/daten/sandbox/aux/mcps/venv/bin/python %h/Dokumente/daten/sandbox/aux/mcps/gateway.py
Restart=on-failure
RestartSec=3
 
[Install]
WantedBy=default.target
EOF
 
systemctl --user daemon-reload
systemctl --user enable --now mcp-gateway.service
systemctl --user status mcp-gateway.service
# 4. Health-Check
curl -s --max-time 3 http://127.0.0.1:3000/sse        # → SSE-Endpoint-Feld
curl -s http://127.0.0.1:1234/v1/models                # → meta/muse-glimmer muss gelistet sein
journalctl --user -u mcp-gateway.service --since "1 min ago"   # „Slave-Tools geladen: N …"
# 5. Client (z. B. opencode) anbinden
#    ~/.config/opencode/opencode.jsonc → mcp.gateway (s. „Registrierung in opencode")

Komplett-Schnelltest (Ende-zu-Ende):

call_tool("delegate_to_slave", {
    "slave_type": "general",
    "task": "Antworte nur mit OK.",          # 1: Smoke-Test der Kette
    "max_tokens": 256,
})
call_tool("delegate_to_slave", {
    "slave_type": "coder",
    "task": "Liste Ordner X auf und zeige Datei Y",   # 2: Lese-Test
    "tool_names": ["ls", "cat"],
})
call_tool("delegate_to_slave", {
    "slave_type": "coder",
    "task": "Schreibe Datei X mit Inhalt Y",          # 3: Schreib-Test
    "tool_names": ["write"],
})

Bei jedem Schritt der Verifikation: Timing (siehe „Performance-Rahmenbindung”), nicht parallel delegieren, Beschreibung + Resultat prüfen.

Hinweis zum Testcode oben: max_tokens: 256 ist NUR für die Minimal-Antworten des Smoke-Tests („OK”) gedacht. Für echte Aufgaben den Standard 8192 verwenden und nicht künstlich senken (s. Kernregel 3).

Werkzeug-Auswahl für den Slave (wichtigste Erkenntnis)

Glimmer verhält sich beim Tool-Picking sehr empfindlich. Gemessene Regeln:

  • Kleine Tool-Sets sind zuverlässig, große nicht. Mit >13 Tools degradiert das Tool-Picking; schon bei 5 Datei-Tools (write, edit, ls, cat, grep) blieb der Tool-Call aus (leerer Content, keine tool_calls). Mit einem einzigen Tool ruft Glimmer es zuverlässig auf.
  • Konsequenz: Relevanz vor Vollständigkeit. Nur Tools anbieten, die zur konkreten Aufgabe passen. Seit 2026-08-29 sind für die Coder-Slaves Lese- und Schreib-Werkzeuge freigeschaltet (s. u.), damit der Slave selbstständig lesen und schreiben kann.

Daraus entstand die aufgabengebundene Tool-Freigabe: delegate_to_slave akzeptiert optional tool_names (Liste). Nur diese Werkzeuge sind für den Aufruf sichtbar; ohne Angabe gilt das Standard-Set des Slave-Typs.

# Beispiel: Slave soll einen Ordner lesen — er sieht genau `ls` und `cat`
call_tool("delegate_to_slave", {
    "slave_type": "coder",
    "task": "Liste Ordner X auf und zeige Datei Y",
    "tool_names": ["ls", "cat"],
})

Aktuelle Tool-Sets (Stand 2026-08-29)

Konfiguration in modules/orchestrator.py, SLAVE_TOOLS_BY_TYPE:

  • coder: ls, cat, grep, filesystem_info, write, edit, rechnen, statistik
  • general: ls, cat, grep, filesystem_info, write, edit
  • math: nur Rechen-/Konverter-Tools (rechnen, statistik, integriere, differenziere, alle Einheiten-Konverter)

Hintergrund: Solange coder/general nur write durften, konnte der Slave Projekt-Header nicht lesen und musste alle nötigen Infos eingebettet bekommen. Mit ls/cat/grep kann er selbst nachschlagen; edit erlaubt gezielte Ersetzungen statt Ganzdatei-Writes.

Der Reasoning-feste Tool-Loop

Zweite zentrale Erkenntnis: reasoning_content darf den Tool-Loop nicht beenden. Das Modell „denkt” (Reasoning) nach, bevor es den Tool-Call eines Drafts emittiert. Wurde dieses Reasoning fälschlich als finale Antwort gewertet, brach der Loop mit „0 Werkzeug-Aufrufe” ab, obwohl das Modell noch arbeiten wollte.

Der Fixed Loop (orchestrator.py, _chat_with_tools):

  • tool_calls vorhanden → ausführen, Ergebnis in den Verlauf legen, weiter.
  • content vorhanden (echte finale Textantwort) → Loop beenden.
  • nur reasoning_content, kein Content/Tool-Call → als Zwischenschritt weitertragen (Verlauf assistant + reasoning_content) und weiter iterieren; bei zu vielen aufeinanderfolgenden Reasoning-Schritten (MAX_REASONING_ROUNDS = 6) abbrechen.
  • leere Antwort → Warnung zurückgeben.

Grenzwerte: MAX_TOOL_ROUNDS = 100, Request-Timeout 900 s (MAX_REASONING_ROUNDS = 6).

Anwendung (Praxis-Muster)

Für Implementierungsarbeit im erlaubten Pfadrahmen:

  1. Aufgabe in kleine, einzeln verifizierbare Schritte teilen – ein synchroner MCP-Call soll nur ~1–3 Tool-Calls ausführen, damit er schnell durchläuft (keine 15-min-Calls).
  2. tool_names auf genau die nötigen Werkzeuge beschränken (z. B. ls+cat zum Nachlesen, nur write für eine Datei, edit für ein gezieltes Ersetzen).
  3. Der Slave kann seit 2026-08-29 selbst ls/cat/grep nutzen – präzise Pfade und Dateinamen in die Task schreiben, damit er gezielt nachschlägt. Vollständige Information (z. B. wörtlicher Header/API-Namen) einbetten, wenn er ohne Lese-Zugriff reine write-Aufgaben ausführen soll – sonst rät er Bezeichner.
  4. Der Master verifiziert das Ergebnis und korrigiert gezielt, dann committet er (Committen bleibt Master-Aufgabe).
  5. max_tokens/timeout großzügig (Standard 8192); ein Call, der wegen zu knapper Zuteilung abbricht, kostet mehr als ein sauberer zweiter.
  • Zuverlässige Delegation: Datei schreiben (write allein) → Slave erstellt die Datei selbst (im Test 2 Tool-Calls).
  • Grenze: Große, mehrschrittige Implementierungen überfordern einen einzelnen synchronen MCP-Call; lieber slice-weise delegieren.

Betrieb

Gateway als systemd-User-Service (mcp-gateway.service):

systemctl --user status  mcp-gateway.service   # läuft? letzte Requests
systemctl --user restart mcp-gateway.service   # nach Modul-/Konfig-Änderung
systemctl --user stop    mcp-gateway.service
systemctl --user start   mcp-gateway.service
journalctl --user -u mcp-gateway.service -f    # Live-Log
journalctl --user -u mcp-gateway.service --since "5 min ago"
  • Service-Datei: ~/.config/systemd/user/mcp-gateway.service (ExecStart=…/mcps/venv/bin/python …/gateway.py).
  • Health-Check:
    curl -s --max-time 3 http://127.0.0.1:3000/sse          # SSE-Endpoint erreichbar?
    curl -s http://127.0.0.1:1234/v1/models                  # LM Studio: welche Modelle geladen?
    Beim Start druckt der Prozess „Slave-Tools geladen: N Werkzeuge aus M Modulen”.

Diagnose vor Gateway-Neustart

Gateway-Neustarts stören laufende Clients (SSE-Session invalid → -32602). Deshalb prüfen, ob wirklich etwas hängt, bevor neu gestartet wird:

  1. systemctl --user is-active mcp-gateway.service → muss active sein.
  2. curl -s --max-time 3 http://127.0.0.1:3000/sse → liefert ein endpoint-Feld?
  3. curl -s --max-time 5 http://127.0.0.1:1234/v1/modelsmeta/muse-glimmer gelistet?
  4. journalctl --user -u mcp-gateway.service --since "5 min ago" → Tracebacks / Serverfehler?

Nur bei tatsächlichem Hänger (Tracebacks, kein SSE-Endpoint, LM Studio ohne Modell) systemctl --user restart mcp-gateway.service ausführen und danach den Client neu verbinden. Fehler bei einzelnen delegate_to_slave-Calls sind kein Grund für einen Restart — die Ursache liegt dort fast immer in Task oder Tool-Set (s. Troubleshooting).

Wichtig: Das Gateway ist zustandsbehaftet gegenüber Clients – nach einem Neustart nutzen bestehende MCP-Clients (z. B. opencode) die alte SSE-Session-ID weiter und erhalten -32602. Immer neu verbinden (s. Troubleshooting).

Troubleshooting

SymptomUrsacheBehebung
MCP error -32602: Invalid request parameters auf ALLEN Gateway-ToolsGateway neu gestartet, opencode hält abgelaufene SSE-SessionMCP-Server in opencode neu verbinden (TUI /mcp-Menü: gateway togglen) oder opencode neu starten
MCP error -32001: Request timed outSynchroner Call läuft länger als der Client wartet (LM Studio langsam, viele Tool-Runden)Task verkleinern (1 Tool, kurze Antwort), timeout/max_tokens hochsetzen, auf Background-Delegation ausweichen
Abgeschnittenes Ergebnis / „unvollständig” ohne Fehlermax_tokens künstlich niedrig gesetztKeine künstlichen Limits: Standard 8192 verwenden, für größere Tasks höher (s. Kernregeln)
Slave/Gateway wirkt „kaputt”Häufig KEIN Gateway-DefektErst diagnostizieren (s. „Diagnose vor Gateway-Neustart”); Restart nur bei nachgewiesenem Hänger
„❌ ADRESSIERUNG FEHLGESCHLAGEN: Slave ist NICHT in LM Studio geladen”Modell meta/muse-glimmer nicht geladenIn LM Studio das Glimmer-Modell laden, Aufruf wiederholen
„❌ tool_names […] enthält keine verfügbaren Werkzeuge für ‘coder‘“Tool-Name nicht im Set des Slave-Typs (SLAVE_TOOLS_BY_TYPE)Gültige Namen des Typs verwenden bzw. erst freischalten (s. u.)
Slave antwortet, aber 0 Werkzeug-Aufrufe obwohl gewolltReasoning-Only-Loop oder Tool-Set zu großTask mit exakt passender tool_names-Liste wiederholen; MAX_REASONING_ROUNDS=6 begrenzt Endlosschleifen
Slave läuft endlos und emittiert immer dieselben Tool-Calls / nie finale Antwort (Stundenlauf)Tool-Loop-Stall (glummer-Wiederholungs-Schleife)Seit 2026-08-30 automatisch durch MAX_STALLED_ROUNDS/MAX_TOOL_ONLY_ROUNDS abgebrochen; für Wiki-Wissen zusätzlich wiki_* im general-Set
Slave „denkt” extrem lange (Stunden/Minuten)CPU-Offload: Glimmer generiert nur ~3,5 Tokens/sTasks klein halten; ~500 Tokens ≈ 2–3 min einplanen; nicht parallel delegieren (teilen GPU-Slots)

Tool-Freischaltung (Änderung an einem Slave-Tool-Set)

  1. modules/orchestrator.pySLAVE_TOOLS_BY_TYPE editen (Tool-Namen aus den Tool-Definitionen der Modul-Dateien, z. B. filesystem.py). Nur Namen verwenden, die existieren, sonst schlägt der Filter fehl.
  2. Syntax: python3 -m py_compile …/modules/orchestrator.py.
  3. systemctl --user restart mcp-gateway.service – Module werden nur beim Start geladen.
  4. Im Log prüfen: „Slave-Tools geladen: N Werkzeuge aus M Modulen”.
  5. Client neu verbinden (s. -32602-Zeile oben).

Performance-Rahmenbindung

LM Studio liefert bei CPU-Offload nur ~3,5 Tokens/s (n_gen-Prüfung im LM-Studio-Log: tg = 3.5 t/s). Konsequenzen für Delegationen:

  • Eine Tool-Runde (Reasoning ~300–500 Token + Tool-Aufruf) dauert minutenlang.
  • Planungsgröße: 1 Tool-Call ≈ 2–4 min, kurze Textantwort ≈ 1–3 min.
  • Mehrere parallel gestartete Delegates laufen gleichzeitig gegen dieselben GPU-Slots und verlangsamen sich gegenseitig – nicht parallel delegieren.
  • Ein delegate_to_slave-Call mit mehreren Werkzeugen kann den 900-s-Request-Timeout erreichen; lieber aufteilen.
  • Keine künstlichen Limits: max_tokens großzügig (Standard 8192) wählen. Ein knappes Budget bricht die Generierung ab — bei 3,5 T/s ist das teurer (Neu-Start) als ein größer geplanter, sauberer Aufruf.

Sicherheit (erlaubter Pfadrahmen)

  • modules/filesystem.py: ALLOWED_DIRECTORIES (…/sandbox/apps, …/libraries, …/llm_wiki/wiki, /tmp/mcp_workspace), BLOCKED_PATHS sperrt /etc, /root, ~/.ssh, /var/log, /boot, /sys, /proc, /dev.
  • write/edit sind strikt auf erlaubte Pfade begrenzt (positiv/negativ getestet); Systemmodifikation, Compiler-Zugriff und TTS sind über die SAFE_SLAVE_MODULES- Whitelist geregelt.
  • Der Slave wird nur über delegate_to_slave angesprochen; tool_names begrenzt das Tool-Angebot pro Aufgabe zusätzlich.
  • Netzwerk: Gateway bindet nur 127.0.0.1:3000 (gateway.py), LM Studio 127.0.0.1:1234 – das System lauscht bewusst nur lokal (kein Remote-Zugriff, keine Credentials im Netz).

Registrierung in opencode

~/.config/opencode/opencode.jsonc, Abschnitt mcp.gateway:

"mcp": {
  "gateway": {
    "type": "remote",
    "url": "http://127.0.0.1:3000/sse",
    "enabled": true
  }
}

Zusätzlich liegt …/mcps/config.json mit dem SSE-URL der Gateway-Beschreibung vor. Nach einem Gateway-Neustart muss der Client neu verbinden (s. Troubleshooting).

Verwandte Artikel

Quellen

  • Eigene Entwicklung: ~/Dokumente/daten/sandbox/aux/mcps/gateway.py, …/modules/orchestrator.py, …/modules/filesystem.py
  • Beispiel-Projekt: ~/Dokumente/daten/sandbox/apps/rbs