Skip to content

About

A Model Context Protocol (MCP) server providing full access to BookStack's knowledge management capabilities

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

BookStack MCP Server

Connect BookStack to Claude and other AI assistants through the Model Context Protocol (MCP). This server exposes 71 tools, 5 resources and 6 resource templates covering the supported subset of the BookStack API — books, pages, chapters, shelves, search, users, roles, permissions, attachments, images, comments, ZIP imports, tags, the recycle bin, the audit log and system info.

This server supports two transport modes: Streamable HTTP (default) and Stdio.

  • Streamable HTTP (default): A stateless HTTP transport. The BookStack token can be overridden per request with x-bookstack-token, and the BookStack URL with x-bookstack-url when the operator allows it (see per-request overrides).
  • Stdio Mode: Standard input/output for local integration (e.g., with Claude Desktop). Set MCP_TRANSPORT=stdio to enable.

⚠️ Looking for the HTTP endpoint? The MCP endpoint is POST /message — not /. See Transports and HTTP endpoints below.

🍴 About this fork

This is a fork of pnocera/bookstack-mcp-server, maintained at cego/bookstack-mcp-server. Its primary use is per-user OAuth: each person signs in to their AI assistant with your OIDC provider, and BookStack sees their account, permissions and audit trail instead of one shared API token.

What the fork adds on top of upstream:

  • Per-user OAuth (MCP_AUTH_MODE=oauth): the HTTP transport is an OAuth resource server per the MCP authorization spec. Access tokens must be issued for this server, and each one is exchanged (RFC 8693) for a BookStack token, so the caller's own token is never forwarded. See Per-user OAuth for the full flow and requirements.
  • More tools: comments, ZIP imports, tags and ZIP export.
  • Hardening from a full security review: allowlisted upstream overrides, guarded and size-limited uploads, linear-time page parsing, and no stack traces or paths in errors.
  • Container images instead of npm releases.

Run it with OAuth

Images are published as ghcr.io/cego/bookstack-mcp-server:<version>-r<revision>, where <version> is the upstream version the fork builds on and <revision> counts the fork's releases of it. See Releases for the current tag and digest, and pin the digest when you deploy.

# bookstack-mcp.env - keep it out of version control; it holds the client secret
BOOKSTACK_BASE_URL=https://bookstack.example.com/api
MCP_AUTH_MODE=oauth
MCP_OAUTH_ISSUER=https://idp.example.com
MCP_OAUTH_RESOURCE=https://bookstack-mcp.example.com/message
MCP_OAUTH_CLIENT_ID=bookstack-mcp-server
MCP_OAUTH_CLIENT_SECRET=<exchange client secret>
BOOKSTACK_OAUTH_AUDIENCE=bookstack-api
docker run -d --name bookstack-mcp -p 3000:3000 --env-file bookstack-mcp.env \
  ghcr.io/cego/bookstack-mcp-server:<version>-r<revision>

Before it works end to end you need:

  1. A BookStack that accepts OIDC access tokens on its API (OIDC_API_ACCESS_TOKENS=true, from BookStack PR 6237), with OIDC_API_AUDIENCE set to BOOKSTACK_OAUTH_AUDIENCE and OIDC_API_ALLOWED_CLIENTS set to MCP_OAUTH_CLIENT_ID. Users must have logged in to BookStack once.
  2. Two OAuth clients at your provider: one that users sign in with, whose access tokens carry MCP_OAUTH_RESOURCE and MCP_OAUTH_CLIENT_ID (but not BookStack's audience) in aud, and the confidential exchange client MCP_OAUTH_CLIENT_ID, held only by this server, allowed to exchange those tokens for BOOKSTACK_OAUTH_AUDIENCE.
  3. The server reachable at MCP_OAUTH_RESOURCE, including /.well-known/oauth-protected-resource/message on the same origin.

Then connect your assistant with the sign-in client, never the exchange client:

# Claude Code
claude mcp add --transport http --client-id <sign-in client id> --client-secret \
  --callback-port <port> bookstack https://bookstack-mcp.example.com/message

For a Claude custom connector, use https://bookstack-mcp.example.com/message as the URL and the sign-in client's ID (and secret, if it is confidential) under Advanced settings.

The rest of this README is upstream's documentation, kept current for this fork. The shared-token and stdio modes below still work.

✨ What You Get

  • BookStack Integration - Access your books, pages, chapters, and content
  • 71 MCP Tools, 5 Resources & 6 Resource Templates - CRUD, search and export across the supported endpoint families
  • Search & Export - Find content and export in multiple formats
  • User Management - Handle users, roles, and permissions
  • Production Ready - Rate limiting, validation, error handling, and logging

🚀 Quick Start

ℹ️ bunx/bun add below install upstream's npm release, which lacks this fork's changes. For the fork, use the container image (Run it with OAuth) or run from a clone (bun install && bun run start).

⚠️ Requires Bun 1.1.0 or newer. Node.js is not supported. This package ships TypeScript source rather than a compiled bundle, and its executable starts with #!/usr/bin/env bun — Bun must be installed on the machine that runs it. npx/npm install -g will not work.

Configure first — the default HTTP transport refuses to start until both tokens below are set:

# 1. Configure
export BOOKSTACK_BASE_URL="https://your-bookstack.com/api"
export BOOKSTACK_API_TOKEN="token_id:token_secret"   # OUTBOUND: the credential this server spends
export MCP_AUTH_TOKEN="$(openssl rand -hex 32)"      # INBOUND: who may make it spend that credential

# 2. Run without installing (starts the HTTP server on port 3000)
bunx bookstack-mcp-server

# Or install globally, then run it by name
bun add -g bookstack-mcp-server
bookstack-mcp-server

The two tokens are not interchangeable and must not be set to the same value: BOOKSTACK_API_TOKEN is what the server presents to BookStack; MCP_AUTH_TOKEN is what callers must present to POST /message, which dispatches all 71 tools with the authority of the BookStack account behind BOOKSTACK_API_TOKEN. Skip MCP_AUTH_TOKEN only for stdio, which has no network surface and ignores it.

Check it started:

curl http://localhost:3000/          # => {"status":"running", ...}
curl -i http://localhost:3000/health # => 200 healthy, or 503 if BookStack is unreachable

Add to Claude

To use with Claude Desktop (requires Stdio mode):

# For Claude Code
claude mcp add bookstack bunx bookstack-mcp-server \
  --env BOOKSTACK_BASE_URL=https://your-bookstack.com/api \
  --env BOOKSTACK_API_TOKEN=token_id:token_secret \
  --env MCP_TRANSPORT=stdio

Configuration

Set these environment variables:

export BOOKSTACK_BASE_URL="https://your-bookstack.com/api"
export BOOKSTACK_API_TOKEN="token_id:token_secret"

# Required for the HTTP transport (the default); ignored by stdio.
export MCP_AUTH_TOKEN="$(openssl rand -hex 32)"

# Optional: transport mode — "http" (default) or "stdio"
export MCP_TRANSPORT="http"

💡 Token Format: Combine your BookStack Token ID and Token Secret as token_id:token_secret

Environment variables

Variable Default Description
MCP_TRANSPORT http Transport mode. Only the exact value stdio selects stdio; any other value (or unset) starts the HTTP server.
MCP_AUTH_TOKEN (none — required for HTTP, unless MCP_AUTH_MODE=oauth) Inbound secret callers must present as Authorization: Bearer <value> on POST /message. The HTTP transport refuses to start without it — there is no "no auth" mode. Unrelated to BOOKSTACK_API_TOKEN, and must not be the same value. Ignored in stdio mode. Generate with openssl rand -hex 32.
MCP_AUTH_MODE token token uses MCP_AUTH_TOKEN; oauth makes the HTTP transport an OAuth resource server that calls BookStack as each user. See Per-user OAuth.
MCP_OAUTH_ISSUER (required with oauth) OIDC issuer the access tokens come from (its exact iss value).
MCP_OAUTH_RESOURCE (required with oauth) This server's public /message URL. Access tokens must carry it in aud.
MCP_OAUTH_CLIENT_ID / MCP_OAUTH_CLIENT_SECRET (required with oauth) Confidential client only this server holds, used for token exchange. Not the client MCP clients sign in with.
BOOKSTACK_OAUTH_AUDIENCE (required with oauth) Audience BookStack requires on access tokens (its OIDC_API_AUDIENCE).
BOOKSTACK_BASE_URL http://localhost:8080/api Full URL to the BookStack API. Must be a valid URL and include the /api suffix.
BOOKSTACK_API_TOKEN (none — required, unless MCP_AUTH_MODE=oauth) Outbound BookStack API token as token_id:token_secret. This is the credential the server spends on every tool call. Startup fails if unset.
BOOKSTACK_TIMEOUT 30000 BookStack request timeout in milliseconds.
BOOKSTACK_ALLOWED_BASE_URLS (none) Comma-separated BookStack API base URLs a caller may name in the x-bookstack-url header, compared canonically. Unset refuses that header. See per-request overrides.
BOOKSTACK_UPLOAD_ROOT (none) Directory the image, attachment and import tools' file_path may read from. Unset refuses file_path under every transport, stdio included; the path must resolve inside this directory. Must be unset with MCP_AUTH_MODE=oauth.
SERVER_PORT 3000 Port the HTTP transport listens on; if it is taken, startup fails with exit code 1. Ignored in stdio mode.
HTTP_BODY_LIMIT 73400320 (70 MiB) Maximum accepted POST /message body, in bytes. Sized for the largest inline base64 upload the image/attachment tools advertise (50,000 KB). Express's own default is ~100 KB, which would reject real uploads with a 413. Lower it if untrusted callers can reach the port.
SERVER_NAME bookstack-mcp-server Server name reported over MCP and by GET /.
SERVER_VERSION the package's own version Version reported over MCP initialize, by GET /, and by bookstack_server_info. Defaults to package.json#version; leave it unset unless you deliberately want a different value.
RATE_LIMIT_REQUESTS_PER_MINUTE 60 Outbound rate limit toward BookStack.
RATE_LIMIT_BURST_LIMIT 10 Outbound burst allowance toward BookStack.
VALIDATION_ENABLED true Input validation. Set to false to disable.
VALIDATION_STRICT_MODE true Reject invalid tool params at the boundary. Set to false to log a warning and forward them to BookStack instead.
LOG_LEVEL info One of error, warn, info, debug.
LOG_FORMAT pretty One of json, pretty.

💡 Need detailed setup? See the complete Setup Guide

🔌 Transports

The transport is chosen at startup from MCP_TRANSPORT:

MCP_TRANSPORT Result
unset (default) Streamable HTTP server on SERVER_PORT (default 3000)
http Same as unset
stdio Stdio transport — reads MCP messages from stdin

Stdio is opt-in. If you do not set MCP_TRANSPORT=stdio, you get the HTTP server.

HTTP endpoints

When running in HTTP mode the server exposes three endpoints, plus the OAuth metadata document in OAuth mode. GET and DELETE on /message answer 405 with Allow: POST, before authentication: the server offers no SSE stream and no sessions. Any other path returns a JSON 404 listing the valid ones.

Method & path Purpose Status codes
GET / Server info JSON (name, version, status: "running", endpoint list) 200
GET /health Health check — verifies live connectivity to BookStack 200 healthy, 503 unhealthy
POST /message The MCP endpoint. Send JSON-RPC MCP messages here 200, 400 on a malformed body or refused override, 401 without a valid bearer token, 500 on error

GET / and GET /health are unauthenticated. POST /message requires an inbound Authorization: Bearer <secret> header — it dispatches every tool, including permanent-delete and user/role operations, so the HTTP transport refuses to start without a secret configured. The startup error names the exact variable to set; the stdio transport has no network surface and needs none.

Check the server is up:

curl http://localhost:3000/
{
  "name": "bookstack-mcp-server",
  "version": "1.0.0",
  "status": "running",
  "mcp": true,
  "endpoints": {
    "health": "/health",
    "message": "/message (POST, requires an Authorization: Bearer header)"
  },
  "documentation": "Send MCP protocol messages to POST /message"
}

Check health:

curl -i http://localhost:3000/health

/health verifies live connectivity to BookStack, so a wrong token returns 503 with the failing check named:

{
  "status": "unhealthy",
  "checks": [
    { "name": "bookstack_connection", "healthy": false, "message": "BookStack API connection" },
    { "name": "tools_loaded", "healthy": true, "message": "71 tools loaded" },
    { "name": "resources_loaded", "healthy": true, "message": "5 resources and 6 resource templates loaded" }
  ]
}

⚠️ A missing BOOKSTACK_API_TOKEN behaves differently: the transport refuses to start. In HTTP token mode the process prints Failed to start HTTP transport: Configuration validation failed: bookstack.apiToken: BookStack API token is required - set BOOKSTACK_API_TOKEN environment variable to stderr and exits 1 before Express ever listens. There is no /health to call — curl gets a connection refused, and under Docker the container restart-loops. A 503 therefore always means the token is present but not working; a dead port means it is absent. Under stdio the same reason is printed as Failed to start stdio transport: …, stdout stays empty and the process exits 1.

Call the MCP endpoint — an initialize handshake. Both the Content-Type and Accept headers are required by the Streamable HTTP transport, and Authorization carries the same MCP_AUTH_TOKEN you exported in the quick start:

# Fail fast rather than sending an empty bearer header and puzzling over a 401.
: "${MCP_AUTH_TOKEN:?export MCP_AUTH_TOKEN first — the inbound secret this server was started with}"

curl -X POST http://localhost:3000/message \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $MCP_AUTH_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": { "name": "curl", "version": "1.0.0" }
    }
  }'

Per-request overrides

POST /message accepts two optional headers in token mode. OAuth mode refuses both.

  • x-bookstack-token alone spends the caller's own BookStack token at BOOKSTACK_BASE_URL.
  • x-bookstack-url is refused with a 400 unless it names one of BOOKSTACK_ALLOWED_BASE_URLS, and then x-bookstack-token is required too. A caller-chosen URL is never sent the configured BOOKSTACK_API_TOKEN.

Per-user OAuth

With MCP_AUTH_MODE=oauth the HTTP transport follows the MCP authorization spec: each person signs in with your OIDC provider, and BookStack sees their own account, permissions and audit trail instead of one shared API token.

  1. A request without a token gets 401 with WWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource/message", which points the client at MCP_OAUTH_ISSUER.
  2. Each access token is checked against the issuer's published keys (RS256), plus iss, exp, sub, and an aud containing MCP_OAUTH_RESOURCE.
  3. The server exchanges it (RFC 8693) as MCP_OAUTH_CLIENT_ID for a token with aud=BOOKSTACK_OAUTH_AUDIENCE, cached until shortly before it expires, and calls BookStack with that. The caller's token is never forwarded.

Requirements:

  • BookStack accepts OIDC access tokens on its API (OIDC_API_ACCESS_TOKENS=true, from BookStack PR 6237), with OIDC_API_ALLOWED_CLIENTS set to MCP_OAUTH_CLIENT_ID. Users must have logged in to BookStack once.
  • Two OAuth clients. The one MCP clients sign in with issues access tokens whose aud contains MCP_OAUTH_RESOURCE and MCP_OAUTH_CLIENT_ID, but not BookStack's audience, so those tokens are useless against BookStack directly. Most providers ignore RFC 8707's resource parameter, so configure that audience as a fixed one. MCP_OAUTH_CLIENT_ID is a separate confidential client, held only by this server, that may exchange those tokens for BOOKSTACK_OAUTH_AUDIENCE.
  • MCP_AUTH_TOKEN, BOOKSTACK_API_TOKEN and BOOKSTACK_UPLOAD_ROOT are unset; the server refuses to start otherwise (an upload root would be readable by every user).

GET /health then checks issuer discovery and that BookStack answers, since there is no service token to call it with. Connect Claude Code with the sign-in client, using the callback port registered for it:

claude mcp add --transport http --client-id <sign-in client id> --client-secret \
  --callback-port <port> bookstack https://bookstack-mcp.example.com/message

Using with n8n

Point n8n's MCP client at the /message path — not the root URL:

http://<host>:3000/message

Use the host as n8n sees it: http://localhost:3000/message when n8n runs on the same machine, or the container/service name (e.g. http://mcp:3000/message) when both run in Docker on a shared network.

Running stdio in Docker

A stdio MCP server reads requests from stdin — so docker run without -i gives it no stdin, stdin hits EOF immediately, and the container exits on startup. Pass -i to keep stdin attached:

docker run -i --rm \
  -e MCP_TRANSPORT=stdio \
  -e BOOKSTACK_BASE_URL=https://your-bookstack.com/api \
  -e BOOKSTACK_API_TOKEN=token_id:token_secret \
  bookstack-mcp-server

Use -i alone, not -it: allocating a TTY breaks the JSON-RPC stream that MCP clients pipe over stdin/stdout. For stdio you also don't need -p 3000:3000 — nothing listens on a port in stdio mode.

If your container "starts then dies immediately" with MCP_TRANSPORT=stdio, a missing -i is almost always the cause.

🛠️ Available Tools

71 tools across 16 categories:

  • 📚 Books (6) - Create, read, update, delete, and export books (html, pdf, plaintext, markdown, zip)
  • 📄 Pages (9) - Manage pages with HTML/Markdown content, including partial editing
  • 📑 Chapters (6) - Organize pages within books
  • 📚 Shelves (5) - Group books into collections
  • 🔍 Search (1) - Search across content types
  • 👥 Users (5) - User account management
  • 🎭 Roles (5) - Roles and their permissions
  • ⚙️ System (2) - Instance info and the audit log
  • 🔐 Permissions (2) - Content access control
  • 🗑️ Recycle Bin (3) - Deleted item recovery
  • 📎 Attachments (5) - File attachments
  • 🖼️ Images (5) - Image gallery
  • 💬 Comments (5) - Page comments, replies and archiving
  • 📦 Imports (5) - Upload and run BookStack ZIP imports
  • 🏷️ Tags (2) - Tag names and values in use
  • 🧭 Meta (5) - Ask the server about its own tools and conventions

Not exposed (no tools): the image-gallery data endpoints.

📖 See the complete Tools Overview for detailed documentation

✂️ Partial page editing

The BookStack API has no PATCH for page content — PUT /api/pages/{id} takes a complete html or markdown body. Changing one paragraph of a long page therefore meant reading all of it, having the model reproduce it verbatim with the change applied, and sending it all back: the content crosses the model twice, and the whole page rides on it being copied byte-perfectly.

Three tools run that read-modify-write cycle inside the server instead, so a caller sends only the fragment it wants changed:

Tool What it does
bookstack_pages_outline Heading structure with offsets and section sizes. No content transferred.
bookstack_pages_read with grep Matches from the stored source, each with an exact context slice (no ellipses) — paste it straight into old_string.
bookstack_pages_edit Literal find-and-replace. old_string must match exactly and be unique unless replace_all is set.
bookstack_pages_append Insert at the end of the page, the end of a named section (after its subsections), or right after a section heading. Reports the heading used as section_matched.
// 1. What sections exist, and how big are they?
{ "tool": "bookstack_pages_outline", "arguments": { "id": 12 } }

// 2. Get an exact anchor without loading the page
{ "tool": "bookstack_pages_read",
  "arguments": { "id": 12, "grep": "retention period", "context": 300 } }

// 3. Rehearse: nothing is written
{ "tool": "bookstack_pages_edit",
  "arguments": { "id": 12, "dry_run": true,
    "edits": [{ "old_string": "retention period of 6 months",
                "new_string": "retention period of 24 months" }] } }

// 4. Apply with a stale-page preflight
{ "tool": "bookstack_pages_edit",
  "arguments": { "id": 12, "expected_updated_at": "2026-08-17T09:12:44.000000Z",
    "edits": [{ "old_string": "retention period of 6 months",
                "new_string": "retention period of 24 months" }] } }

Guards. An ambiguous anchor is refused rather than applied to the wrong place, and the error carries the first few matches with context. A missing anchor reports the same text found with different whitespace, which is the usual near-miss. A result smaller than half the original is refused unless allow_shrink is set. After a write the page is re-read and the change is looked for in the raw stored source, then in normalised text — BookStack rewrites stored HTML on save (heading anchors, injected id attributes), so a byte comparison alone would call every success a failure. If that re-read fails, the result is verified: null: the write landed, so do not retry it. Every write creates a BookStack revision, so an applied edit can be rolled back in the UI. No response from these tools contains page content. expected_updated_at detects a page changed before this server reads it; BookStack's page API does not provide an atomic version condition, so it cannot prevent a write that races after that check.

Two invariants, if you touch this code (src/utils/page-content.ts): markdown pages are patched and written through markdown, because writing html to one switches the page's editor type; every other page is patched against raw_html, never the rendered html — patching the rendered output would write back expanded page-include tags and destroy the includes permanently.

bookstack_pages_update is unchanged and still replaces the whole content field; these tools are additive. bookstack_pages_read called without any of the new options returns exactly what it always did.

📚 Documentation

Find comprehensive guides in the docs/ folder:

⚡ Quick Examples

List all books:

bookstack_books_list({ count: 10, sort: "updated_at" })

Create a new page:

bookstack_pages_create({
  name: "Getting Started",
  book_id: 1,
  markdown: "# Welcome\nYour content here..."
})

Search for content:

bookstack_search({ query: "API documentation", count: 20 })

🛠️ Development

This project is Bun-native — Bun runs the TypeScript source directly, so there is no compile step.

git clone <repository-url>
cd bookstack-mcp-server
bun install
bun run dev          # hot reload; equivalent to: bun --watch src/server.ts
bun run src/server.ts   # start the server
bun test                # run tests
bun run typecheck       # tsc --noEmit
bun run lint            # biome check .

🔧 See the Setup Guide for development, Docker, and production deployment

🐳 Local testing with Docker Compose

The included docker-compose.yml spins up a full local stack — MariaDB, a real BookStack instance, and this MCP server (built from the Bun Dockerfile).

  1. Start the backing services:

    docker compose up -d db bookstack
  2. Wait for BookStack to finish first-boot migrations, then open http://localhost:6875. Default linuxserver credentials:

    • Email: admin@admin.com
    • Password: password
  3. Create an API token in the UI (Edit Profile → API Tokens → Create Token). Combine the Token ID and Token Secret as token_id:token_secret and put it in a .env file next to docker-compose.yml, together with an inbound secret of your own:

    echo "BOOKSTACK_API_TOKEN=token_id:token_secret" > .env
    echo "MCP_AUTH_TOKEN=$(openssl rand -hex 32)" >> .env

    The token can only be created after BookStack is running, so it cannot be baked into the image — this manual step is required once.

    Both entries are required. docker-compose.yml passes MCP_AUTH_TOKEN through to the mcp service, and the HTTP transport refuses to start without it, so a .env carrying only BOOKSTACK_API_TOKEN leaves the container in a restart loop.

  4. Start the MCP server (it reads both tokens from .env):

    docker compose up -d mcp
  5. Check health — returns 200 with {"status":"healthy"} once the server can reach BookStack with your token:

    curl http://localhost:3000/health

Until a valid BOOKSTACK_API_TOKEN is supplied the mcp container reports unhealthy, because /health verifies live connectivity to BookStack. If either token is missing entirely the container does not get that far and restart-loops instead of answering 503 — check docker compose logs mcp for Configuration validation failed (no BOOKSTACK_API_TOKEN) or MCP_AUTH_TOKEN is not set (no inbound secret).

The compose file pins BookStack to lscr.io/linuxserver/bookstack:version-v26.05.2 — the release this repo's tool contract was verified against — and ships a throwaway dev APP_KEY. Generate your own for anything beyond local testing, using the same pinned tag:

docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:version-v26.05.2 appkey

It prints one base64:… line to paste into APP_KEY. The --entrypoint override is required: without it the image runs its normal init first, which halts with The application key is missing, halting init! — the very key you are trying to generate — and never reaches the appkey script.

📝 License

MIT License - see LICENSE file for details.

🌟 Community

This project is part of the BookStack ecosystem! Check out other API-based tools and scripts in the BookStack API Scripts repository.

🆘 Support


Built with ❤️ for the BookStack community

About

A Model Context Protocol (MCP) server providing full access to BookStack's knowledge management capabilities

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages