This commit is contained in:
2026-08-16 22:21:19 -07:00
parent 4ad9a62f0f
commit b3212235db
5 changed files with 474 additions and 43 deletions
+115 -17
View File
@@ -32,11 +32,38 @@ Write tools appear only when `BOOKSTACK_READ_ONLY=false`: `create_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.
BookStack's API is authenticated **separately from web login**. API tokens work
the same whether your instance uses standard, LDAP, SAML, or OIDC authentication —
BookStack cannot accept an OIDC access token for API calls, only its own tokens.
So an OIDC-backed instance changes nothing about this step except how you get the
token into existence.
1. 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**.
3. Get a token for that user:
- **Standard auth:** log in as them → profile menu → **Edit Profile**
**API Tokens****Create Token**.
- **OIDC auth:** the service user never needs to log in at all. As an admin,
go to Settings → Users → (the user) and use the **API Tokens** section at the
bottom of the edit view. An admin with both *Manage Users* and *Access System
API* can mint tokens on another user's behalf.
4. Copy the **Token ID** and **Token Secret**. The secret is shown once.
### If you use OIDC group sync
`OIDC_REMOVE_FROM_GROUPS=true` strips any BookStack role that doesn't match a
group from the ID token, on every login. That will silently revoke your service
user's API access if you assigned its role by hand. Two ways around it:
- **Never log the service user in.** Create it in the admin UI and mint its token
as an admin (step 3 above). Group sync only runs at login, so a user that never
authenticates through the IdP never gets re-synced. Simplest option.
- **Or model it in the IdP.** Create a group like `bookstack-api`, map it to a
BookStack role that has *Access System API*, and put the service user in it.
Survives logins, and access is managed where the rest of your access lives.
Either way, a 401 that appears weeks later "for no reason" is almost always this.
BookStack rate-limits the API to 180 requests/minute per user by default
(`API_REQUESTS_PER_MIN`).
@@ -56,25 +83,85 @@ 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)
## 3. Set up 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.
no auth at all. So the server needs an OAuth layer. Rather than hand-rolling an
OAuth 2.1 authorization server, it proxies an existing identity provider.
### Option A: your own OIDC provider (recommended if you have one)
If BookStack already uses OIDC — Authelia, Authentik, Keycloak, Zitadel — point
this server at the same IdP. Same login, same groups, no second identity system.
**Register a new confidential client** for the MCP server. Do not reuse the
BookStack client; the redirect URI differs and they are separate relying parties.
- Redirect URI: `https://mcp.example.com/auth/callback`
- Grant types: `authorization_code` and `refresh_token`
- PKCE with S256
- Scopes: `openid profile email offline_access`, plus your groups scope
Then in `.env`:
```ini
MCP_AUTH_MODE=oidc+token
MCP_PUBLIC_URL=https://mcp.example.com
OIDC_CONFIG_URL=https://auth.example.com/.well-known/openid-configuration
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...
MCP_REQUIRED_GROUPS=wiki-users
```
Discovery URLs by provider:
| Provider | `OIDC_CONFIG_URL` |
|---|---|
| Authelia | `https://auth.example.com/.well-known/openid-configuration` |
| Authentik | `https://auth.example.com/application/o/<app-slug>/.well-known/openid-configuration` |
| Keycloak | `https://auth.example.com/realms/<realm>/.well-known/openid-configuration` |
| Zitadel | `https://auth.example.com/.well-known/openid-configuration` |
**Authelia users:** a ready-made config snippet is in
[`deploy/authelia/configuration.snippet.yml`](deploy/authelia/configuration.snippet.yml).
Three things are easy to miss and each one produces a confusing failure:
1. **Bypass forward-auth for `mcp.example.com`.** If the host sits behind
Authelia's forward-auth, Claude gets an HTML login page instead of the MCP
endpoint. Claude can't hold session cookies — the MCP server runs its own
OAuth flow instead. Add an `access_control` rule with `policy: 'bypass'`.
2. **Add a claims policy.** Since 4.39, Authelia leaves non-standard claims out
of the ID Token by default, so `groups` won't be there and every tool call
gets denied. Put `groups` and `preferred_username` back via
`claims_policies`.
3. **Set `OIDC_VERIFY_ID_TOKEN=true`.** Authelia issues opaque access tokens,
which can't be verified as JWTs. The ID Token always can.
Also worth setting a custom `lifespan` with a long `refresh_token` — Authelia's
default is 90 minutes, after which an idle claude.ai connector needs manual
re-authorisation.
For nested group claims, `OIDC_GROUPS_CLAIM` takes dot-notation, e.g. Keycloak's
`resource_access.bookstack-mcp.roles`. If your provider rejects any of the scopes
above, override the whole list with `OIDC_SCOPES`.
### Option B: GitHub (if you have no IdP)
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`.
4. Set `MCP_AUTH_MODE=github+token` plus `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`.
With `MCP_AUTH_MODE=github+token`, both paths work at once: claude.ai does the OAuth
dance, Claude Code sends a static bearer token.
### Who gets in
> 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.
`MCP_ALLOWED_USERS` matches on `login` / `preferred_username` / `email` / `sub`,
case-insensitively. `MCP_REQUIRED_GROUPS` requires membership of at least one
listed group. Set at least one of them — leaving both empty means anyone who can
authenticate to your IdP can read your wiki through Claude. The server logs a
warning at startup if you do.
With `oidc+token` (or `github+token`), both paths work at once: claude.ai does the
OAuth dance, Claude Code sends a static bearer token that bypasses OAuth entirely.
## 4. Deploy
@@ -231,8 +318,13 @@ npx @modelcontextprotocol/inspector
- 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.
- Keep `MCP_ALLOWED_USERS` or `MCP_REQUIRED_GROUPS` populated. Empty means anyone
who can authenticate to your IdP gets in — and with a public GitHub app, that is
everyone on GitHub.
- Everyone who connects shares one BookStack identity, so per-user BookStack
permissions do not apply. `MCP_REQUIRED_GROUPS` gates *who may connect*; it does
not scope *what they see*. If different people need different wiki visibility,
run one instance per group with its own BookStack service user.
- `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.
@@ -245,5 +337,11 @@ npx @modelcontextprotocol/inspector
| 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 |
| Worked for weeks, now 401 from BookStack | OIDC group sync stripped the service user's role on its last login — see the group sync note in step 1 |
| OAuth completes but every tool call is denied | Username or group claim doesn't match `MCP_ALLOWED_USERS` / `MCP_REQUIRED_GROUPS`; check `journalctl -u bookstack-mcp` for the exact value seen |
| `invalid_scope` from your IdP | The groups scope isn't defined on that client; add it, or drop `MCP_REQUIRED_GROUPS` |
| Token verification fails with an opaque token | Set `OIDC_VERIFY_ID_TOKEN=true` (Authelia, Okta) |
| Claude shows a login page or redirect loop instead of connecting | The MCP host is behind forward-auth; add an Authelia `bypass` rule for it |
| Connector drops after ~90 minutes idle | Authelia's default refresh token lifespan; set a custom `lifespan` on the client |
| Connection drops mid-stream | Reverse proxy buffering; keep `flush_interval -1` and disabled timeouts in the Caddyfile |
| 429 | BookStack's 180 req/min limit |