Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 67 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BookStack MCP Server

Connect BookStack to Claude and other AI assistants through the Model Context Protocol (MCP). This server exposes 56 tools and 11 resources covering the supported subset of the BookStack API — books, pages, chapters, shelves, search, users, roles, permissions, attachments, images, the recycle bin, the audit log and system info.
Connect BookStack to Claude and other AI assistants through the Model Context Protocol (MCP). This server exposes 59 tools and 11 resources covering the supported subset of the BookStack API — books, pages, chapters, shelves, search, users, roles, permissions, attachments, images, the recycle bin, the audit log and system info.

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

Expand All @@ -13,7 +13,7 @@ This server supports two transport modes: **Streamable HTTP** (default) and **St
## ✨ What You Get

- **BookStack Integration** - Access your books, pages, chapters, and content
- **56 MCP Tools & 11 Resources** - CRUD, search and export across the supported endpoint families
- **59 MCP Tools & 11 Resources** - 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
Expand Down Expand Up @@ -44,7 +44,7 @@ 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 56 tools with the
what callers must present to `POST /message`, which dispatches all 59 tools with the
authority of the BookStack account behind `BOOKSTACK_API_TOKEN`. Skip `MCP_AUTH_TOKEN`
only for [stdio](#-transports), which has no network surface and ignores it.

Expand Down Expand Up @@ -171,7 +171,7 @@ with the failing check named:
"status": "unhealthy",
"checks": [
{ "name": "bookstack_connection", "healthy": false, "message": "BookStack API connection" },
{ "name": "tools_loaded", "healthy": true, "message": "56 tools loaded" },
{ "name": "tools_loaded", "healthy": true, "message": "59 tools loaded" },
{ "name": "resources_loaded", "healthy": true, "message": "11 resources loaded" }
]
}
Expand Down Expand Up @@ -247,10 +247,10 @@ MCP clients pipe over stdin/stdout. For stdio you also don't need `-p 3000:3000`

## 🛠️ Available Tools

**56 tools across 13 categories:**
**59 tools across 13 categories:**

- **📚 Books** (6) - Create, read, update, delete, and export books
- **📄 Pages** (6) - Manage pages with HTML/Markdown content
- **📄 Pages** (9) - Manage pages with HTML/Markdown content, including [partial editing](#-partial-page-editing)
- **📑 Chapters** (6) - Organize pages within books
- **📚 Shelves** (5) - Group books into collections
- **🔍 Search** (1) - Search across content types
Expand All @@ -268,6 +268,67 @@ Not exposed (no tools): comments, imports, tag-name listings, the image-gallery

> 📖 See the complete [Tools Overview](docs/tools-overview.md) 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` | Matching excerpts from the **stored** source — paste one 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, or right after a section heading. |

```jsonc
// 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 normalised text — BookStack rewrites stored HTML on save (heading
anchors, injected `id` attributes), so a byte comparison would call every success a failure.
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:
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ services:
start_period: 30s

bookstack:
# Pinned, not :latest. The tool contract in this repo (56 tools, field shapes,
# Pinned, not :latest. The tool contract in this repo (59 tools, field shapes,
# error codes) was verified against BookStack 26.05.2; a floating tag silently
# re-points at a future release and turns a BookStack change into a mystery
# failure in this suite. Currently resolves to v26.05.2-ls274.
Expand Down Expand Up @@ -80,7 +80,7 @@ services:
DB_USERNAME: bookstack
DB_PASSWORD: bookstack_secret
# BookStack throttles its REST API per user (default 180/min). The
# integration suites drive all 56 tools and share one admin token, so the
# integration suites drive all 59 tools and share one admin token, so the
# default becomes the bottleneck. This is a throwaway test instance, so
# raise it. NOTE: real deployments keep the 180 default — that is why the
# client retries on 429 and the test harness rides out throttling.
Expand All @@ -105,7 +105,7 @@ services:
BOOKSTACK_API_TOKEN: ${BOOKSTACK_API_TOKEN:-}
# INBOUND authentication for POST /message - unrelated to the token above,
# and required: the container refuses to start without it, because
# /message dispatches all 56 tools (permanent-delete, users, roles,
# /message dispatches all 59 tools (permanent-delete, users, roles,
# permissions) using BOOKSTACK_API_TOKEN. Generate with `openssl rand -hex 32`.
MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN:-}
SERVER_PORT: "3000"
Expand Down
88 changes: 83 additions & 5 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The BookStack MCP Server provides comprehensive access to the BookStack knowledg

### Key Features

- **API Coverage**: 56 tools and 11 resources across 13 categories, covering the supported subset of the BookStack API (comments, imports, tag listings, image-gallery `data` endpoints and ZIP export are not exposed)
- **API Coverage**: 59 tools and 11 resources across 13 categories, covering the supported subset of the BookStack API (comments, imports, tag listings, image-gallery `data` endpoints and ZIP export are not exposed)
- **Type Safety**: Full TypeScript interfaces for all operations
- **Robust Error Handling**: Comprehensive error mapping and recovery guidance
- **Rate Limiting**: Token bucket algorithm with configurable limits
Expand Down Expand Up @@ -325,9 +325,22 @@ interface CreatePageParams {
```typescript
// Tool: bookstack_pages_read
interface PageWithContent extends Page {
html: string; // Rendered HTML content
raw_html: string; // Raw HTML as stored
markdown?: string; // Markdown source if available
html: string; // Rendered HTML content, page includes resolved
raw_html: string; // Raw HTML as stored - this is what the edit tools patch
markdown?: string; // Markdown source; empty string for HTML-authored pages
}

// With any option below set, the response is a narrowed summary instead of the
// full page object. With none set, the complete page object comes back unchanged.
interface ReadPageOptions {
id: number; // Required
grep?: string; // Literal phrase in the STORED source (1-1000 chars); returns excerpts only
case_sensitive?: boolean; // Default false
context?: number; // Context characters per match, 1-2000, default 200
max_matches?: number; // Excerpts returned, 1-50, default 10 (total is still reported)
offset?: number; // Start of a character window into the stored source
length?: number; // Length of that window
metadata_only?: boolean; // Metadata and total_chars only, no content
}
```

Expand All @@ -336,6 +349,71 @@ interface PageWithContent extends Page {
// Tool: bookstack_pages_update
// Same as CreatePageParams but all fields optional except id
// Can move pages between books/chapters by changing book_id/chapter_id
// NOTE: replaces the ENTIRE content field. For a partial change use
// bookstack_pages_edit or bookstack_pages_append below.
```

#### Edit Page (partial update)
```typescript
// Tool: bookstack_pages_edit
//
// The BookStack API has no PATCH for page content, so the server performs the
// read-modify-write cycle itself: it reads the page, applies the edits to its
// STORED source (raw_html, or markdown for markdown pages) and writes the result
// back. The caller sends only the fragment it wants changed.
interface EditPageParams {
id: number; // Required
edits: Array<{ // Required, at least one; applied in order
old_string: string; // Exact text, whitespace included; must be unique
new_string: string; // Replacement; '' deletes the anchored text
replace_all?: boolean; // Default false; opt-in for a repeated anchor
}>;
dry_run?: boolean; // Report what would change, write nothing
expected_updated_at?: string; // Best-effort stale-page preflight; not an atomic lock
allow_shrink?: boolean; // Permit a result under half the original size
}

// The response carries no page content:
// {
// page_id, name, slug, book_id, chapter_id, updated_at, revision_count,
// editor, field, chars_before, chars_after, delta,
// edits: [{ index, occurrences_replaced, context }],
// written, verified, unverified_fragment_count, chars_stored
// }
```

An anchor that cannot be applied comes back as `InvalidParams` with actionable
detail rather than a bare failure: `found_with_different_whitespace` when the text
exists but the whitespace differs, `first_occurrences` when it matched more than
once, `chars_before`/`chars_after` when the shrink guard fired. A page that changed
before this server read it comes back as `InvalidRequest` with
`type: 'concurrent_modification'`.

#### Append to Page
```typescript
// Tool: bookstack_pages_append
interface AppendPageParams {
id: number; // Required
content: string; // Required, in the page's own format
position?: 'start' | 'end'; // Default 'end'; 'start' means after the heading
section?: string; // Heading text; omit to target the whole page
separator?: string; // Default '\n\n' for markdown, '\n' otherwise
dry_run?: boolean;
expected_updated_at?: string;
}
```

An unknown `section` fails with `available_sections` listing the real heading names.

#### Outline Page
```typescript
// Tool: bookstack_pages_outline
// Heading structure only - no content is transferred.
// {
// page_id, name, slug, book_id, chapter_id, updated_at, revision_count,
// editor, field, total_chars, heading_count,
// headings: [{ level, text, offset, length }] // length = section size
// }
```

#### Delete Page
Expand Down Expand Up @@ -1230,4 +1308,4 @@ for (const book of books.data) {
- Validation is **strict by default** (`VALIDATION_STRICT_MODE=true`): invalid params are rejected at the boundary rather than forwarded to BookStack. Set it to `false` to log a warning and forward them instead
- Pagination `count` is capped at 500 (100 for `bookstack_search`); above the cap the call is **rejected, not clamped**

For additional help, use the `bookstack_help` tool or consult the error guides with `bookstack_error_guides`.
For additional help, use the `bookstack_help` tool or consult the error guides with `bookstack_error_guides`.
2 changes: 1 addition & 1 deletion docs/examples-and-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ curl http://localhost:3000/health
"status": "healthy",
"checks": [
{ "name": "bookstack_connection", "healthy": true, "message": "BookStack API connection" },
{ "name": "tools_loaded", "healthy": true, "message": "56 tools loaded" },
{ "name": "tools_loaded", "healthy": true, "message": "59 tools loaded" },
{ "name": "resources_loaded", "healthy": true, "message": "11 resources loaded" }
]
}
Expand Down
2 changes: 1 addition & 1 deletion docs/integration-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ three services; `mcp` is not needed to run these tests.
| `mcp` | built from `./Dockerfile` | Our MCP server. **Not required** by the integration suite. |

The BookStack tag is **pinned, not `:latest`**. The whole tool contract in this repo —
56 tools, field shapes, error codes — was verified against v26.05.2, and a floating tag
59 tools, field shapes, error codes — was verified against v26.05.2, and a floating tag
would silently re-point at a future release, turning an upstream change into a mystery
failure here. Bump it deliberately in `docker-compose.yml`, then re-run the suite.

Expand Down
4 changes: 2 additions & 2 deletions docs/setup-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
The BookStack MCP Server provides comprehensive access to BookStack's knowledge management capabilities through the Model Context Protocol (MCP). This guide covers everything you need to set up and configure the server for optimal performance.

### Key Features
- **56 MCP Tools** across 13 categories, covering the supported subset of the BookStack API
- **59 MCP Tools** across 13 categories, covering the supported subset of the BookStack API
- **11 Resources** for dynamic content retrieval
- **Rate Limiting** with configurable limits
- **Comprehensive Validation** using Zod schemas
Expand Down Expand Up @@ -678,7 +678,7 @@ curl -i http://localhost:3000/health
"status": "healthy",
"checks": [
{"name": "bookstack_connection", "healthy": true, "message": "BookStack API connection"},
{"name": "tools_loaded", "healthy": true, "message": "56 tools loaded"},
{"name": "tools_loaded", "healthy": true, "message": "59 tools loaded"},
{"name": "resources_loaded", "healthy": true, "message": "11 resources loaded"}
]
}
Expand Down
Loading