update
This commit is contained in:
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user