Skip to main content
Glama

wikijs-mcp (Tier 1)

Remote MCP server exposing Wiki.js pages (read/write markdown) to Claude. Pages only — never comments. Comments are private user-to-user data and are unreachable by this server through two independent layers.

Part of a two-tier documentation system

This repo is Tier 1 of a dual-purpose (human- and LLM-readable) docs setup, split across two independent repos:

  • Tier 1 — this repo: the remote MCP server that exposes Wiki.js pages as the global knowledge base (knowledge broader than any single project).

  • Tier 2 — ../claude-marketplace: a companion Claude Code docs plugin that enforces per-project local docs and bundles this MCP's config. That marketplace is self-hosted and deployment-specific; this repo does not depend on it.

Knowledge routing: broader-than-project facts → global Wiki.js (this server); project-specific detail → local CLAUDE.md / docs/.

Related MCP server: BookStack MCP Server

Comment isolation (two layers)

  1. By construction — this server implements no comment queries/mutations; pages.* operations never carry comment data.

  2. By auth — the Wiki.js API key is bound to a dedicated "MCP-bot" group that has read/write/manage:pages but none of read/write/manage:comments. Even a crafted request is denied by Wiki.js.

Layer 2 only works if the key is actually bound to that group. A key created with administrator scope ignores the group's permissions and its page rules entirely — it will happily write comments and reach paths the group was never granted. This is easy to miss because everything the MCP itself does keeps working, so nothing looks broken. Run the check under Verify comment isolation after any key rotation; if a comment mutation succeeds, the key is over-scoped and only layer 1 is protecting you.

Tools

Tool

Purpose

Default

wiki_search

Full-text search → totalHits, suggestions, page summaries

on

wiki_list

List/browse pages (optional path prefix and tag filter)

on

wiki_read

Read a page's markdown by id or path

on

wiki_create

Create a markdown page

on

wiki_update

Update a page's markdown by id (partial; unset fields preserved)

on

wiki_move

Move/rename a page's path, keeping its history

on

wiki_history

List a page's revision trail

on

wiki_version

Read one historical version's content

on

wiki_restore

Roll a page back to a past version

off

There is deliberately no delete tool. Removing a page is a human action, done in the Wiki.js UI.

Every tool in that table is subject to the locale lock and the tag blocklist below.

wiki_search repairs the ids it returns before handing them over. Under Wiki.js's postgres search engine, results come from the pagesVector index table, which has its own serial id and no page-id column — so the engine's id addresses a different page under pages.single(id:). The server maps each hit back to its real page id by path, which is the only key the two sides share. An id from wiki_search is therefore safe to pass straight to wiki_read; a hit whose path is not in the locked locale is dropped rather than guessed at.

Any tool can be withheld via WIKIJS_DISABLED_TOOLS (comma-separated). Withheld tools are never registered, so they do not appear in tools/list at all — invisible to the model rather than present-but-erroring. wiki_restore ships withheld because it overwrites live content wholesale.

Locale lock

The server is pinned to exactly one locale, set by WIKIJS_LOCALE (required — there is no default). No tool exposes a locale field, so the calling model cannot see that other locales exist, let alone choose one. Pages in any other locale are invisible and unwritable here, including by numeric id: wiki_read re-checks the locale of whatever the id resolved to and refuses a mismatch.

This exists because a per-request locale argument (and a WIKIJS_DEFAULT_LOCALE that silently defaulted to en) let agents build a second, parallel page tree in the wrong locale. Note this is a namespace concern, not a language one — writing English prose into a pl-locale wiki is fine and expected.

To repair an existing split, move every page from one locale to another (paths and content untouched — nothing is translated):

# inside the container, or anywhere WIKIJS_URL/WIKIJS_API_TOKEN are set
node dist/migrate-locale.js --from en --to pl            # dry run: lists moves, checks collisions
node dist/migrate-locale.js --from en --to pl --apply    # perform them

It refuses to move anything if even one destination path is already taken, since a half-finished migration is much harder to unpick than a refused one.

Blocked tags

Some pages must never reach a model at all — local network topology and addresses, credential inventories, the machine-readable input a security scanner consumes. WIKIJS_BLOCKED_TAGS (comma-separated, required — there is no default) names the Wiki.js tags that withhold such a page. Such a page is announced but never served:

  • it still appears in wiki_search and wiki_list results — including when the caller filters for exactly that tag — but as a placeholder: withheld: true, with its title and description replaced by a notice saying the page is a security vault whose content will not be returned;

  • every wiki_read, wiki_create, wiki_update, wiki_move, wiki_history, wiki_version and wiki_restore touching it is refused, by id and by path alike, with that same notice.

Announcing beats hiding here. The content is unreachable either way, and a page that silently is not there is exactly what sends an agent looking for the next route to it; a wall it can see makes it report back to you instead.

The check lives in src/wiki-client.ts, beside the locale lock and for the same reason: enforcement sits under every tool rather than inside each one, so a tool added later inherits it instead of having to remember. It is code, not a prompt — the tool descriptions mention the rule only so the model reports the refusal to you instead of hunting for a way around it.

The tag is applied by hand, in the Wiki.js UI. The MCP refuses to apply a blocked tag as well as to honour one: wiki_create/wiki_update reject a tag set containing one. Blocking is a human decision (the same reasoning that keeps back-marking off the tool surface), and this also means text inside a page cannot talk an agent into hiding pages by tagging them.

WIKIJS_BLOCKED_TAGS is required() for a sharper reason than WIKIJS_LOCALE is. A default of "block nothing" would mean one missing line in .env silently exposes every protected page, with nothing appearing broken. Refusing to boot is the safe failure — so adding this variable to an existing deployment is a breaking config change: set it before you redeploy, or the container crash-loops. The active list is printed at startup; check the logs after a deploy to confirm the tag there matches the tag on the pages.

Two things it deliberately does not do. Blocking is per page, not per subtree — a child of a blocked page needs its own tag. And it does not apply to mark-llm.js or migrate-locale.js: those are human-run CLIs that never hand content to a model, the same reason they are not MCP tools.

LLM-generated marker

Every write through wiki_create, wiki_update (and after wiki_restore) is stamped, by the server, as machine-written:

<!-- wikijs-mcp:llm-generated -->
> 🤖 **Generated by an LLM.** Written by an AI agent through `wikijs-mcp` on 2026-08-28. Verify before relying on it.
{.is-warning}
<!-- /wikijs-mcp:llm-generated -->

plus an llm-generated tag, so every AI-written page can be listed at once (wiki_list with tags: ["llm-generated"]).

The wording is a compile-time constant in src/marker.ts and the date comes from the server clock. No tool input reaches it: the model cannot write it, reword it, omit it, or influence it — that is the whole point. The {.is-warning} line is Wiki.js 2 blockquote-callout syntax and renders as a warning box.

The HTML-comment sentinels make the stamp idempotent: a repeated write replaces the block rather than stacking banners, and refreshes the date. If someone hand-deletes the banner, the next write puts it back.

Back-marking pages written before this existed

Pages an LLM wrote before the stamp landed carry no marker, and Wiki.js cannot tell you which those are — every page shows creatorId = 1 because the API key authenticates as the admin user. A worksheet records the verdict per page ([x] written through this server, [p] generated in a chat and pasted in by hand, [a] written by a scheduled job, [ ] human, [?] undecided), and a one-shot script applies it. Copy LLM-AUDIT.example.md to LLM-AUDIT.md and fill it in — a filled-in worksheet quotes real page paths and opening lines out of your wiki, so LLM-AUDIT.md is gitignored and never committed:

docker compose exec -T wikijs-mcp node dist/mark-llm.js --file - < LLM-AUDIT.md           # dry run
docker compose exec -T wikijs-mcp node dist/mark-llm.js --file - --apply < LLM-AUDIT.md

A back-marked banner carries the page's own creation date, not today's. [p] pages get wording that does not claim wikijs-mcp authorship, and [a] pages get a different banner altogether — generated by automation, edits may be overwritten — plus the automated tag rather than llm-generated, because a CI job is not an LLM. The script is idempotent (an already-correct page is reported unchanged and not rewritten) and reversible (flipping a row to [ ] strips the banner and the tag, restoring the body byte-for-byte).

Like the locale migration, this is not an MCP tool: deciding what an LLM wrote is a human judgement, and a model that could add or remove its own attribution would defeat the point.

One-time Wiki.js setup

  1. Administration → Groups → New Group → name it MCP-bot.

  2. Global permissions: enable read:pages, write:pages, manage:pages. Do NOT enable any *:comments permission.

  3. Page Rules (this is required, not optional — see the Wiki.js pages.single quirk, GitHub #6216): add a rule granting read, write, manage on path `` (empty = all paths) or scope it to the paths the bot should manage (e.g. infra). Without a matching page rule, wiki_read returns "not authorized" even with the global permission set.

  4. Administration → API → enable the API, Create New Key, assign it to the MCP-bot group, copy the token into WIKIJS_API_TOKEN.

Configure & run

cp .env.example .env      # fill in WIKIJS_URL, WIKIJS_API_TOKEN, WIKIJS_LOCALE,
                          #          WIKIJS_BLOCKED_TAGS, MCP_BEARER_TOKEN
# local dev
npm install && npm run dev
# or containerized, beside Wiki.js
docker compose up -d --build

The MCP endpoint is POST/GET/DELETE /mcp and requires Authorization: Bearer $MCP_BEARER_TOKEN. Put it behind your reverse proxy with TLS; never expose it raw.

Smoke test (before wiring Claude)

# initialize handshake (expect a JSON-RPC result, not 401)
curl -sS -X POST https://wiki-mcp.example.com/mcp \
  -H "Authorization: Bearer $MCP_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

Add to Claude Code

Two routes. Pick one — installing the plugin while a manual wikijs entry exists gives you the server twice.

Route A — directly (this server alone)

claude mcp add --transport http --scope user wikijs https://wiki-mcp.example.com/mcp \
  --header "Authorization: Bearer $MCP_BEARER_TOKEN"

--scope user registers it across all your projects, which is what you want for a global knowledge base. Use --scope local to limit it to the current project, or --scope project to write a shared .mcp.json into the repo — but note that scope commits the file, so keep the token in an env var rather than inlining it.

Verify from inside Claude Code:

/mcp

Expect wikijs listed as connected, with the wiki_* tools (nine, minus anything in WIKIJS_DISABLED_TOOLS). To remove it: claude mcp remove wikijs.

This is the intended path: the plugin bundles the same MCP config and the per-project doc enforcement, so the two tiers arrive together. The marketplace and plugin names below are placeholders — substitute your own, since Tier 2 is self-hosted per deployment.

export WIKI_MCP_URL=https://wiki-mcp.example.com/mcp
export WIKI_MCP_TOKEN=<your MCP_BEARER_TOKEN>

Put those in your shell profile — the plugin's .mcp.json interpolates them at load time, and Claude Code will not resolve them if they're only set in a subshell. (Match the variable names to whatever your plugin's .mcp.json actually reads.) Then, inside Claude Code:

/plugin marketplace add https://git.example.com/<org>/claude-marketplace.git
/plugin install <docs-plugin>@<marketplace>

Or, if you have the repo cloned beside this one, point at the working copy instead: /plugin marketplace add ../claude-marketplace. Restart the session and confirm with /mcp.

A bare owner/repo here would resolve against GitHub — this marketplace is self-hosted Gitea, so always pass the full URL or a path.

If it doesn't connect

  • 401 — token mismatch between the client header and the server's MCP_BEARER_TOKEN.

  • Connected, but wiki_read says "not authorized" — missing Wiki.js page rule, not a Claude problem. See step 3 of the one-time setup above.

  • TLS errors against a .lan host — Claude Code will not accept a self-signed cert; terminate TLS at a reverse proxy with a cert your machine trusts.

Verify comment isolation

With the same API key, run a comment mutation directly against Wiki.js GraphQL and confirm it is denied — proving the group-permission layer:

mutation { comments { create(pageId: 1, replyTo: 0, content: "x") { responseResult { succeeded message } } } }

Expect succeeded: false / an authorization error. (The MCP itself exposes no such tool at all.)

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessUnresponsive

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/artur-matkowski/wikijs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server