silverbullet-mcp-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@silverbullet-mcp-serverlist my recent notes"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
silverbullet-mcp-server
A standalone MCP server that exposes a SilverBullet space to Claude over HTTP — with built-in OAuth 2.1, collision-safe writes, and structured, model-visible errors.
It runs as its own service and talks to SilverBullet over the HTTP API, so it works against any hosted SilverBullet — including managed hosts where you can't drop in a sidecar container. (It can equally run as a sidecar next to a self-hosted instance; nothing ties it to one deployment model.) The examples below deploy to Fly.io, but the server is host-agnostic on both ends.
It's built for a single user at personal note volume. A few scope choices (stateless JWTs, no refresh token, naive client-side search) are deliberate and flagged below — they double as the contribution roadmap.
Highlights
Standalone remote server — works with managed hosts, not just sidecar setups.
OAuth 2.1 built in (spec-compliant for the Claude.ai connector), plus a static-token bypass for dev/curl.
Collision-safe overwrites via a
lastModifiedversion handshake.Structured, model-visible errors carrying remediation hints.
Soft-delete, body-size cap, path validation, audit logging.
Status: v0.5. See the CHANGELOG for the full version history and the design reasoning behind each release.
Architecture
Claude (web / mobile)
│
│ Streamable HTTP, Bearer <JWT> (OAuth)
│ -or- Bearer MCP_TOKEN (dev / curl)
▼
[ silverbullet-mcp-server on Fly.io ] <-- this repo
│ ↑ /authorize, /token, /.well-known/...
│ │ Owner approves via OWNER_TOKEN in browser
│
│ GET /.fs, GET /.fs/<path>, Bearer SB_TOKEN
▼
[ Your SilverBullet instance ]
notes.example.comThree credentials, each scoped tightly:
SB_TOKEN— this server presents to SilverBullet upstream.OAuth (
OAUTH_CLIENT_ID/OAUTH_CLIENT_SECRET/OWNER_TOKEN/JWT_SIGNING_KEY) — primary path for Claude clients. Claude.ai's web UI requires this flow.MCP_TOKEN— static Bearer kept alongside OAuth as a dev/curl bypass so the server stays smoke-testable without running the full auth dance.
Any of them can rotate without touching the others.
Tools
Read tools
Tool | Inputs | Returns |
|
| Every |
|
| Two content blocks: |
|
| Top substring matches with snippets and match counts. |
include_trash: true surfaces pages that have been soft-deleted (under _trash/).
Search is naive (fan-out fetch + substring). Fine for personal note volumes; revisit if latency bites.
Write tools
Tool | Inputs | Behavior |
|
| Creates a new page; errors if it already exists. Returns |
|
| Overwrites an existing page. Collision-safe: rejected with a conflict error if the server's current |
|
| Appends content at the end, separated by a blank line. No |
|
| Inserts at top or after YAML frontmatter (default). Server-side concat — the existing body never passes through the model. No |
|
| Soft-deletes to |
Collision-safe overwrites. write_page enforces a version handshake. Workflow: read_page returns the current lastModified alongside the body; pass that value back as expected_last_modified on the subsequent write_page. If the page changed in between, the write is rejected with a conflict error and the caller should re-read to reconcile. create_page returns a fresh lastModified so a caller is left write-ready immediately after creation. append_to_page and prepend_to_page deliberately omit lastModified from their responses — they merge server-side without the caller seeing the full body, so the caller is not in a position to follow up with a guarded write_page. There is a narrow TOCTOU window inside write_page between the conflict check and the PUT — acceptable for a single-user space; closing it would require an If-Unmodified-Since (or equivalent) on the SilverBullet side.
Version marker hygiene. The ms-precision lastModified is the optimistic-concurrency token for write_page. By contract it is only obtainable from read_page, create_page, or write_page — the three tools that have surfaced the full page body. To stop the same value from leaking through other surfaces, list_pages and search_pages round each entry's lastModified to second precision (still useful for recency display, useless as a write key — the rounded value almost never matches the server's true ms value). The conflict error response includes expectedLastModified (echoing what the caller sent) and a generic "page has been modified" message, but does not include the server's current lastModified — otherwise a caller could retry the write using the leaked value without re-reading the body.
Error shape. Every tool failure comes back as a regular content block carrying a JSON payload with {error, status, message, ..., remediation}. The error field is a short machine-readable code (conflict, not_found, already_exists, too_large, forbidden_path, invalid_path, upstream, internal); status mirrors the closest HTTP analog (e.g. 409 for conflict, 413 for too_large); remediation gives the agent a concrete next step. The handler stack does not set isError: true on the MCP response — the Claude.ai connector has been observed to swallow the content payload when that flag is set, leaving the model with only "Error occurred during tool execution." Returning the structured payload as ordinary content keeps the remediation visible. Every error also emits an [ERROR] tool=... code=... page=... audit line to stderr (Fly logs).
Write permission model. Writes are gated by Claude.ai's per-tool permission UI — set each write tool to Ask for confirmation before every call. No server-side flag or separate OAuth scope; the same connector and credentials serve both read and write tools. On every write the server enforces: a 256 KB body cap, path validation (no .., empty segments, double .md), and X-Permission: rw on every PUT (omitting it would make SilverBullet silently mark the page read-only in its UI). Soft-delete moves pages to _trash/YYYY-MM/; collisions in the same month get a -<unix-ms> filename suffix. Trash is hidden from list and search by default; include_trash: true reveals it. All write operations emit [WRITE] audit lines to stderr (visible in Fly logs); write_page audit lines include both expected_last_modified and the resulting last_modified.
Environment
See .env.example. All of the below are required at boot.
Variable | What it is |
| Base URL of the SilverBullet instance (no trailing slash). |
| The |
| Static Bearer accepted as a dev/curl bypass. Generate with |
| Canonical URL of this MCP server. Goes into OAuth metadata documents. |
| Opaque string. Paste into Claude.ai connector "Advanced settings." |
| Opaque string. Paste into Claude.ai connector "Advanced settings." |
| The password you type into the browser login page to approve a client. |
| Key used to sign 90-day access-token JWTs. Rotating it revokes all tokens. |
| HTTP listen port (Fly maps internally). Default |
Local development
cp .env.example .env
# fill in every variable; commands to generate the random ones are in the file
npm install
npm run devSmoke-test against your live SB instance using the dev-bypass token:
set -a; source .env; set +a
curl http://localhost:8080/healthz # 200 always
curl http://localhost:8080/readyz # 200 if SB reachable
# OAuth discovery documents (no auth required on these)
curl http://localhost:8080/.well-known/oauth-protected-resource
curl http://localhost:8080/.well-known/oauth-authorization-server
# MCP initialize handshake (uses dev-bypass MCP_TOKEN)
curl -X POST http://localhost:8080/mcp \
-H "Authorization: Bearer $MCP_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{
"protocolVersion":"2025-03-26",
"capabilities":{},
"clientInfo":{"name":"curl","version":"0"}
}
}'Deploy to Fly.io
First time only:
fly apps create your-app
fly secrets set \
SB_URL=https://notes.example.com \
SB_TOKEN=<your SB_AUTH_TOKEN> \
MCP_TOKEN=$(openssl rand -hex 32) \
PUBLIC_URL=https://your-app.fly.dev \
OAUTH_CLIENT_ID=$(uuidgen | tr 'A-Z' 'a-z') \
OAUTH_CLIENT_SECRET=$(openssl rand -hex 32) \
OWNER_TOKEN=<something memorable but not guessable> \
JWT_SIGNING_KEY=$(openssl rand -hex 64) \
--app your-appThen every deploy:
fly deployThe server scales to zero when idle (see auto_stop_machines = "suspend" in
fly.toml).
Once you add a custom domain (fly certs add mcp.example.com), update
PUBLIC_URL to match — OAuth metadata must point at the URL clients actually
reach you on:
fly secrets set PUBLIC_URL=https://mcp.example.com --app your-appConnect from Claude (OAuth)
In Claude.ai → Settings → Connectors → Add custom connector:
Field | Value |
URL |
|
(Advanced) Client ID | The |
(Advanced) Client Secret | The |
What happens on first use:
Claude calls the MCP and gets a 401 with a
WWW-Authenticateheader.Claude reads the discovery documents and opens your browser to
https://your-app.fly.dev/authorize?....You see a one-field login page. Enter
OWNER_TOKEN. Approve.Claude exchanges the resulting code for a 90-day JWT and uses it on every subsequent tool call.
When the JWT expires, step 3 repeats. To force re-auth across all clients
immediately, rotate JWT_SIGNING_KEY and redeploy.
Roadmap
v0.2 — write tools:
create_page,write_page,append_to_page,prepend_to_page,delete_page. Soft-delete, 256 KB cap, audit logging. Shipped.v0.3 — collision-safe overwrites via
lastModifiedenvelopes;read_pagereturns{path, lastModified}+ body;write_pagerequiresexpected_last_modifiedand is overwrite-only. Shipped.v0.4 — typed errors (
ConflictError,PageNotFoundError,FileNotFoundError,PageAlreadyExistsError,BodyTooLargeError,ForbiddenPathError,InvalidPathError,UpstreamError) routed through a centralmapToolErrorthat returns structured{error, status, message, remediation}payloads as regular content (noisError), plus[ERROR]audit lines. Shipped.v0.5 — version-marker hygiene:
lastModifiedno longer leaks throughlist_pages,search_pages, or conflict-error payloads; rounded to second precision in recency contexts. Shipped.v0.6 — cached file index, real search ranking, frontmatter awareness.
v0.7 — refresh tokens, so JWT renewal is silent.
future — path-prefix allowlist (restrict writes to specific directories if the broad write surface ever feels too open); atomic writes (current SB backend uses non-atomic
os.WriteFile); switchstatFileto a header-based metadata GET once SB'sLast-Modifiedheader behavior is verified, to avoid the per-write directory listing.
The CHANGELOG doubles as a design log — the reasoning behind each release, not just the diff.
Maintenance
Provided as-is. This is a personal-scale project, shared in case it's useful to others; expect light, best-effort maintenance. Issues and pull requests are welcome — especially against the roadmap items above — but response times will vary.
License
MIT © Beta Brooklyn.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/xmatthewx/silverbullet-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server