BookStack MCP Server
A self-hosted MCP server that gives Claude read (and optionally write) access to your BookStack wiki. Speaks streamable HTTP, so the same deployment works with both Claude Code and claude.ai.
Claude Code ──┐
├──► https://mcp.example.com/mcp ──► BookStack REST API
claude.ai ──┘ (this server)
Tools
Read-only (always available):
| Tool | What it does |
|---|---|
search_content |
Full BookStack search, including {type:page} and [tag=value] syntax |
get_page |
Full page body as markdown, plaintext, or HTML |
get_book |
Book metadata plus its full table of contents |
get_chapter |
Chapter metadata plus its page list |
list_books |
Browse books |
list_shelves |
Browse shelves |
list_recent_pages |
Most recently updated pages |
Write tools appear only when BOOKSTACK_READ_ONLY=false: create_page,
update_page, create_book, create_chapter, and — behind a second flag —
delete_page.
1. Create the BookStack API token
- In BookStack, make a dedicated user for Claude. Its roles decide exactly what the MCP server can see, so give it the narrowest access you're comfortable with.
- Settings → Roles → (that user's role) → enable Access System API.
- Log in as that user → profile menu → Edit Profile → API Tokens → Create Token.
- Copy the Token ID and Token Secret.
BookStack rate-limits the API to 180 requests/minute per user by default
(API_REQUESTS_PER_MIN).
2. Configure
cp .env.example .env
$EDITOR .env
Fill in BOOKSTACK_URL, BOOKSTACK_TOKEN_ID, BOOKSTACK_TOKEN_SECRET.
Generate a static token for Claude Code:
openssl rand -hex 32 # paste into MCP_STATIC_TOKENS
3. Set up GitHub OAuth (needed for claude.ai)
claude.ai will not send custom headers to a connector — it only supports OAuth or no auth at all. Rather than hand-rolling an OAuth 2.1 server, this project uses GitHub as the identity provider and restricts access to an allowlist of usernames.
- https://github.com/settings/developers → New OAuth App
- Homepage URL:
https://mcp.example.com - Authorization callback URL:
https://mcp.example.com/auth/callback(must match exactly) - Generate a client secret.
- Put the client ID and secret in
.env, setMCP_PUBLIC_URL=https://mcp.example.com, and list your GitHub username inGITHUB_ALLOWED_USERS.
With MCP_AUTH_MODE=github+token, both paths work at once: claude.ai does the OAuth
dance, Claude Code sends a static bearer token.
Prefer a different IdP? FastMCP ships providers for Google, Auth0, Keycloak, Azure, WorkOS, Clerk, Descope, Supabase, and generic OIDC. Swapping
GitHubProviderinbookstack_mcp/server.pyis a few lines.
4. Deploy
Point a DNS A record for mcp.example.com at your host, edit the domain in
Caddyfile, then:
docker compose up -d --build
docker compose logs -f bookstack-mcp
Caddy handles TLS automatically. Verify:
curl -i https://mcp.example.com/mcp # expect 401 with a WWW-Authenticate header
curl -s https://mcp.example.com/.well-known/oauth-protected-resource/mcp | jq
Give this server its own subdomain. Claude discovers OAuth metadata at
/.well-known/oauth-authorization-server, which lives at the domain root — not
under /mcp. Sharing a domain with BookStack means those paths collide.
The server must be reachable from the public internet; Claude connects from Anthropic's infrastructure, not from your machine. If your BookStack sits on a private network, that's fine — only this MCP server needs to be exposed, and it can reach BookStack over the internal network.
Alternative: LXC container (Proxmox / LXD)
If you already run BookStack in an LXC, this fits the same pattern. It's a single
Python process, so skip Docker entirely and run it as a systemd service — no
nesting, no keyctl, works in an unprivileged container.
Create a container (1 core, 512 MB RAM, 4 GB disk is plenty) on Debian 12+ or Ubuntu 24.04+ — the server needs Python 3.11 or newer, and Debian 11 / Ubuntu 22.04 ship something older. On Proxmox:
pct create 210 local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst \
--hostname bookstack-mcp --cores 1 --memory 512 --rootfs local-lvm:4 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp --unprivileged 1 --features nesting=0
pct start 210 && pct enter 210
Then inside the container:
apt update && apt install -y git
git clone <your-repo> /tmp/bookstack-mcp && cd /tmp/bookstack-mcp
bash deploy/lxc/install.sh
The installer creates a bookstack-mcp system user, builds a venv at
/opt/bookstack-mcp/venv, writes config to /etc/bookstack-mcp/env (mode 640,
readable only by root and the service user), and installs a hardened systemd unit.
nano /etc/bookstack-mcp/env # fill in BookStack URL + token
systemctl start bookstack-mcp
systemctl status bookstack-mcp
journalctl -u bookstack-mcp -f
curl -i http://127.0.0.1:8080/mcp # 401 + WWW-Authenticate = working correctly
To upgrade later, re-run install.sh from the updated source; it preserves your
existing /etc/bookstack-mcp/env.
Networking. BookStack can stay entirely private. Point BOOKSTACK_URL at its
internal address (http://10.0.0.42 or http://bookstack.lan) — only the MCP
container needs to be reachable from the internet, and only on /mcp plus the
/.well-known/* paths.
For TLS you have two options:
- Existing reverse proxy (NPM, Caddy, or Traefik in another container): point
mcp.example.comat<mcp-container-ip>:8080. Disable response buffering — streamable HTTP holds a long-lived GET stream open. In Nginx Proxy Manager that means addingproxy_buffering off;andproxy_read_timeout 3600s;to the custom config for that host. - Caddy in the same container:
apt install caddy, drop the site block from this repo'sCaddyfileinto/etc/caddy/Caddyfile(changebookstack-mcp:8080to127.0.0.1:8080), thensystemctl reload caddy.
If you use a local reverse proxy, set MCP_HOST=127.0.0.1 so the app isn't
directly reachable on the LAN.
If the service won't start with status 226/NAMESPACE or 227/SECCOMP, your
kernel is older than the systemd hardening options expect. Comment out the
hardening block in /etc/systemd/system/bookstack-mcp.service, then
systemctl daemon-reload && systemctl restart bookstack-mcp.
Alternative: Cloudflare Tunnel
If you'd rather not open ports, drop the caddy service and run
cloudflared tunnel --url http://bookstack-mcp:8080 instead. Everything else is
unchanged.
5. Connect Claude Code
claude mcp add --transport http bookstack https://mcp.example.com/mcp \
--header "Authorization: Bearer YOUR_STATIC_TOKEN" \
--scope user
Or use OAuth instead — omit --header, then run /mcp inside Claude Code and
complete the browser login.
Check it: claude mcp list, then /mcp in a session.
To share with a team via a committable .mcp.json without leaking the token:
{
"mcpServers": {
"bookstack": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer ${BOOKSTACK_MCP_TOKEN}" }
}
}
}
6. Connect claude.ai
Settings → Connectors → Add custom connector → URL https://mcp.example.com/mcp.
Leave the Advanced Settings client ID/secret blank — this server supports Dynamic
Client Registration, so Claude registers itself. Click Connect and approve the
GitHub login.
Custom connectors need a Pro, Max, Team, or Enterprise plan. On Team and Enterprise, an Owner adds the connector via Organization Settings before members can enable it.
Testing locally
python -m venv .venv && .venv/bin/pip install -e . starlette
.venv/bin/python test_smoke.py # runs against a fake BookStack, no real instance needed
To inspect the live server's tools:
npx @modelcontextprotocol/inspector
Security notes
- The BookStack token's permissions are the real security boundary. A read-only
BookStack user plus
BOOKSTACK_READ_ONLY=truemeans a prompt injection in a wiki page can't cause damage. - Every Claude user shares one BookStack identity. This server does not map GitHub users to individual BookStack accounts, so per-user BookStack permissions don't apply — everyone sees whatever the token user sees.
- Keep
GITHUB_ALLOWED_USERSpopulated. An empty allowlist means anyone with a GitHub account who finds your URL can complete the login. MCP_AUTH_MODE=noneis for local testing only. Never expose it publicly.- Wiki content is untrusted input. A page containing instructions aimed at an LLM is a real prompt-injection vector — another reason to start read-only.
Troubleshooting
| Symptom | Cause |
|---|---|
| 401 from BookStack | Token wrong, or its user lacks Access System API |
| 403 from BookStack | Token user's role can't see that content |
| claude.ai says "Disconnected" right after Connect | Callback URL mismatch, or /.well-known/* not proxied to this server |
| Claude Code reports a hard failure | An invalid static Authorization header does not fall back to OAuth — remove the header to let OAuth take over |
| Connection drops mid-stream | Reverse proxy buffering; keep flush_interval -1 and disabled timeouts in the Caddyfile |
| 429 | BookStack's 180 req/min limit |