- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
pages.list holt die Tags über withGraphJoined('tags') und legt limit auf
das Ergebnis dieses Joins — also auf Zeilen, nicht auf Seiten. Eine Seite
mit vier Tags verbraucht vier Plätze, die Antwort bricht mitten in einer
Seite ab. Gemessen gegen eine produktive Instanz mit 128 Seiten: limit 10
→ 3 Seiten, limit 100 → 91 Seiten, deren letzte 2 statt 4 Tags trägt.
orderBy ist daran unbeteiligt. Dass ID unauffällig wirkte und TITLE nicht,
lag allein daran, dass die Seiten mit niedriger id ungetaggt sind — ID als
Default hätte den Fehler also nur verschoben, nicht behoben.
Deshalb geht kein limit mehr in die Query. Sortiert wird in sort_pages()
im Client, gekürzt wird im Tool, das die Gesamtzahl kennt: der Kopf nennt
jetzt immer "X von Y Seiten", limit 0 zeigt alles. Damit entfällt auch der
alte Vollständigkeits-Hinweis, der über len(pages) >= limit riet — er
schlug am exakten Limit falsch an und blieb bei oberhalb gekürzten
Antworten stumm.
Ein unbekanntes order_by fällt jetzt im Client auf, statt als GraphQL-Fehler
aus Wiki.js zurückzukommen; createdAt wird für CREATED mitgelesen.
Schließt #1, schließt #2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZyPb2JEJGp3KVcKawVR9z
|
||
| .github/workflows | ||
| src/wikijs_mcp | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| server.py | ||
| uv.lock | ||
wikijs-mcp
Ein schlanker, selbst geschriebener MCP-Server (stdio) für Wiki.js — kein
Fork von Fremdcode, volle Kontrolle über Tool-Scope und Token-Rechte. Ersetzt
Community-Server wie heAdz0r/wikijs-mcp-server.
Autoren: Lyra (Konzept & Umsetzung), Draven / DravenTec (Co-Autor). Lizenz: MIT.
Tool-Scope
Der Scope ist in drei Stufen gegliedert, damit der Blast-Radius eines kompromittierten Tokens eine Konfigurations- und keine Code-Entscheidung ist.
Lesen — immer verfügbar (read:pages)
| Tool | Signatur | Zweck |
|---|---|---|
search_pages |
(query, path=None, locale=None) |
Volltextsuche, optional auf Teilbaum/Sprache begrenzt |
get_page |
(path, locale="de") |
Seite über Pfad lesen, inkl. Inhalt |
get_page_by_id |
(id) |
Seite über id lesen — Gegenstück zu den Schreib-Tools |
list_pages |
(locale="de", tags=None, limit=100, order_by="TITLE", order_direction="ASC") |
Seitenliste mit Status und Tags; limit kürzt nur die Ausgabe (0 = alle), der Kopf nennt immer die Gesamtzahl |
get_page_tree |
(path="", locale="de", mode="ALL") |
Einträge unterhalb von path; leer für die Wurzel |
list_tags |
() |
Alle vergebenen Tags |
search_tags |
(query) |
Tags suchen |
list_links |
(locale="de") |
Ausgehende Links + Ziele ohne passende Seite |
page_history |
(id, offset_page=0, offset_size=25) |
Versionshistorie (read:history) |
get_page_version |
(page_id, version_id) |
Inhalt einer früheren Version (read:history) |
Schreiben — außer bei WIKIJS_READONLY=true (write:pages)
| Tool | Signatur | Zweck |
|---|---|---|
create_page |
(path, title, content, locale="de", description="", tags=None, published=True) |
Neue Seite oder Entwurf anlegen |
update_page |
(id, content, published=None) |
Inhalt ersetzen, übriger Zustand bleibt |
update_page_meta |
(id, title=None, description=None, tags=None, published=None) |
Metadaten ändern, Inhalt bleibt |
publish_page |
(id) |
Veröffentlichen, ohne Inhalt zu senden |
unpublish_page |
(id) |
Zurückziehen, ohne Inhalt zu senden |
move_page |
(id, path, locale=None) |
Verschieben/Umbenennen (manage:pages) |
restore_version |
(page_id, version_id) |
Frühere Version wiederherstellen (read:history) |
Löschen — nur mit WIKIJS_ENABLE_DELETE=true (delete:pages)
| Tool | Signatur | Zweck |
|---|---|---|
delete_page |
(id) |
Seite löschen — nicht rückgängig zu machen |
Kein User-/Gruppen-Management, keine Asset- oder Kommentar-Tools — nur bei echtem Bedarf später ergänzen.
Warum update_page die Seite erst liest
In Wiki.js 2.x ist pages.update kein reines Patch. Ein Blick in
server/models/pages.js erklärt zwei Verhaltensweisen, die im Alltag als
stiller Datenverlust auftreten:
isPublished: opts.isPublished === true || opts.isPublished === 1, // Zeile 430
await WIKI.models.tags.associateTags({ tags: opts.tags, page }) // Zeile 443
- Fehlt
isPublished, ergibt der Ausdruckfalse— keinundefined, das Knex beimpatchüberspringen würde. Die Seite verschwindet dadurch still aus der Sicht normaler Leser.titleunddescriptionüberleben ein Update genau deshalb, weil dort tatsächlichundefinedsteht. - Fehlt
tags, ruftassociateTagstags.map()aufundefinedauf. Das ist die Quelle der MeldungCannot read properties of undefined (reading 'map')— sie kommt nach dem erfolgreichen Schreiben, weshalb der Aufruf trotz gespeichertem Inhalt als Fehler zurückkommt. contentist bei jedem Update Pflicht (PageEmptyContent). Deshalb liest auchupdate_page_metaden Inhalt und schreibt ihn unverändert zurück.- Die Antwort einer Mutation trägt Spaltennamen statt Schema-Feldnamen
(
localeCode,editorKey). Wer impage-Blocklocaleanfordert, bekommtCannot return null for non-nullable field Page.locale— nach erfolgreichem Schreiben. Mutationen wählen deshalb nurid,path,title,isPublishedundupdatedAtaus.
Der Client lädt daher vor jedem Update den aktuellen Stand und schickt jedes
nicht explizit überschriebene Feld unverändert mit. path und locale bleiben
bewusst außen vor: weichen sie vom Ist-Zustand ab, macht Wiki.js daraus einen
Move. Dafür gibt es move_page.
Bekannte Grenze: publishStartDate / publishEndDate liegen in Wiki.js 2.x
als String-Spalte hinter einem Date!-Scalar, dessen serialize()
toISOString() aufruft. Für jede Seite ohne Veröffentlichungsfenster würde ein
Abruf dieser Felder die gesamte Query mit einem GraphQL-Fehler beantworten — sie
werden deshalb weder gelesen noch gesetzt. Ein per Weboberfläche gesetztes
Veröffentlichungsfenster geht bei einem Update über diesen Server verloren.
Voraussetzungen
- Python 3.11+
uv- Eine erreichbare Wiki.js-2.x-Instanz und ein API-Token
Einrichtung
# Abhängigkeiten auflösen
uv sync
# Zugangsdaten hinterlegen
cp .env.example .env
# .env öffnen und WIKIJS_URL / WIKIJS_TOKEN eintragen
Die .env ist über .gitignore vom Commit ausgeschlossen. Das Token wird
nur als Authorization: Bearer …-Header verwendet und landet nicht in Logs
oder Fehlermeldungen.
Guardrails
Drei Umgebungsvariablen begrenzen, was der Server überhaupt anbietet:
# Nur Lesen — Schreib-Tools werden gar nicht erst registriert
set -x WIKIJS_READONLY true
# Schreiben nur unterhalb bestimmter Pfade (kommagetrennt, Segmentgrenze)
set -x WIKIJS_ALLOWED_PATHS claude/,notizen/
# delete_page überhaupt anbieten
set -x WIKIJS_ENABLE_DELETE true
Die Allowlist greift zusätzlich zu den Rechten des Tokens, nicht statt ihrer. Der wirksamste Schutz bleibt ein Token, das nur kann, was es können soll.
Interne CA (TLS)
Läuft die Instanz hinter einer eigenen CA (z. B. eine *.internal-Adresse),
der die von Python mitgelieferte CA-Liste nicht traut, scheitert die
Verbindung mit CERTIFICATE_VERIFY_FAILED — obwohl curl funktioniert, weil
es den System-Trust-Store nutzt. Dann WIKIJS_CA_BUNDLE auf ein CA-Bundle
zeigen lassen:
set -x WIKIJS_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt
Als Notlösung schaltet WIKIJS_VERIFY_SSL=false die Verifikation ab — nur
verwenden, wenn kein Bundle greifbar ist. Per Default wird normal verifiziert.
Testen
Die Tests laufen ohne Wiki.js-Instanz — alle HTTP-Aufrufe gehen über respx
an einen Mock. Geprüft wird, was der Client sendet:
uv run pytest -q
uv run ruff check .
uv run mypy src
Interaktiv mit dem MCP Inspector im Browser, jedes Tool einzeln aufrufbar:
uv run mcp dev src/wikijs_mcp/server.py
Danach ein echter Roundtrip gegen die Instanz, z. B. search_pages mit einem
bekannten Suchbegriff und get_page mit einem bekannten Pfad (etwa home).
Registrierung in Claude Code
claude mcp add wikijs --env WIKIJS_URL=https://wiki.example.com \
--env WIKIJS_TOKEN=<token> \
--env WIKIJS_ALLOWED_PATHS=claude/ \
-- uv run --directory /pfad/zu/wikijs-mcp wikijs-mcp
Registrierung in Claude Desktop
Eintrag unter mcpServers in ~/.config/Claude/claude_desktop_config.json:
{
"mcpServers": {
"wikijs": {
"command": "uv",
"args": ["run", "--directory", "/pfad/zu/wikijs-mcp", "wikijs-mcp"],
"env": {
"WIKIJS_URL": "https://wiki.example.com",
"WIKIJS_TOKEN": "<token>",
"WIKIJS_CA_BUNDLE": "/absoluter/pfad/zum/ca-bundle.crt"
}
}
}
}
Seit 0.2.0 liegt der Server unter src/wikijs_mcp/. Bestehende Konfigurationen
mit server.py als letztem Argument funktionieren über einen Shim weiter; der
Konsolen-Entry-Point wikijs-mcp ist aber der bevorzugte Weg.
Hinweise zum GraphQL-Schema
Die Query- und Mutation-Namen wurden gegen das Schema einer echten
Wiki.js-2.x-Instanz verifiziert (server/graph/schemas/page.graphql). Die
Feldnamen können sich zwischen Wiki.js-Versionen unterscheiden — bei einer
anderen Version die Queries in src/wikijs_mcp/client.py gegen die eigene
Instanz prüfen.