2026-08-16 21:57:01 -07:00
2026-08-16 21:57:01 -07:00
2026-08-16 21:57:01 -07:00
2026-08-16 21:57:01 -07:00
2026-08-16 21:57:01 -07:00
2026-08-16 21:57:01 -07:00

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

  1. 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.
  2. Settings → Roles → (that user's role) → enable Access System API.
  3. Log in as that user → profile menu → Edit ProfileAPI TokensCreate Token.
  4. 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.

  1. https://github.com/settings/developersNew OAuth App
  2. Homepage URL: https://mcp.example.com
  3. Authorization callback URL: https://mcp.example.com/auth/callback (must match exactly)
  4. Generate a client secret.
  5. Put the client ID and secret in .env, set MCP_PUBLIC_URL=https://mcp.example.com, and list your GitHub username in GITHUB_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 GitHubProvider in bookstack_mcp/server.py is 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.com at <mcp-container-ip>:8080. Disable response buffering — streamable HTTP holds a long-lived GET stream open. In Nginx Proxy Manager that means adding proxy_buffering off; and proxy_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's Caddyfile into /etc/caddy/Caddyfile (change bookstack-mcp:8080 to 127.0.0.1:8080), then systemctl 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 → ConnectorsAdd 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=true means 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_USERS populated. An empty allowlist means anyone with a GitHub account who finds your URL can complete the login.
  • MCP_AUTH_MODE=none is 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
S
Description
No description provided
Readme
86 KiB
Languages
Python 86.8%
Shell 13.2%