wikijs-mcp
Provides tools for searching, listing, reading, creating, updating, moving, and viewing history/versions of Wiki.js pages as markdown, while enforcing locale restrictions, blocked tags, and an LLM-generated marker.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@wikijs-mcpsearch our wiki for the incident response runbook"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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)
By construction — this server implements no comment queries/mutations;
pages.*operations never carry comment data.By auth — the Wiki.js API key is bound to a dedicated "MCP-bot" group that has
read/write/manage:pagesbut none ofread/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 |
| Full-text search → | on |
| List/browse pages (optional path prefix and tag filter) | on |
| Read a page's markdown by id or path | on |
| Create a markdown page | on |
| Update a page's markdown by id (partial; unset fields preserved) | on |
| Move/rename a page's path, keeping its history | on |
| List a page's revision trail | on |
| Read one historical version's content | on |
| 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 themIt 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_searchandwiki_listresults — 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_versionandwiki_restoretouching 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.mdA 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
Administration → Groups → New Group → name it
MCP-bot.Global permissions: enable
read:pages,write:pages,manage:pages. Do NOT enable any*:commentspermission.Page Rules (this is required, not optional — see the Wiki.js
pages.singlequirk, GitHub #6216): add a rule grantingread,write,manageon path `` (empty = all paths) or scope it to the paths the bot should manage (e.g.infra). Without a matching page rule,wiki_readreturns "not authorized" even with the global permission set.Administration → API → enable the API, Create New Key, assign it to the
MCP-botgroup, copy the token intoWIKIJS_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 --buildThe 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:
/mcpExpect wikijs listed as connected, with the wiki_* tools (nine, minus anything in
WIKIJS_DISABLED_TOOLS). To remove it:
claude mcp remove wikijs.
Route B — via the Tier 2 docs plugin (recommended)
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/repohere 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_readsays "not authorized" — missing Wiki.js page rule, not a Claude problem. See step 3 of the one-time setup above.TLS errors against a
.lanhost — 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.
This server cannot be installed
Maintenance
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
- hiveWikiOAuthai.hivewiki
Shared project wiki for AI agents: read and write pages, next actions, and activity logs over MCP.
Read, edit, publish, and preview your pepita websites from Claude.
Search, read, create and edit your Memol notes from Claude. Team note-taking with AI search.
Read spaces, collections, pages and content; search docs and manage GitBook organization spaces.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceA Model Context Protocol server that enables Claude to read and update documentation in Wiki.js instances through capabilities like searching, reading, creating, and updating wiki pages.17-
- AlicenseBqualityAmaintenanceConnects BookStack knowledge bases to Claude through 47+ tools covering complete CRUD operations for books, pages, chapters, shelves, users, search, attachments, and permissions. Enables full management of BookStack content and configuration through natural language.5641384MIT
- FlicenseNot gradedqualityCmaintenanceEnables comprehensive management of WikiJS instances through tools for searching, creating, updating, and deleting wiki pages. It supports advanced features like knowledge graph exploration, page summaries, and content exporting for documentation workflows.-
- AlicenseNot gradedqualityCmaintenanceEnables AI assistants to search, retrieve, and optionally edit pages in a WikiJS knowledge base via the Model Context Protocol.401MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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