Files
bookstack-mcp/README.md
T
2026-08-16 21:57:01 -07:00

250 lines
9.3 KiB
Markdown

# 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 Profile****API Tokens****Create 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
```bash
cp .env.example .env
$EDITOR .env
```
Fill in `BOOKSTACK_URL`, `BOOKSTACK_TOKEN_ID`, `BOOKSTACK_TOKEN_SECRET`.
Generate a static token for Claude Code:
```bash
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/developers → **New 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:
```bash
docker compose up -d --build
docker compose logs -f bookstack-mcp
```
Caddy handles TLS automatically. Verify:
```bash
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:
```bash
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:
```bash
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.
```bash
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
```bash
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:
```json
{
"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
```bash
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:
```bash
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 |