# BookStack MCP Server Configuration # Transport # Selects how the server talks to its MCP client. # http (default) - starts the HTTP server on SERVER_PORT # stdio - speaks over stdin/stdout, for local clients like Claude Desktop # Only the exact value "stdio" selects stdio; anything else (or unset) starts HTTP. MCP_TRANSPORT=http # HTTP transport authentication (REQUIRED when MCP_TRANSPORT=http) # The inbound secret a client must present on POST /message: # Authorization: Bearer # This is NOT the same thing as BOOKSTACK_API_TOKEN below, and the two must not be # set to the same value. BOOKSTACK_API_TOKEN is the *outbound* credential this # server spends; MCP_AUTH_TOKEN decides *who may make it spend it*. POST /message # dispatches all 56 tools - including permanent-delete, user, role and permission # operations - so any peer that can reach the port and knows this secret has the # authority of the BookStack account behind BOOKSTACK_API_TOKEN. # Unset => the HTTP transport refuses to start (fail closed). There is no # "no auth required" mode. The stdio transport ignores this setting: its client is # already inside this process's trust domain. # Generate one with: openssl rand -hex 32 MCP_AUTH_TOKEN= # Maximum accepted POST /message request body, in bytes. # Default: 73400320 (70 MiB). Sized from the largest inline upload the image and # attachment tools advertise (50000 KB), which the parser sees base64-encoded and # wrapped in JSON: 50000 KB -> 51,200,000 bytes -> 68,266,668 base64 bytes, plus # envelope, rounded up to 70 MiB. Express's own default is ~100 KB, which caps real # uploads at roughly 75 KB and answers anything larger with 413 before MCP dispatch. # Lower it if this server is exposed to callers you would rather not let allocate # 70 MiB per request; prefer `file_path` + BOOKSTACK_UPLOAD_ROOT for large uploads. # HTTP_BODY_LIMIT=73400320 # BookStack API Configuration BOOKSTACK_BASE_URL=http://localhost:8080/api BOOKSTACK_API_TOKEN=your-api-token-here BOOKSTACK_TIMEOUT=30000 # Uploads (optional) # The image/attachment tools accept a `file_path` to a file on the server instead of # inlined base64. Under the stdio transport the client shares this process's trust # domain, so any readable path is allowed. Under HTTP the caller may be remote, so # `file_path` is refused outright unless you opt in by setting BOOKSTACK_UPLOAD_ROOT: # the path is then resolved with realpath() and must land inside this directory, # which blocks `../` traversal and symlink escapes. # BOOKSTACK_UPLOAD_ROOT=/var/lib/bookstack-mcp/uploads # Server Configuration SERVER_NAME=bookstack-mcp-server # SERVER_VERSION is deliberately unset: it defaults to this package's real # version from package.json. Setting it here would pin a literal that no # release updates, so the server would keep announcing a stale version. # Override only if you intentionally want to report something else. # SERVER_VERSION= SERVER_PORT=3000 # Rate Limiting RATE_LIMIT_REQUESTS_PER_MINUTE=60 RATE_LIMIT_BURST_LIMIT=10 # Validation VALIDATION_ENABLED=true # Strict mode (default: true) rejects invalid tool params at the boundary with a # clear error. Set to false to instead log a warning and forward the params to # BookStack, which will usually reject them with a 422. # # The warning names the schema that rejected the call, how many issues it found and their # kinds ("unrecognized_keys", "invalid_type", ...) - never the offending key or value, # which in this mode are by definition text the caller chose. Turning strict mode back on # is how you see the details: they go to the caller who sent them. VALIDATION_STRICT_MODE=true # Logging # error | warn | info (default) | debug. Every level is written to STDERR, never stdout, # because stdout carries the MCP JSON-RPC stream under MCP_TRANSPORT=stdio. # # WHAT THE LOGS DO NOT CONTAIN, AT ANY LEVEL - AND WHERE THAT STOPS: # Tool argument VALUES are not logged. Not the password on a bookstack_users_create, not a # page body, not an upload's base64 payload (up to HTTP_BODY_LIMIT, per call), and not the # ordinary ones either: not a search query, not a person's name or email address, not a # list filter's value, not an entity's name. The call sites log facts about a request - # which tool, which ids, how many, how long the value was - and every line then goes # through central redaction (src/utils/logger.ts), which applies these rules to every value # at every level: # - a field whose NAME looks like a credential (password, token, secret, authorization, # api_key, ...) is replaced by "[redacted]" - not even its length is kept; # - a field carrying error prose (error, message, data, details, ...) is replaced by its # size, because error text can quote the caller's input - an upload error names the # file_path it was given, and an upstream 422 can quote the content it rejected; # - an Error is reduced to its name, code, HTTP status and stack FRAMES; its message is # replaced by its size; # - a URL is rewritten structurally wherever one appears, including inside prose: any # user:password@ is dropped and query values whose name looks like a credential are # replaced. Host, port, path and ordinary query values are kept. A URL-shaped value # that cannot be parsed is withheld whole rather than guessed at; # - a NAME - a tool name, or the argument/field/filter/parameter names on a line - is # written only if this server registered it: names are checked against the live tool # and schema registries, so one of this server's own names renders and anything else is # replaced by its size. That check exists because a "name" is read out of an object the # caller sent: an unknown tool name is the caller's string, and with # VALIDATION_STRICT_MODE=false so is every unrecognised key; # - EVERY OTHER STRING is replaced by its size, unless it arrives under one of a short, # closed list of field names whose values this server writes itself - HTTP verbs, enum # labels, request paths (SAFE_STRING_KEY_PATTERN in src/utils/logger.ts names each one # and why). This is an allowlist: a field nobody has vetted is withheld by default, so a # value added to a log line next year is withheld until someone decides otherwise. # Numbers and booleans are kept. # # Raising this to `debug` adds request/response and retry diagnostics - request paths, # methods, statuses, byte counts, parameter NAMES - under those same rules. It does not # turn on argument, credential or payload logging: no level does. # # WHAT IS STILL WRITTEN, deliberately, so these logs stay usable: # - tool names, resource URI TEMPLATES, and this server's own argument/field/filter/ # parameter NAMES (never their values, and never a name this server does not define); # - HTTP methods, request paths, status codes, byte counts, value LENGTHS; # - an error's type, code and HTTP status, and its stack frames, which are locations in # this server's own source; # - the BookStack base URL's ORIGIN (scheme, host, port), plus how many path segments it # has. The path itself is spelled out only when it is the documented "/api" (or empty); # any other path - a reverse-proxy mount - is reported as a short digest instead, since # that part of the value is yours and can carry a proxy capability or tenant secret. # The digest is stable, so you can still tell one deployment's mount from another's. # The one thing this cannot promise: a field name is not proof about the value under it. If # a future log line puts a secret under `url` or `type`, it is written - those names are on # the allowlist because of what this codebase puts there today. What the rules DO guarantee # is the direction of the default: a new call site leaks nothing until its key is added to # that list on purpose. The claims above are pinned by tests/unit/log-redaction.test.ts and # tests/transport/stdio.test.ts, which assert them against the real output streams. LOG_LEVEL=info LOG_FORMAT=pretty # Development NODE_ENV=development DEBUG=false