MCP Knowledge Base
Find a file
2026-09-15 20:36:26 +02:00
internal initial commit after rewrite 2026-09-15 20:36:26 +02:00
static initial commit after rewrite 2026-09-15 20:36:26 +02:00
templates initial commit after rewrite 2026-09-15 20:36:26 +02:00
.dockerignore initial commit after rewrite 2026-09-15 20:36:26 +02:00
.env.example initial commit after rewrite 2026-09-15 20:36:26 +02:00
.gitignore initial commit after rewrite 2026-09-15 20:36:26 +02:00
AGENTEN-GUIDE.md initial commit after rewrite 2026-09-15 20:36:26 +02:00
AUSBAUSTUFEN.md initial commit after rewrite 2026-09-15 20:36:26 +02:00
Dockerfile initial commit after rewrite 2026-09-15 20:36:26 +02:00
go.mod initial commit after rewrite 2026-09-15 20:36:26 +02:00
go.sum initial commit after rewrite 2026-09-15 20:36:26 +02:00
main.go initial commit after rewrite 2026-09-15 20:36:26 +02:00
README.md initial commit after rewrite 2026-09-15 20:36:26 +02:00
REPOMAP.md initial commit after rewrite 2026-09-15 20:36:26 +02:00

Munin

Ein projektbezogenes Gedächtnis für KI-Agenten und Menschen.

In der nordischen Überlieferung sind Hugin und Munin die Raben Odins. Munin steht für das Gedächtnis: Er fliegt aus, sammelt und kehrt zurück, um zu berichten — was hier die Agenten tun. Passend zum Torwächter Heimdall, unserem AI-Gateway.

KI-Werkzeuge (Claude, Cline, OpenCode, eigene Anwendungen) legen darin über MCP ab, was sie lernen, und rufen es wieder auf. Menschen pflegen dasselbe Wissen über eine schlanke Weboberfläche. Beides landet im selben Speicher — der Unterschied ist ein Feld am Eintrag, kein zweites System.

KI-Werkzeug ──MCP──┐
                   ├──▶ Projekt ──▶ Einträge (SQLite)
Browser ──HTTP─────┘

Inhalt


Schnellstart

# 1. Konfiguration anlegen
cp .env.example .env

# 2. Geheimnisse erzeugen und eintragen
openssl rand -hex 32   # → SESSION_SECRET
openssl rand -hex 24   # → ADMIN_TOKEN

# 3. Starten
go run .

Dann http://localhost:8080 öffnen und mit dem ADMIN_TOKEN anmelden.

Der weitere Weg:

  1. Projekt anlegen unter Projekte
  2. Zugang erzeugen — der Key wird genau einmal angezeigt
  3. MCP-Client einrichten (die Anleitung erscheint direkt neben dem neuen Key)

Grundbegriffe

Begriff Bedeutung
Projekt Die Einheit der Abschottung. Eigene Einträge, eigene Zugänge, eigener MCP-Endpunkt. Projekte sehen sich gegenseitig nicht.
Eintrag Ein Stück Wissen: Bezeichner, Titel, Markdown-Inhalt, Schlagworte. Von Menschen oder Agenten geschrieben.
Bezeichner (slug) Kurzer, dauerhafter Name eines Eintrags (deployment-prozess). Der Schlüssel gegen Dubletten: Zweimal derselbe Bezeichner aktualisiert, statt eine zweite Fassung danebenzustellen.
API-Key Zugang zu genau einem Projekt, mit Namen und Rolle. Wird nur als Prüfsumme gespeichert.
Plattform-Admin Der ADMIN_TOKEN. Legt Projekte an, kommt an jedes Projekt.

Warum Bezeichner statt Ordner

Ein Wiki gliedert nach Ordnern. Für ein Gedächtnis, in das Agenten schreiben, funktioniert das nicht: Wo eine Erkenntnis „hingehört", ist selten eindeutig, und ein Agent trifft diese Entscheidung schlecht.

Stattdessen gibt es Schlagworte zum Einordnen und Verweise zum Verknüpfen. Im Inhalt zeigt [[bezeichner]] auf einen anderen Eintrag; die Gegenrichtung („was verweist hierher") entsteht daraus von selbst und steht in der Detailansicht wie auch in der MCP-Antwort.


MCP einrichten

Nach dem Erzeugen eines Keys zeigt die Weboberfläche den passenden Abschnitt an. Der Key bestimmt das Projekt — ein zusätzlicher Parameter ist nicht nötig. Wer mit mehreren Projekten arbeitet, trägt mehrere Server mit je eigenem Key ein.

Alle Clients sprechen denselben Endpunkt …/mcp über Streamable HTTP an. Die Konfigurationsdateien sehen trotzdem unterschiedlich aus:

Client Datei Schlüssel Transport heißt dort
Claude Code .mcp.json / ~/.claude.json mcpServers "type": "http"
Cline cline_mcp_settings.json / ~/.cline/mcp.json mcpServers "type": "streamableHttp"
OpenCode opencode.json mcp "type": "remote"

Die beiden ersten Zeilen sind die Falle: gleicher Schlüssel, anderer Wert für type. Ein aus Claude Code kopierter Block läuft in Cline nicht.

Claude Code

Am schnellsten über die Kommandozeile:

claude mcp add --transport http munin-plattform https://munin.intern/mcp \
  --header "Authorization: Bearer wk_…"

Ohne --scope landet der Eintrag in ~/.claude.json und gilt nur im aktuellen Projekt; --scope user macht ihn überall verfügbar.

Fürs Team gehört er stattdessen als .mcp.json ins Repository — dann aber ohne den Key im Klartext. Claude Code löst ${VAR} aus der Umgebung auf:

{
  "mcpServers": {
    "munin-plattform": {
      "type": "http",
      "url": "https://munin.intern/mcp",
      "headers": { "Authorization": "Bearer ${MUNIN_KEY}" }
    }
  }
}

Jede Person setzt MUNIN_KEY auf ihren eigenen Key. Damit steht im Repository die Einrichtung, nicht das Geheimnis — und im Protokoll steht trotzdem, wer geschrieben hat.

Cline

Über die Oberfläche: MCP Servers → Configure → Configure MCP Servers. In der Kommandozeilenfassung liegt dieselbe Datei unter ~/.cline/mcp.json.

{
  "mcpServers": {
    "munin-plattform": {
      "type": "streamableHttp",
      "url": "https://munin.intern/mcp",
      "headers": { "Authorization": "Bearer wk_…" },
      "disabled": false,
      "autoApprove": ["recall", "get", "list"]
    }
  }
}

autoApprove nennt die Werkzeuge, die ohne Rückfrage laufen dürfen. Die drei lesenden gehören dort hinein — sonst muss jede Suche einzeln bestätigt werden, und ein Agent, der für jeden Blick ins Gedächtnis nachfragen muss, benutzt es nicht. remember und forget gehören ausdrücklich nicht hinein: Sie verändern gemeinsames Wissen.

OpenCode

opencode.json im Projektverzeichnis oder global unter ~/.config/opencode/opencode.json. Anderes Schema als bei den beiden anderen — der Block heißt mcp, nicht mcpServers:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "munin-plattform": {
      "type": "remote",
      "url": "https://munin.intern/mcp",
      "enabled": true,
      "headers": { "Authorization": "Bearer {env:MUNIN_KEY}" }
    }
  }
}

OpenCode ersetzt {env:VAR} aus der Umgebung und {file:~/pfad} durch den Inhalt einer Datei. Ein nicht gesetztes {env:…} wird zur leeren Zeichenkette — der Server antwortet dann mit 401, nicht mit einer Fehlermeldung über die Konfiguration.

Danach

Ein Key je Person und je Agent. Nur so steht im Protokoll, wer etwas geschrieben hat, und ein einzelner Zugang lässt sich zurückziehen, ohne alle anderen zu stören.

Verbindung prüfen: Der Agent sollte recall, get, list, remember und forget anbieten. Tut er das nicht, hilft ein Blick ins Zugriffsprotokoll des Servers — ein 401 dort heißt falscher oder zurückgezogener Key, gar kein Eintrag heißt, dass der Client den Server nie aufgerufen hat.

Wie man Agenten so anspricht, dass sie den Speicher auch nutzen: AGENTEN-GUIDE.md.


Die Werkzeuge

Werkzeug Rolle Zweck
recall read Durchsucht den Speicher. Der Haupteinstieg — auch vor dem Schreiben.
get read Lädt einen Eintrag vollständig, samt Verweisen und Fassungsnummer.
list read Überblick: zuletzt geändert, alle Schlagworte.
remember write Legt einen Eintrag an oder aktualisiert ihn.
forget write Löscht einen Eintrag.

Der Server schickt jedem Client eine Anleitung (instructions) mit, die das Vorgehen beschreibt — vor allem: vor dem Schreiben erst recall aufrufen. Das ist das wirksamste Mittel gegen Dubletten und steht in internal/mcpserver/server.go.

Schreibbudget

remember und forget sind je Projekt auf 1.000 Aufrufe pro Stunde gedeckelt (WRITE_LIMIT_HOURLY). Lesen ist nie begrenzt.

Der Grund ist die Kehrseite einer anderen Entscheidung: Ein gültiger Key umgeht die IP-Sperre bewusst, weil Deploy-Skript, MCP-Client und Browser eines Büros sich nach außen oft eine Adresse teilen. Damit hätte ein Agent, der in einer Schleife hängt, sonst überhaupt keine Grenze.

Das Budget hängt am Projekt, nicht am Key: Ein Limit je Key ließe sich umgehen, indem man einen zweiten Key anlegt, und das darf jeder Projekt-Admin selbst. Einzelne Projekte bekommen über Projekte → Bearbeiten eine eigene Obergrenze; die Übersicht zeigt, wie viel im laufenden Fenster noch frei ist. Ändern darf sie nur der Plattform-Admin — aus demselben Grund.

Ist das Budget erschöpft, bekommt das Modell keinen nackten Fehler, sondern die Anweisung, abzubrechen und das Limit zu melden — sonst versucht es sofort wieder oder weicht auf ein anderes Werkzeug aus.

Gleichzeitiges Schreiben

get liefert eine version. Wird sie bei remember als expected_version mitgegeben, meldet der Server einen Konflikt, falls jemand anderes den Eintrag inzwischen geändert hat — statt die fremde Änderung stillschweigend zu überschreiben.

Agent A: get(deployment) → version 3
Agent B: get(deployment) → version 3
Agent A: remember(…, expected_version=3) → OK, jetzt version 4
Agent B: remember(…, expected_version=3) → Konflikt: neu laden, zusammenführen

Weboberfläche

Weg Zweck
/login Anmeldung mit API-Key oder Admin-Token
/admin/projects Projekte anlegen und auflisten (nur Plattform-Admin)
/admin/projects/{projekt}/edit Name, Beschreibung und Schreibbudget ändern; Projekt löschen
/p/{projekt}/ Einträge durchsuchen, nach Schlagwort filtern
/p/{projekt}/e/{bezeichner} Eintrag lesen, mit Rückverweisen
/p/{projekt}/keys Zugänge verwalten (Rolle admin)
/p/{projekt}/audit Protokoll des Projekts
/healthz Healthcheck, ohne Anmeldung

Rollen

Rolle Darf
read lesen, suchen
write zusätzlich schreiben und löschen
admin zusätzlich Zugänge des eigenen Projekts verwalten

Konfiguration

Variable Standard Bedeutung
SESSION_SECRET — Pflicht. Signiert Sitzungen und CSRF-Tokens. Mindestens 32 Zeichen.
ADMIN_TOKEN — Pflicht. Zugang des Plattform-Admins. Mindestens 16 Zeichen.
DB_PATH data/munin.db Pfad zur SQLite-Datenbank
PORT 8080 Port des Servers
COOKIE_SECURE automatisch Secure-Flag der Cookies. Ohne Angabe: an, außer bei localhost.
ENABLE_HSTS false HSTS-Header senden. Nur bei gültigem Zertifikat.
TRUSTED_PROXY — IPs/CIDR-Netze, deren X-Forwarded-For geglaubt wird
MAX_API_BODY_BYTES 4194304 Höchstgröße eines MCP-Requests (4 MB)
WRITE_LIMIT_HOURLY 1000 Schreibende MCP-Aufrufe je Projekt und Stunde. 0 = unbegrenzt.
EXPORT_DIR — Zielverzeichnis des Markdown-Exports. Leer = abgeschaltet.

Fehlen die Pflichtangaben oder sind sie zu kurz, startet der Server nicht. Ein schwaches Geheimnis wäre die Wurzel für alles andere — ein klarer Abbruch ist besser als unsicherer Betrieb.


Sicherheit

Was geprüft wird

Schutz Wo
Signierte Sitzungs-Cookies (HMAC-SHA256) internal/auth/session.go
API-Keys nur als SHA-256-Prüfsumme gespeichert internal/auth/keys.go
Zurückgezogener Key beendet laufende Sitzungen sofort internal/web/middleware.go
CSRF-Token in allen Formularen internal/auth/csrf.go
SameSite=Lax, HttpOnly, Secure internal/auth/cookies.go
Rate-Limiting je Adresse, nur auf Fehlversuche internal/auth/ratelimit.go
Content-Security-Policy und weitere Schutz-Header internal/web/middleware.go
Größenlimits für Request-Bodies internal/web/middleware.go
Server-Timeouts gegen Slowloris main.go
Optimistisches Sperren gegen stilles Überschreiben internal/store/entries.go

Abschottung der Projekte

Sie ist strukturell, nicht eine Frage der richtigen Filterbedingung:

  • Jedes Projekt hat einen eigenen MCP-Server, dessen Werkzeuge fest auf dieses Projekt verdrahtet sind
  • Jedes Projekt hat einen eigenen Suchindex — auch die BM25-Gewichtung wird nicht über Projektgrenzen hinweg berechnet
  • Jeder Datenbankzugriff ist auf die Projekt-ID eingeschränkt
  • Ein fremdes Projekt antwortet mit 404, nicht 403 — sonst ließe sich über die Statuscodes herausfinden, welche Projekte es sonst noch gibt

Warum SHA-256 für Keys und nicht bcrypt

Für Passwörter wären langsame Verfahren richtig: Menschen wählen aus einem kleinen, erratbaren Raum. Ein API-Key hier ist aber 256 Bit aus crypto/rand — diesen Raum durchsucht niemand. Die Langsamkeit brächte keinen Gewinn, würde aber jeden Request bremsen. Entscheidend ist der zweite Punkt: Weil SHA-256 schnell und deterministisch ist, wird der Key in einem indizierten Lookup aufgelöst statt in einer Schleife über alle Keys.

Prompt Injection

Der Inhalt des Speichers landet direkt im Kontext von KI-Werkzeugen. Wer in ein Projekt schreiben darf, schreibt damit indirekt in die Prompts aller anderen, die dieses Projekt nutzen.

Dagegen hilft kein technischer Schutz, nur Nachvollziehbarkeit: Jeder Eintrag trägt Herkunft (human/agent) und den Namen des verwendeten Zugangs, und jede Änderung steht im Protokoll.

Wo die Grenzen liegen

  • Keine Benutzerkonten. Zugänge sind Keys, keine Personen. Ein Key je Person ist der Ersatz — geplant ist später Keycloak.
  • Projekte pflegt nur der Plattform-Admin. Ein Team kann seinen eigenen Projektnamen nicht selbst korrigieren. Solange es wenige Projekte gibt, ist eine Berechtigungsstufe weniger den Umweg wert.
  • Kein TLS im Server selbst. Für HTTPS gehört ein Reverse-Proxy davor.
  • Ein Workspace. Die Struktur für mehrere ist im Schema angelegt (workspace_id), aber nicht in der Oberfläche.
  • Das Schreibbudget zählt im Arbeitsspeicher. Nach einem Neustart steht es wieder bei null. Als Netz gegen eine Schleife reicht das; eine verbindliche Zusage ist es nicht.

Betrieb

Docker

docker build -t munin .

docker run -d --name munin \
  -p 8080:8080 \
  -v munin-daten:/data \
  -e SESSION_SECRET="$(openssl rand -hex 32)" \
  -e ADMIN_TOKEN="$(openssl rand -hex 24)" \
  -e ENABLE_HSTS=true \
  -e TRUSTED_PROXY=172.16.0.0/12 \
  munin

Die Geheimnisse sollten aus einem Passwortspeicher kommen, nicht bei jedem Start neu erzeugt werden — ein neues SESSION_SECRET macht alle Sitzungen ungültig.

Hinter einem Reverse-Proxy

TRUSTED_PROXY=127.0.0.1     # Adresse des Proxys
COOKIE_SECURE=true          # falls der Proxy TLS beendet
ENABLE_HSTS=true            # nur mit gültigem Zertifikat

Ohne TRUSTED_PROXY sieht der Server für alle Besucher dieselbe Adresse — das Rate-Limiting würde dann entweder niemanden bremsen oder beim ersten Angreifer gleich alle aussperren.

Datensicherung

Datenbank. Alles steckt in einer Datei. Weil SQLite im WAL-Modus läuft, sollte sie nicht im laufenden Betrieb kopiert werden:

sqlite3 /pfad/munin.db ".backup '/sicherung/wissen-$(date +%F).db'"

Markdown-Export. Ist EXPORT_DIR gesetzt, schreibt der Server den Bestand täglich als Markdown-Dateien dorthin — ein Ordner je Projekt, eine Datei je Eintrag, dazu eine _index.md als Übersicht:

export/
  plattform/
    _index.md
    deployment.md
    server-zugang.md

Jede Datei trägt alle Felder im Kopf (Bezeichner, Schlagworte, Herkunft, Autor, Fassung, Zeitstempel), sodass sich der Bestand daraus auch wieder einlesen ließe.

Das eignet sich als Git-Sicherung: Ein Lauf ohne inhaltliche Änderungen fasst keine Datei an und erzeugt deshalb auch keinen Diff.

# Auf Knopfdruck: "Jetzt exportieren" unter /admin/projects
# Für Cron oder ein Sicherungsskript:
./munin -export

Der Export ist eine Sicherung, keine zweite Wahrheit — Änderungen an den Dateien wirken nicht zurück und gehen beim nächsten Lauf verloren.

Protokoll

Vorgänge werden 90 Tage aufbewahrt und danach automatisch gelöscht (auditRetention in main.go). Das Protokoll enthält Adressen und Zugangsnamen, aus denen sich Personen ableiten lassen — eine Aufbewahrungsfrist ist deshalb die Voraussetzung dafür, es überhaupt führen zu dürfen.


Entwicklung

go run .          # starten
go test ./...     # alle Tests
go vet ./...      # statische Prüfung
gofmt -l .        # Formatierung prüfen

Aufbau

main.go                  Konfiguration, Verdrahtung, Serverstart
internal/
  store/                 SQLite: Schema, Migrationen, alle Abfragen
  entry/                 Fachlogik: Markdown, Verweise, Textausschnitte
  search/                BM25-Volltextindex, einer je Projekt
  auth/                  Sitzungen, CSRF, Cookies, Keys, Rate-Limiting
  export/                Markdown-Export als Sicherung
  web/                   Routen, Middleware, Handler
  mcpserver/             MCP-Werkzeuge
templates/  static/      eingebettet ins Binary

Die Richtung der Abhängigkeiten zeigt immer nach innen: web und mcpserver kennen store, aber store kennt keinen von beiden.

Datenbank

SQLite über modernc.org/sqlite — ein in Go übersetzter Nachbau, kein C. Nur deshalb lässt sich das Programm als statisches Binary ohne Laufzeitumgebung ausliefern.

Das SQL vermeidet bewusst alles, was nur SQLite kann (kein AUTOINCREMENT, keine Datumsfunktionen, keine INSERT OR REPLACE), damit ein Umstieg auf PostgreSQL offenbleibt. Die Regeln stehen als Kommentar in internal/store/migrations/001_init.sql.

Schema ändern: eine neue Datei 003_….sql in migrations/ anlegen. Sie wird beim nächsten Start automatisch angewandt und vermerkt; bestehende Migrationen werden nie verändert.


Was noch aussteht

AUSBAUSTUFEN.md hält fest, was bewusst nicht gebaut wurde — mit Messwerten, Begründung und den Auslösern, an denen man merkt, dass es Zeit dafür ist. Unter anderem: FTS5 statt eigenem Suchindex, eine Datenbank je Projekt, semantische Suche.