05bef8642d
Paket A des Plans. Ersetzt die flache SQLite-Fakten-DB durch Mem0 (LLM-Auto-
Extraktion + Vektor/Chroma-Suche). Architektur erzwungen durch Python-Split:
MC2-Backend laeuft auf 3.14 (kann mem0 nicht importieren), mem0+chromadb nur
auf 3.12 (~/.mem0/venv) -> Mem0-Sidecar (FastAPI, localhost:8765), MC2 spricht
ihn per HTTP. /api/memory-Form bleibt unveraendert (UI + MCP kompatibel).
- mem0_service/: Sidecar (app.py), Migration (migrate.py), deps.
- Embeddings: neue llama-swap embed-Rolle (Qwen3-Embedding-0.6B, 1024 Dim,
pooling last) ueber /v1/embeddings.
- LLM-Extraktion: lokales fast-Hirn; NoThinkLLM schaltet Qwen3-Thinking ab
(sonst bricht json_object-Extraktion ab), custom_instructions halten Deutsch.
- backend/services/memory.py: duenner HTTP-Client auf den Sidecar (semantische
Suche mit score, verbatim add, learn()). Router: /api/memory/learn.
- mcp/mcp_memory.py: neues learn-Tool (Auto-Lernen aus Gespraechs-Turns),
search jetzt semantisch.
- Frontend: Relevanz-Score + Auto/Manuell-Herkunft im Gedaechtnis-Tab.
- deploy/: mem0-service.service + deploy.sh (uv-Install, Migration, Restart).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Mission Control 2.0 — Memory MCP Server (geteiltes Gedächtnis für ALLE Tools).
|
|
|
|
Stdio-MCP-Server. Läuft als Subprocess von Cline/Claude Code/OpenCode/Hermes.
|
|
Ruft die /api/memory-Endpunkte von Mission Control via HTTP auf.
|
|
|
|
Tool-Beschreibungen bewusst als GUARDS formuliert (konditional, nicht imperativ),
|
|
damit kleine Modelle (z.B. Hermes 14B) nicht in Aufruf-Schleifen laufen.
|
|
|
|
Env: MC_URL (default http://192.168.178.151:9001), MC_TOKEN (optional).
|
|
Install (auf dem Rechner des Tools): pip install mcp httpx
|
|
"""
|
|
|
|
import os
|
|
|
|
import httpx
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
MC_URL = os.environ.get("MC_URL", "http://192.168.178.151:9001").rstrip("/")
|
|
MC_TOKEN = os.environ.get("MC_TOKEN", "")
|
|
|
|
mcp = FastMCP("mission-control-memory")
|
|
|
|
|
|
def _h() -> dict:
|
|
return {"X-MC-Token": MC_TOKEN} if MC_TOKEN else {}
|
|
|
|
|
|
def _get(path: str, **params):
|
|
r = httpx.get(f"{MC_URL}{path}", headers=_h(), params=params, timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _post(path: str, data: dict):
|
|
r = httpx.post(f"{MC_URL}{path}", headers=_h(), json=data, timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _put(path: str, data: dict):
|
|
r = httpx.put(f"{MC_URL}{path}", headers=_h(), json=data, timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
def _delete(path: str):
|
|
r = httpx.delete(f"{MC_URL}{path}", headers=_h(), timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
|
|
@mcp.tool()
|
|
def get_memories(category: str = "") -> str:
|
|
"""Lädt gespeicherte Fakten/Entscheidungen aus dem geteilten Gedächtnis.
|
|
Nutze dies EINMAL am Session-Beginn, wenn du Projekt-Kontext brauchst — nicht wiederholt.
|
|
category: user | instruction | stable | versioned | ephemeral | (leer = alle)"""
|
|
items = _get("/api/memory", **({"category": category} if category else {}))
|
|
if not items:
|
|
return "Keine Memories gespeichert."
|
|
icon = {"stable": "🔵", "versioned": "🟡", "ephemeral": "⏱", "user": "👤", "instruction": "📋"}
|
|
return "\n".join(
|
|
f"{icon.get(m['category'], '·')} [{m['category']}] {m['content']} (ID: {m['id'][:8]})"
|
|
for m in items
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
def search_memories(q: str) -> str:
|
|
"""Sucht SEMANTISCH (nach Bedeutung, nicht nur Stichwort) in den Fakten. Nutze dies
|
|
VOR add_memory (Dubletten-Check) oder wenn du eine konkrete frühere Entscheidung suchst.
|
|
Treffer sind nach Relevanz sortiert."""
|
|
items = _get("/api/memory", q=q)
|
|
if not items:
|
|
return f"Keine Treffer für '{q}'."
|
|
def _line(m):
|
|
sc = f" {m['score']:.2f}" if isinstance(m.get("score"), (int, float)) else ""
|
|
return f"[{m['category']}{sc}] {m['content']} (ID: {m['id'][:8]})"
|
|
return "\n".join(_line(m) for m in items)
|
|
|
|
|
|
@mcp.tool()
|
|
def add_memory(content: str, category: str = "stable", source: str = "agent") -> str:
|
|
"""Speichert EINEN dauerhaften Fakt im geteilten Gedächtnis (alle Tools sehen ihn).
|
|
Nur aufrufen, WENN gerade etwas Dauerhaftes entstanden ist (Konvention, Architektur-
|
|
Entscheidung, Tech-Version, Nutzer-Präferenz) UND es noch nicht existiert (vorher
|
|
search_memories!). Knapp & atomar. Existiert ein passender Eintrag → update_memory.
|
|
category: user | instruction | stable | versioned | ephemeral"""
|
|
m = _post("/api/memory", {"content": content, "category": category, "source": source})
|
|
return f"Gespeichert (ID: {m['id'][:8]}): {content}"
|
|
|
|
|
|
@mcp.tool()
|
|
def update_memory(memory_id: str, content: str = "", category: str = "") -> str:
|
|
"""Aktualisiert einen bestehenden Eintrag (nur gesetzte Felder)."""
|
|
data = {k: v for k, v in (("content", content), ("category", category)) if v}
|
|
if not data:
|
|
return "Nichts zu aktualisieren."
|
|
m = _put(f"/api/memory/{memory_id}", data)
|
|
return f"Aktualisiert: {m['content']}"
|
|
|
|
|
|
@mcp.tool()
|
|
def delete_memory(memory_id: str) -> str:
|
|
"""Löscht einen veralteten/falschen Eintrag anhand seiner ID."""
|
|
_delete(f"/api/memory/{memory_id}")
|
|
return f"Eintrag {memory_id[:8]} gelöscht."
|
|
|
|
|
|
@mcp.tool()
|
|
def learn(text: str) -> str:
|
|
"""Lässt das Gedächtnis AUTOMATISCH aus einem Gesprächsausschnitt lernen: reiche
|
|
rohe Nutzer-Aussagen/Turns durch — das Gedächtnis EXTRAHIERT die dauerhaften Fakten
|
|
selbst und mischt sie ein (ohne Dubletten). Nutze dies, wenn der Nutzer beiläufig etwas
|
|
Dauerhaftes über sich/seine Vorlieben/das Projekt erwähnt hat — du musst nicht selbst
|
|
den Fakt formulieren. Für einen bereits fertig formulierten Einzel-Fakt → add_memory."""
|
|
res = _post("/api/memory/learn", {"text": text})
|
|
items = res.get("results", []) if isinstance(res, dict) else []
|
|
learned = [r for r in items if r.get("event") in (None, "ADD", "UPDATE")]
|
|
if not learned:
|
|
return "Nichts Neues gelernt (keine dauerhaften Fakten erkannt)."
|
|
return "Gelernt:\n" + "\n".join(f"· {r.get('content')}" for r in learned)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|