BookStack MCP Server
An MCP (Model Context Protocol) server that gives Claude full access to a BookStack instance. Built in Rust with tokio/axum as a Cargo workspace with pluggable database backends and optional semantic vector search.
Features
- Full CRUD on all core BookStack resources (shelves, books, chapters, pages, attachments)
- Full-text search with BookStack query operators
- Semantic vector search — natural language search across all content via embeddings (optional). Two modes on
semantic_search:standard(default, vector + keyword + Markov-blanket blend) andprecision(4-stage cascade — semantic → keyword → Markov-blanket → cross-encoder). An optionalrerank: boolflag layers a cross-encoder pass on top of the standard top-N (v0.13.0; replaces the pre-v0.13.0mode: "rerank"). Per-page access control enforced via BookStack's API on every result. - Settings UI (
/settings) — browser-based admin configuration page (token-gated via the same/authorizeflow). Surfaces only the global server fields the index worker needs (hive_shelf_id,user_journals_shelf_id). - Pluggable database — SQLite for simple deployments, PostgreSQL + pgvector for production
- Separate embedder — background embedding service with pluggable backends (local ONNX, Ollama, OpenAI, Voyage)
- Cross-encoder reranker (optional) — embedder exposes
POST /rerankwhenBSMCP_RERANK_PROVIDERis configured. Three providers:local(in-process ONNX cross-encoder via fastembed, defaultBAAI/bge-reranker-v2-m3),voyage(Voyage's/v1/rerank),openai(any OpenAI-shape rerank endpoint — covers Voyage/Jina/Cohere-via-shim/self-hosted). Off by default; consumed bysemantic_search'srerank: trueflag (refinement on the standard mode) +mode: "precision"(cascade), and bysearch_content'srerank: trueflag (v0.13.0). - Server-side markdown to HTML conversion — send markdown, server converts before sending to BookStack
- Staging upload flow — upload local images and attachments through a two-step staging endpoint without exposing local paths to the container (see below)
- OAuth 2.1 support — use as a Claude.ai or Claude Desktop custom connector without config files
- Encrypted token storage — OAuth tokens encrypted at rest with AES-256-GCM
- Dual transport — SSE (MCP 2024-11-05) and Streamable HTTP (MCP 2025-03-26)
- Dynamic structure discovery — AI automatically learns your BookStack hierarchy on connect
- Auto-migration — seamlessly migrate from SQLite to PostgreSQL on startup
- Multi-user support via per-session BookStack API tokens
- Multi-arch Docker images (amd64 + arm64)
Architecture
crates/
bsmcp-common/ Shared types, traits, config, chunking, vector utils
bsmcp-db-sqlite/ SQLite backend (rusqlite, bundled)
bsmcp-db-postgres/ PostgreSQL + pgvector backend (sqlx)
bsmcp-server/ MCP server binary (axum, no ONNX dependency)
bsmcp-embedder/ Embedder + reconciliation worker (single binary, role-selected via --role flag)
— local ONNX / Ollama / OpenAI / Voyage embedding, job queue worker, HTTP /embed + optional /rerank
— reconciliation worker: initial walk + webhook/cron delta walk on the index_jobs queue
docker/
Dockerfile.server Lightweight server image (~35MB)
Dockerfile.embedder Embedder + worker image with ONNX Runtime (~45MB)
docker-compose.yml PostgreSQL deployment (production)
docker-compose.sqlite.yml SQLite deployment (simple)
The MCP server handles all client-facing protocol, OAuth, and search. The embedder runs separately, polling a database-backed job queue to embed pages and serving a /embed HTTP endpoint for query-time embedding (and /rerank when a reranker provider is configured). The embedder supports four embedding backends: local ONNX models (fastembed), Ollama, OpenAI-compatible APIs, and Voyage. The reconciliation worker (same binary, run with --role=worker) owns the index_jobs queue — runs the initial full walk on cold start, then consumes webhook + cron jobs and the periodic delta walk. Run as two compose services (separate embedder + worker) or as one with --role=both.
Available Tools (59 BookStack + 3 semantic = 62)
| Category | Tools |
|---|---|
| Search | search_content |
| Semantic | semantic_search, reembed, embedding_status |
| Shelves | list_shelves, get_shelf, create_shelf, update_shelf, delete_shelf |
| Books | list_books, get_book, create_book, update_book, delete_book |
| Chapters | list_chapters, get_chapter, create_chapter, update_chapter, delete_chapter |
| Pages | list_pages, get_page, create_page, update_page, delete_page, edit_page, append_to_page, replace_section, insert_after |
| Move | move_page, move_chapter, move_book_to_shelf |
| Attachments | list_attachments, get_attachment, create_attachment, update_attachment, delete_attachment, upload_attachment |
| Staging | prepare_upload (used with upload_image / upload_attachment for local file uploads) |
| Exports | export_page, export_chapter, export_book (markdown, plaintext, html) |
| Comments | list_comments, get_comment, create_comment, update_comment, delete_comment |
| Recycle Bin | list_recycle_bin, restore_recycle_bin_item, destroy_recycle_bin_item |
| Users | list_users, get_user |
| Audit Log | list_audit_log |
| System | get_system_info |
| Images | list_images, get_image, upload_image, update_image, delete_image |
| Permissions | get_content_permissions, update_content_permissions |
| Roles | list_roles, get_role |
Semantic tools (semantic_search, reembed, embedding_status) only appear when BSMCP_SEMANTIC_SEARCH=true and an embedder is running. Without semantic search: 59 BookStack tools.
The server is a thin BookStack CRUD facade plus semantic-search enrichment, OAuth, audit, and the reconciliation worker. Personal-memory primitives (journals, identities, reminders) and the v0.8.0/v0.9.0 briefing surface were removed in v0.10.0; v0.11.0 added the optional cross-encoder reranker on the embedder side; v0.13.0 (current) refactors that reranker from a third semantic_search mode into a flag on both semantic_search and search_content (breaking — see the v0.12 → v0.13 migration below). See the migration notes below.
Setup
Prerequisites
- A BookStack instance with API access enabled
- A BookStack API token (created in your BookStack user profile under "API Tokens")
- Docker and Docker Compose (for container deployment)
Quick Start (PostgreSQL — recommended)
cp .env.example .env
# Edit .env with your BookStack URL, encryption key, and database password
docker compose -f docker/docker-compose.yml up -d
This starts four containers:
- bsmcp-postgres — PostgreSQL 17 with pgvector extension
- bsmcp-server — MCP server (port 8080)
- bsmcp-embedder — Background embedding service (
--role=embedder, default); also serves/rerankwhen a reranker is configured - bsmcp-worker — Reconciliation worker (same image as the embedder, started with
--role=worker): initial walk on cold start, webhook + cron job consumption, periodic delta walk
Quick Start (SQLite — simple)
cp .env.example .env
# Edit .env with your BookStack URL and encryption key
docker compose -f docker/docker-compose.sqlite.yml up -d
This starts three containers (server + embedder + worker) sharing a SQLite database file.
Run from source
The project distributes as multi-arch (linux/amd64 + linux/arm64) container images on GHCR — ghcr.io/bees-roadhouse/bsmcp-server and ghcr.io/bees-roadhouse/bsmcp-embedder. Native binaries for bsmcp-server only are attached to each GitHub Release for linux-x86_64, linux-aarch64, darwin-x86_64, darwin-aarch64, and windows-x86_64. The embedder is not distributed as a bare binary — it depends on ONNX Runtime (a per-platform C++ shared library), so running it outside Docker is awkward. Either run the published embedder container, or build from source:
# Server
cargo run --release -p bsmcp-server
# Embedder (separate terminal)
cargo run --release -p bsmcp-embedder
The server is pure Rust + bundled SQLite and builds cleanly on any target the Rust toolchain supports. The embedder depends on fastembed, which links ONNX Runtime; the crate downloads a matching prebuilt at build time for common targets, but cross-compiling or running on uncommon platforms may require installing ONNX Runtime separately. For most users, running the embedder from the published container avoids that complexity entirely.
Configuration
Server Variables
| Variable | Required | Default | Description |
|---|---|---|---|
BSMCP_BOOKSTACK_URL |
Yes | - | Your BookStack instance URL |
BSMCP_ENCRYPTION_KEY |
Yes | - | 32+ char key for AES-256-GCM token encryption |
BSMCP_DB_BACKEND |
No | sqlite |
Database backend: sqlite or postgres |
BSMCP_DATABASE_URL |
If postgres | - | PostgreSQL connection string |
BSMCP_DB_PATH |
No | /data/bookstack-mcp.db |
SQLite database path |
BSMCP_PUBLIC_DOMAIN |
No | - | Public domain for OAuth redirects (e.g. mcp.example.com) |
BSMCP_INTERNAL_DOMAIN |
No | - | Internal/Docker-network domain |
BSMCP_HOST |
No | 0.0.0.0 |
Bind address |
BSMCP_PORT |
No | 8080 |
Bind port |
BSMCP_INSTANCE_NAME |
No | - | Instance name shown to AI |
BSMCP_INSTANCE_DESC |
No | - | Instance description shown to AI |
BSMCP_SEMANTIC_SEARCH |
No | false |
Enable semantic search tools |
BSMCP_EMBEDDER_URL |
No | http://bsmcp-embedder:8081 |
Embedder HTTP endpoint |
BSMCP_WEBHOOK_SECRET |
If semantic | - | BookStack webhook secret |
BSMCP_ACCESS_TOKEN_TTL |
No | 86400 |
Access token TTL in seconds (24h) |
BSMCP_REFRESH_TOKEN_TTL |
No | 7776000 |
Refresh token TTL in seconds (90d) |
BSMCP_BACKUP_INTERVAL |
No | - | Hours between backups (0 = disabled) |
BSMCP_BACKUP_PATH |
No | /data/backups |
Backup directory |
BSMCP_BOOKSTACK_RATE_LIMIT_PER_MIN |
No | 180 |
Per-process BookStack API request cap. Lower if multiple processes share a token and you see 429s. |
Embedder Variables
| Variable | Required | Default | Description |
|---|---|---|---|
BSMCP_EMBED_TOKEN_ID |
Yes | - | BookStack API token ID for crawling |
BSMCP_EMBED_TOKEN_SECRET |
Yes | - | BookStack API token secret |
BSMCP_EMBED_PROVIDER |
No | local |
Embedding backend: local (fastembed ONNX), ollama, openai (or OpenAI-compatible), voyage. See Embedding Providers for per-provider config. |
BSMCP_EMBED_MODEL |
No | (per provider) | Model name (see Embedding Providers) |
BSMCP_EMBED_API_KEY |
If openai | - | API key for OpenAI embedding provider |
BSMCP_EMBED_API_URL |
No | (per provider) | Base URL for Ollama or OpenAI-compatible endpoint |
BSMCP_EMBED_DIMS |
No | (auto) | Embedding dimensions (auto-detected for Ollama) |
BSMCP_MODEL_PATH |
No | /data/models |
ONNX model cache directory (local provider only) |
BSMCP_EMBED_CPUS |
No | 0 (unlimited) |
Docker CPU limit for embedder |
BSMCP_EMBED_JOB_TIMEOUT |
No | 14400 |
Seconds before stuck jobs reset |
BSMCP_EMBED_BATCH_SIZE |
No | 32 |
Chunks per embedding batch |
BSMCP_EMBED_DELAY_MS |
No | 50 |
Delay between pages (API throttle) |
BSMCP_EMBED_POLL_INTERVAL |
No | 5 |
Seconds between job queue polls |
BSMCP_EMBED_ON_STARTUP |
No | false |
true = auto-embed on startup, clean = clear all embeddings first |
BSMCP_EMBED_FAILURE_THRESHOLD |
No | 10 |
Failed pages before a job is marked failed |
BSMCP_EMBED_CONSECUTIVE_ABORT |
No | 10 |
Consecutive failures before a job aborts early |
BSMCP_EMBED_HOST |
No | 0.0.0.0 |
Embedder listen address |
BSMCP_EMBED_PORT |
No | 8081 |
Embedder listen port |
BSMCP_RERANK_PROVIDER |
No | (unset) | Cross-encoder rerank provider: local, voyage, openai, none. Off by default; enables POST /rerank on the embedder, which the server consumes for semantic_search/search_content rerank: true and semantic_search mode: "precision". See Reranker Providers. |
BSMCP_RERANK_MODEL |
If reranker on | (per provider) | Reranker model. Defaults: BAAI/bge-reranker-v2-m3 (local), rerank-2 (voyage). Required for openai. |
BSMCP_RERANK_API_KEY |
If voyage/openai | - | API key for external rerank provider. |
BSMCP_RERANK_API_URL |
If openai | (per provider) | Base URL. Voyage defaults to https://api.voyageai.com; openai requires explicit URL. |
Role Selector
The embedder image hosts both the embedder loop and the reconciliation worker loop. Select which runs per container via --role (CLI flag, primary) or BSMCP_ROLE (env fallback). Default is embedder.
| Variable / flag | Default | Description |
|---|---|---|
--role=embedder / BSMCP_ROLE=embedder |
(default) | Runs the embed job queue, the /embed + /rerank HTTP endpoints, and the cross-encoder when configured. Does NOT spawn the reconciliation worker — operators running embedder-only get no automatic index retry-chain reconciliation. |
--role=worker / BSMCP_ROLE=worker |
- | Runs the reconciliation worker only: initial full walk, periodic delta walk, lifecycle housekeeper across index_jobs + embed_jobs. No HTTP listener, no ONNX model loaded. |
--role=both / BSMCP_ROLE=both |
- | Runs both loops in one process. Useful for single-host SQLite deployments. |
The CLI flag wins over the env. Setting both (compose command: + BSMCP_ROLE) is belt-and-suspenders — recommended for clarity in compose files.
Worker Variables
Read when the embedder is running with --role=worker or --role=both. The worker shares the same database as the server and owns the index_jobs queue.
| Variable | Required | Default | Description |
|---|---|---|---|
BSMCP_BOOKSTACK_URL |
Yes | - | Same as server |
BSMCP_ENCRYPTION_KEY |
Yes | - | Must match the server's (the DB layer initializes its encryption context on every connection) |
BSMCP_INDEX_TOKEN_ID |
Yes* | - | Admin BookStack API token ID for the worker. Falls back to BSMCP_EMBED_TOKEN_ID if unset, so single-token deployments don't have to duplicate creds. |
BSMCP_INDEX_TOKEN_SECRET |
Yes* | - | Admin BookStack API token secret. Falls back to BSMCP_EMBED_TOKEN_SECRET. |
BSMCP_DB_BACKEND |
No | sqlite |
Must match the server's (shared DB) |
BSMCP_DB_PATH |
No | /data/bookstack-mcp.db |
SQLite path |
BSMCP_DATABASE_URL |
If postgres | - | PostgreSQL connection string |
BSMCP_INDEX_DELTA_INTERVAL_SECONDS |
No | 300 |
Cadence of the periodic delta walk. 0 = disable the delta walk (webhook-driven only). |
* Required, but the fallback to BSMCP_EMBED_TOKEN_* covers most setups.
Job Lifecycle Variables
Read by the worker role's lifecycle housekeeper. Apply to both embed_jobs and index_jobs.
| Variable | Required | Default | Description |
|---|---|---|---|
BSMCP_JOB_TIMEOUT_SECS |
No | 3600 |
Hard timeout — any job running longer than this is failed. |
BSMCP_JOB_RECONCILE_SECS |
No | 300 |
Reconciler poll interval (scan for failed jobs and retry them). |
BSMCP_JOB_MAX_RETRY_CHAIN |
No | 5 |
Max retry-chain length before a job is given up on. |
BSMCP_JOB_CLOSE_GRACE_SECS |
No | 30 |
Audit-grace before succeeded/cancelled jobs are archived to closed status. |
See .env.example for the full list with comments.
Semantic Search Setup
- Set
BSMCP_SEMANTIC_SEARCH=truein your server env - Set
BSMCP_WEBHOOK_SECRETto a random string (16+ characters) - Create a BookStack API token with read access for the embedder (
BSMCP_EMBED_TOKEN_ID/BSMCP_EMBED_TOKEN_SECRET) - Start the embedder container — it downloads the ONNX model (~1.3GB) on first run
- Use the
reembedtool (via Claude) to trigger initial embedding of all pages - Configure a webhook in BookStack for automatic re-embedding on page changes:
BookStack Webhook Configuration
Go to Settings > Webhooks > Create Webhook in your BookStack instance:
| Field | Value |
|---|---|
| Name | MCP Semantic Search |
| Endpoint URL | https://your-mcp-host/webhooks/bookstack |
| Active | Yes |
Events to select:
- Page Create
- Page Update
- Page Delete
Custom header (required for verification):
X-Webhook-Secret: YOUR_WEBHOOK_SECRET
The YOUR_WEBHOOK_SECRET value must match BSMCP_WEBHOOK_SECRET in your server environment. The server uses constant-time comparison to verify the header.
After saving, any page create/update/delete in BookStack automatically queues a re-embedding job. The embedder picks it up within seconds (configurable via BSMCP_EMBED_POLL_INTERVAL).
Reranker Setup (optional — for the rerank: true flag and mode: "precision")
The cross-encoder reranker is off by default. semantic_search (default mode: "standard", rerank: false) and search_content (default rerank: false) both work fine without it. To enable the rerank: true flag on either tool, or to use mode: "precision" on semantic_search:
- Pick a provider and set
BSMCP_RERANK_PROVIDERon the embedder to one of:local— in-process ONNX cross-encoder via fastembed. No API key. Default model:BAAI/bge-reranker-v2-m3(~600 MB, downloads on first run). ReusesBSMCP_MODEL_PATHfor the cache directory.voyage— Voyage AI's/v1/rerank. SetBSMCP_RERANK_API_KEY. Default model:rerank-2.openai— any OpenAI-shape/v1/rerankendpoint (Voyage, Jina, Cohere-via-shim, self-hosted). Requires all ofBSMCP_RERANK_API_KEY,BSMCP_RERANK_API_URL, andBSMCP_RERANK_MODEL(no upstream default — OpenAI itself has not shipped a rerank API).
- Restart the embedder. On startup it logs
Reranker: <provider> <model>and starts answeringPOST /rerank. Without configuration it logsReranker: disabled (BSMCP_RERANK_PROVIDER unset or 'none')and/rerankreturns 503. - Use the reranker from a tool call:
semantic_searchwithmode: "standard", rerank: true— runs the standard pipeline, then the cross-encoder re-orders the top-N. Equivalent to the pre-v0.13.0mode: "rerank".semantic_searchwithmode: "precision"— wider candidate pool, cross-encoder is the ranker of record (4-stage cascade; rerank is always on).search_contentwithrerank: true— BookStack keyword search, then the cross-encoder re-orders the matched results. In all three cases the response includesscoring.rerankper result andstats.{rerank_ms, rerank_provider, rerank_model, candidates_reranked}. If the reranker is disabled, the server surfaces a clear error pointing atBSMCP_RERANK_PROVIDERso callers can drop the flag and retry.
Per-provider config blocks are documented under Reranker Providers.
Connecting
The MCP endpoint URL is:
https://your-host/mcp/sse
Important: Use the full path including
/mcp/sse— not just the base domain.
Claude.ai (Custom Connector)
- Go to Settings > Integrations > Add custom MCP in Claude.ai
- Enter the MCP endpoint URL:
https://your-host/mcp/sse - A login form opens in your browser — enter your BookStack API Token ID and Token Secret
- Once authorized, BookStack tools appear automatically in your conversations
Claude Desktop (Custom Connector)
- Add a custom connector with URL:
https://your-host/mcp/sse - When connecting, a login form opens in your browser with instructions
- Enter your BookStack API Token ID and Token Secret
No config files needed — authentication happens entirely through the browser via OAuth 2.1.
Claude Code (Direct Bearer Token)
Add to your MCP server configuration:
{
"mcpServers": {
"bookstack": {
"url": "https://your-host/mcp/sse",
"headers": {
"Authorization": "Bearer YOUR_TOKEN_ID:YOUR_TOKEN_SECRET"
}
}
}
}
The token ID and secret come from your BookStack API token (created under My Account > Access & Security > API Tokens).
API Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/mcp/sse |
SSE connection (MCP 2024-11-05) |
POST |
/mcp/sse |
Streamable HTTP (MCP 2025-03-26) |
POST |
/mcp/messages/?sessionId=<id> |
Send MCP JSON-RPC messages (SSE transport) |
GET |
/health |
Health check |
POST |
/webhooks/bookstack |
BookStack webhook receiver (semantic search) |
GET |
/status |
Embedding progress page with live progress bar |
GET |
/.well-known/oauth-authorization-server |
OAuth metadata (RFC 8414) |
GET |
/.well-known/oauth-protected-resource |
Protected resource metadata (RFC 9728) |
GET |
/authorize |
Login form for BookStack API token |
POST |
/authorize |
Validate credentials and issue auth code |
POST |
/token |
OAuth token exchange |
POST |
/register |
Dynamic client registration (RFC 7591) |
Upgrading
All schema migrations are automatic on startup (CREATE TABLE IF NOT EXISTS, ALTER TABLE for new columns). No manual SQL is needed.
Heads up. v0.10.0 stripped the briefing layer + per-user settings; v0.11.0 added the optional cross-encoder reranker; v0.12.x was CI/build only; v0.13.0 (current) is breaking on two fronts — folds
bsmcp-workerintobsmcp-embedder(single image, role-selected) AND refactors the reranker surface from a thirdsemantic_searchmode into arerank: boolflag on bothsemantic_searchandsearch_content. Older entries describe functionality that no longer ships and are kept only for upgrade-path archaeology.
From v0.12.x to v0.13.0 (current) — breaking on two fronts
What changed — worker fold (issue #56)
bsmcp-workerandbsmcp-embedderare now one binary, one image. The embedder image (ghcr.io/bees-roadhouse/bsmcp-embedder:0.13.0) runs in either role depending on--role=embedder|worker|both(CLI flag, primary) orBSMCP_ROLE=embedder|worker|both(env var, fallback). Default with no flag isembedder— preserves v0.12.x behavior for unmigrated compose files that hit the embedder service.- Indexer walks every visible shelf on every full walk. Per-call scoping (
shelf_ids/book_ids/chapter_ids/page_ids/scopesonsemantic_search) is the only scope surface — the v0.12.xhive_shelf_id/user_journals_shelf_idconfig + the v0.13.0-RCindexed_shelvesinterim are both gone. Issue #122 supersedes #119/#120: the briefly-shippedGlobalSettings::indexed_shelvesfield (empty = walk all, non-empty = walk subset) conflated indexing-time corpus selection with query-time result scoping. With #80's per-call scope params already shipped, callers narrow at the search call instead. The startup migration dropsindexed_shelves_jsonfrom both backends (DROP COLUMN IF EXISTS— Postgres native, SQLite 3.35+); the v0.12.xhive_shelf_id+user_journals_shelf_idcolumns get the same idempotent drop. The/settingsUI loses its indexed-shelves input. Operators who carved a narrow corpus viaindexed_shelveswill see the indexer expand to walk every visible shelf on next boot; keep search results scoped by passingshelf_ids(or a namedkb_scopesentry) on eachsemantic_searchcall. bsmcp-workerDocker image is now an alias ofbsmcp-embedder. Pulls ofghcr.io/bees-roadhouse/bsmcp-worker:0.13.0resolve to the same manifest. This alias is one-release-only and will be removed in v0.14.0. Update your compose now.- CI matrix drops the
bsmcp-workerentry. Release builds are now two images:bsmcp-server+bsmcp-embedder. /healthcarries a newrolefield ("embedder","worker", or"both") so operators can curl two compose services and verify the role flag actually took effect.
What changed — rerank as flag (issue #115)
rerank: boolflag onsemantic_search. Replaces the v0.11.0–v0.12.xmode: "rerank"value. Setrerank: trueonmode: "standard"(ormode: "default") to get the same cross-encoder-on-top-of-standard behavior the oldmode: "rerank"produced — same candidate pool, same/rerankcall path, samescoring.rerank+stats.rerank_*response shape. Defaultfalse.rerank: boolflag onsearch_content. New: whentrue, the BookStack keyword results are reordered by the cross-encoder, each result picks up ascoring.rerankfield, andstats.{rerank_ms, rerank_provider, rerank_model, candidates_reranked}is added to the response. Same shape assemantic_search's rerank surface — a caller can parse both tools' output with one parser. Defaultfalse.mode: "precision"keeps the cascade behavior unchanged. The cross-encoder is always on in precision mode by definition; passingrerank: trueis a no-op there.
What's removed (breaking)
semantic_search mode: "rerank"is hard-cut — there is no deprecation period. Callers passingmode: "rerank"get a structured error:mode: "rerank" was removed in v0.13.0. Passrerank: truewithmode: "standard"instead — same cross-encoder pass, now a flag.
What's automatic
- No DB schema changes. No env-var renames. Existing
BSMCP_INDEX_TOKEN_*/BSMCP_EMBED_TOKEN_*precedence is preserved (index tokens primary, embed tokens fallback). - Existing semantics (FOR UPDATE SKIP LOCKED, retry chains, supersedence, lifecycle housekeeper) unchanged.
- Callers that never used
mode: "rerank"see no behavior change from the search-surface refactor.
What you must do — compose
In your compose file, find the bsmcp-worker service and change two lines:
bsmcp-worker:
- image: ghcr.io/bees-roadhouse/bsmcp-worker:0.12.x
+ image: ghcr.io/bees-roadhouse/bsmcp-embedder:0.13.0
+ command: ["bsmcp-embedder", "--role=worker"]
environment:
+ BSMCP_ROLE: worker
BSMCP_DB_BACKEND: postgres
If you don't set --role=worker (or BSMCP_ROLE=worker), the container will boot in default embedder mode and contend with your real embedder for jobs. Setting both is belt-and-suspenders.
The bsmcp-server and bsmcp-embedder services only need the version bump to :0.13.0.
If you want to collapse to a single container, set --role=both on the embedder service and delete the worker service entirely.
What you must do — mode: "rerank" callers
- Replace
{"mode": "rerank"}with{"mode": "standard", "rerank": true}(or{"rerank": true}—modedefaults to"standard"/"default"). Same cross-encoder pass, same response shape, just expressed as a flag. The 503 fallback on a missingBSMCP_RERANK_PROVIDERis unchanged.
Strict env hygiene
When the embedder boots with --role=worker, it emits a WARN worker_role_ignoring_embedder_env env=BSMCP_EMBED_PROVIDER line for each embedder-only env it finds set. Surfaces stale config you might have copied from the old worker service block. Not fatal — these envs are silently ignored under role=worker.
Footprint note
The bsmcp-worker role now ships ONNX Runtime (~150 MB on-disk; runtime memory unaffected because the worker role does not load the ONNX model). One-time disk cost per host; the image cache makes subsequent pulls free.
From v0.11.0 to v0.12.x
What's automatic
- No schema, env-var, or API surface changes. Pull new images, restart.
What you must do
- Nothing required. v0.12.1 is the recommended pull because it's the first release with all five native
bsmcp-serverbinaries attached (aarch64-unknown-linux-gnuwas missing from v0.11.0 and v0.12.0 due to a cross-build issue, now fixed).
From v0.10.0 to v0.11.0
What's new
- Cross-encoder reranker on the embedder. New
POST /rerankendpoint whenBSMCP_RERANK_PROVIDERis set on the embedder. Three providers:local(in-process ONNX cross-encoder via fastembed; defaultBAAI/bge-reranker-v2-m3),voyage(Voyage's/v1/rerank),openai(any OpenAI-shape rerank endpoint). Off by default —BSMCP_RERANK_PROVIDER=none(or unset) leaves the endpoint disabled and returns 503. - Three-mode
semantic_search(v0.11.0–v0.12.x — see v0.13.0 notes above for the breaking refactor into a flag). Replaces the prior single-shape behavior withmode: "standard" | "rerank" | "precision", defaulting to"standard":standard— vector + keyword + Markov-blanket blend (the v0.10.0 default behavior).rerank— same candidate pool as standard, but the final top-N is re-ordered by the cross-encoder. Cheap refinement (~10–30 ms for top-10 against a local cross-encoder). Hard-cut in v0.13.0 in favor of thererank: boolflag onmode: "standard".precision— wider initial vector pool (5× limit), no keyword/blanket blend, cross-encoder is the ranker of record. More expensive, can rescue hits the blend would miss.
- Per-result
scoring.rerankandstats.rerank_*in the search response when either rerank-enabled mode fires (mode,hybrid,rerank_ms,rerank_provider,rerank_model,candidates_reranked).
What's automatic
- No schema changes. Drop-in at the database layer.
- Existing
semantic_searchcallers keep working unchanged —modedefaults to"standard"and reproduces v0.10.0 behavior.
What you must do (only to use the new modes)
- Pull new images:
ghcr.io/bees-roadhouse/bsmcp-server:0.11.0+ghcr.io/bees-roadhouse/bsmcp-embedder:0.11.0+ghcr.io/bees-roadhouse/bsmcp-worker:0.11.0. - Set
BSMCP_RERANK_PROVIDER(and the matchingBSMCP_RERANK_MODEL/BSMCP_RERANK_API_KEY/BSMCP_RERANK_API_URL) on the embedder. - Pass
mode: "rerank"ormode: "precision"onsemantic_searchcalls. If the reranker is disabled, the embedder returns 503 and the server surfaces a clear error pointing atBSMCP_RERANK_PROVIDERso callers can drop back tomode: "standard". (As of v0.13.0, themode: "rerank"value is hard-cut in favor of arerank: boolflag — see the v0.12.x → v0.13.0 notes above.)
From v0.9.0 to v0.10.0
What's removed
briefingMCP tool,POST /briefing/v1/readHTTP route, and the auto-injectedmeta.briefingenvelope are all gone. The single-call reconstitution shell from v0.8.0 / v0.9.0 turned out to fan out 5+ parallel BookStack page fetches per request and fail open on stalesystem_prompt_page_idsconfig. Removed wholesale.- Per-user
UserSettingsstruct and theuser_settingstable (both Postgres and SQLite) — every consumer was the briefing path or related setup nudges. No per-user state to persist after the cut. - Per-user role-level ACL filtering on semantic search and
tools/list— depended onUserSettings.bookstack_user_id. Semantic search becomes user-anonymous on the embedder side; per-page access control still runs through BookStack's API on every result. user_role_cachetable — fed only the per-user role-level filter.- Briefing-only
GlobalSettingsfields (org_required_instructions_page_ids,org_ai_usage_policy_page_ids,org_identity_page_id,org_domains,guide_page_id,policies_scope,sops_scope,best_practices_scope,friendly_structure,full_content_in_briefing,strict_setup) and the matching/settingsUI sections. - Instance summary subsystem (Ollama caller + the
Summary: …log lines +BSMCP_LLM_*/BSMCP_SUMMARY_*env vars). session_eventanddismiss_setup_nudgeMCP tools (briefing-only).try_auto_populate_bookstack_user_idin the OAuth flow — no settings row to populate.- v0.7.x
extrasmigration shims.
What survives
- 59 BookStack CRUD tools (
create_*/update_*/delete_*/get_*/list_*+search_content). semantic_search,reembed,embedding_status.bsmcp-embedder+bsmcp-workerimages and the reconciler.- Rate limiter + audit log (#54 infra).
- OAuth 2.1 /
/authorizeflow. /settingsadmin UI for the surviving global server config (hive_shelf_id,user_journals_shelf_id).
Tool count
- With semantic search: 62 tools (59 CRUD + 3 semantic). Without semantic: 59.
What's automatic
user_settings,user_role_cache,remember_audit,token_bindings,sessionstables are dropped on first startup (idempotent).- Briefing-only
global_settingscolumns are dropped viaALTER TABLE DROP COLUMN(Postgres native; SQLite ≥ 3.35). - Existing org-level page-id config is discarded — re-enter via the trimmed
/settingspage if any of the surviving fields apply.
Breaking changes
- Clients calling
briefingor/briefing/v1/readgettool not found/404. - Tool responses no longer carry
meta.briefing. tools/listno longer filters by role.
From v0.8.0 to v0.9.0
What's new
- v1.0.0 rollback. The Phase 2 re-introduction of personal-memory MCP tools (
user,config,directory,identity,journal,migrate,reminders,events,sessions,session_event,dismiss_setup_nudge) is gone. The singlebriefingtool from v0.8.0 stays. The codebase is back to v0.8.0's posture plus the issue #54 rate-limiter / job-lifecycle infrastructure. - DB tables
token_bindingsandsessionsare no longer created on fresh installs. Existing v1.0.0 deployments upgrading to v0.9.0 keep the on-disk tables (inert;DROP TABLEmanually if cleanup matters). UserSettingsshed the per-account-settings + journal-resolver fields added in v1.0.0 (account_label,use_org_identity,journaling_enabled,chosen_ai_identity,setup_complete,tool_overrides,user_journal_book_id,cached_user_email*,cached_first_name*,cached_is_admin*). TheextrasJSON catch-all silently preserves any leftover keys until the briefing's migration handler clears them.GlobalSettings.tool_defaultsandadmin_setup_completedropped — admin-only defaults followed the per-tool toggle infrastructure into the bin./setup/userand/setup/adminbrowser wizards removed. The/settingspage is the only browser-side configuration surface.oauth.rs::ensure_token_bindingreverted to v0.8.0'stry_auto_populate_bookstack_user_idshape. Tokens key theuser_settingsrow directly viatoken_id_hashagain; no binding indirection.
What survives from the v0.8.0 → v1.0.0 era
- Rate limiter + job lifecycle (
bsmcp_common::rate_limit,embed_jobs/index_jobslifecycle columns,/jobs/{embed,index}/{id}/cancelendpoints, the lifecycle housekeeper inbsmcp-worker). Issue #54 work is general infra and is kept verbatim.
What's automatic
- No DB migration ships for v1.0.0 → v0.9.0 downgraders.
CREATE TABLE IF NOT EXISTSwon't re-shape the v1.0.0user_settingsPK (stable_id→token_id_hash). If a clean reset is needed, drop the table manually before first start, or wait for a follow-up one-shot migration. - v0.8.0 → v0.9.0 is a no-op schema-wise.
From v0.7.4 to v0.8.0
What's new
- Personal-memory layer moved to memberberry.ai. All 12
remember_*MCP tools (remember_briefing/remember_journal/remember_collage/remember_shared_collage/remember_user_journal/remember_whoami/remember_user/remember_identity/remember_directory/remember_config/remember_audit/remember_search) no longer ship. ThePOST /remember/v1/{resource}/{action}HTTP namespace is gone. - Single
briefingMCP tool replaces the 12 remember tools. Same response shape as the oldremember_briefing action=read, noactionarg. HTTP form:POST /briefing/v1/read. meta.briefingauto-injection on every MCP tool response — full content on the first call per(token_hash, session_id), sticky bits (time + setup summary) thereafter. Callingbriefingexplicitly resets the session for the next response — useful after the AI's harness compacts the conversation.- Typed setup slots on global settings —
guide_page_id,org_identity_page_id,policies_scope,sops_scope,best_practices_scope, plus org-wide booleansfriendly_structure,full_content_in_briefing,strict_setup. IdempotentALTER TABLE ADD COLUMNmigrations on first startup. - Removed:
default_ai_identity_*global columns (dropped viaALTER TABLE DROP COLUMN),remember_audittable (DROP TABLE IF EXISTS), and most per-user pointer fields fromUserSettings(ai_*_book_id,user_journal_book_id,recent_*_count, etc.). The settings UI shrank ~1,300 lines to match.
What's automatic
- Idempotent
ALTER TABLE ADD COLUMNfor the new global slots. ALTER TABLE DROP COLUMN [IF EXISTS]fordefault_ai_identity_*(Postgres native; SQLite swallows duplicate-drop errors via.ok(), requires SQLite ≥ 3.35).DROP TABLE IF EXISTS remember_auditon startup.user_settingsis a JSON blob — old keys are silently ignored on read and dropped on next save.
From v0.7.3 to v0.7.4
What's new
- Briefing payload trimmed —
*_semantic_matchesentries now cap at 3 chunks of ~100 chars each (kb_semantic_matches: 4 × 150). Truncated chunks carrytruncated: trueand a…suffix. A new top-levelsemantic_matches_hintfield tells consumers to callget_page(page_id)for full content. Briefing responses shrink ~50% in typical use, well under Claude Code's response-size threshold. semantic_searchtool trimmed — same shape, slightly more headroom (5 chunks × ~200 chars). New top-levelhintfield on the tool response.- Shared trim helper — chunk-truncation logic lives in one place (
semantic::trim_match), each caller passes its own budget.
What's automatic
- No env vars, no schema changes, drop-in.
From v0.7.2 to v0.7.3
What's new
meta.timeblock on every/rememberresponse —now_unix,now_utc,now_local,now_human,timezone,timezone_source,timezone_refresh_due. Per-user timezone cached server-side; refresh by passingclient_timezone(IANA name) on anyremember_*call.- Actionable setup errors —
settings_not_configurederrors now carry anerror.fixblock with the exact MCP call to make. - Per-call elapsed timing exposed in
meta.elapsed_ms.
From v0.7.0 to v0.7.2
What's new
- Briefing latency cut — book-scoped semantic-search candidate filtering, parallel KB fetches, batched DB lookups.
remember_briefingis significantly faster on instances with large embedded corpora. kb_semantic_matchesreshape — now an envelope{enabled, reason, detail, results}so consumers can branch onenabledrather than guessing whether an empty list means opt-out vs. zero hits.- Permission ACL filtering for semantic search — set
bookstack_user_idin user settings to enable role-based filtering at the candidate-pool layer (much faster than the per-page HTTP fallback). - Per-user identity auto-provisioning — first call to
remember_user action=readcreates the per-user Identity book + Identity page + journal-agent page if missing, returning what was created inauto_provisioned. - Global org settings — admin-only
org_identity_page_id,org_domains,org_required_instructions_page_ids,org_ai_usage_policy_page_idssh
No comments yet
Be the first to share your take.