mcp-silverbullet
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., "@mcp-silverbulletsearch my SilverBullet space for pages about Q3 planning"
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.
mcp-silverbullet
Model Context Protocol bridge between SilverBullet
and MCP clients (Grok Custom Connectors, mcp CLI, …). The bridge is a
side-car on loopback; it does not provision tunnels.
Architecture and threat model: docs/design.md.
What it exposes
Fourteen tools + one resource template. Every write tool returns
the new etag on every successful write — read it and feed it back via
if_match on the next call to fail fast on stale etags.
read_page(name)→{body, etag, size_bytes, last_modified_ms}— markdown body and metadata.etagisNoneif SB stripped theETagresponse header.page_exists(name)→bool—Trueon 200,Falseon 404,ToolErroron 5xx so "no, proceed" stays distinct from "SB is broken".write_page(name, content, if_match?)→{name, etag, size_bytes, last_modified_ms, created_ms}— create or overwrite. Emptyname/contentraisesToolError("... must not be empty")upfront (T40).create_page(name, content)→ same envelope aswrite_page— but refuses to overwrite an existing page, surfacingToolError("page already exists: {name}; use write_page to overwrite")on collision.if_match="*"is implied; usewrite_pagedirectly if you want to write with a precondition.append_to_page(name, text, if_match?, dry_run=False)— read-modify- write append (one newline separator inserted unless the body already ends in one); returns the same envelope. Withdry_run=Truereturns{dry_run, original, patched, diff}without writing. EmptytextraisesToolError("text must not be empty")upfront (T40).prepend_to_page(name, content, position="after_frontmatter"|"top", if_match?, dry_run=False)— top-of-body insert with YAML frontmatter awareness. Defaultposition="after_frontmatter"inserts the new content between the closing---of the frontmatter block and the first body line (the human-meaningful default for journal / daily-notes pages);position="top"overrides and inserts above the frontmatter. Both positions produce the same splice on pages without frontmatter. Malformed frontmatter (opening fence but no close) is treated as no-frontmatter.patch_page_lines(name, start_line, end_line, new_content, if_match?, dry_run=False)— replace linesstart_line..end_line(1-indexed, inclusive) withnew_content; passnew_content=""to delete a range; preserves the page's trailing newline if it had one. EmptynameraisesToolError("name must not be empty")upfront (T40).patch_page_replace(name, find, new_string, replace_all=False, if_match?, dry_run=False)— literal substring replace (no regex);replace_all=False(the safe default) errors iffindmatches more than once, so a typo never silently mass-edits.move_page(name, new_name, if_match?)— rename (write-then-delete so a partial failure leaves the body at the new name); destination always refuses to overwrite. Emptyname/new_nameraisesToolError("name must not be empty")upfront (T40).delete_page(name, if_match?)→{name, etag, size_bytes=None, last_modified_ms=None, created_ms=None}— hard delete; SB's DELETE response doesn't echo timestamps / size. EmptynameraisesToolError("name must not be empty")upfront (T40).list_pages(prefix?, contains?)→[{name, etag, size_bytes, last_modified_ms, created_ms}][]— sendsX-Sync-Mode: 1so SB 2.x returns JSON fromGET /.fsinstead of 307-redirecting to the SPA.prefix=doesstartswithmatching (unchanged from v1);contains=(T37) does substring matching against the page name. Both filters compose as AND when both are set; either empty is a no-op for that criterion; both empty returns the full listing. Both filters run client-side before per-page hydration, so a narrow filter reduces the N+1 round-trip count. The etag field isnullon this SB build unless you opt in to per-page hydration withMCP_SILVERBULLET_LIST_PAGES_HYDRATE_ETAGS=1(one GET per row; partial failures leave the affected row's etag asnullrather than failing the whole call). The filter only ever matches against page names; body-content search lives behind the journal gate (see "Discovery tools (journal-gated)" below).diff_pages(name, other_name?, other_body?)→{diff, name, other?}— line-based unified diff between two pages (or a page and a literal string). Pass exactly one ofother_name/other_body; passing neither or both isToolError("pass exactly one of other_name or other_body")upfront, no wasted read.list_tasks(page?, prefix?)→[{name, ref, line, state, text}][]— enumerate checkbox bullets on a page (per-page form, always available viaGET /.fs/{page}) or across the whole space (space-walk form, gated — see Discovery tools (journal-gated)).stateis the literal checkbox character (" "for[ ],"x"for[x],"X"for[X]). Frontmatter-block bullets are skipped.check_task(page, ref, state="done", if_match?, dry_run=False)— flip a checkbox bullet's state by its wikilink ref. Reads the page, finds the unique bullet whose wikilink target equalsref, flips the marker, writes the body back viaPUT /.fs/{page}withIf-Match: <read_etag>so a concurrent edit fails rather than silently clobbering.state="done"flips to[x],"todo"to[ ],"cancelled"to[X].silverbullet://page/{name}→ JSON envelope{body, etag, size_bytes, last_modified_ms}(same shape asread_page; MIME typeapplication/json).
Concurrency: read-then-write with current etag
Every write tool accepts if_match ("*" to require existence,
<etag> to require an exact body match, None for unconditional).
On this build, SilverBullet does not honor If-Match and does
not return ETag on PUT — the bridge synthesizes a fallback etag
from X-Content-Length (T44; pre-T44 the form was
"{last_modified_ms}-{size_bytes}") and runs a post-write
re-read to compare. If a stale etag slips through, the bridge raises
ToolError("concurrent edit detected: …; read it again and re-issue the write with the current etag"). The fix is always the same: read
the page again, take the new etag from the response, retry the write.
On a 412, the bridge tells you the next call's exact if_match=
value (silent-overwrite path) or the read_page(<name>) call that
gives you that value (standard path). You don't need to guess. The
silent-overwrite path (the W36 pattern: SB ignored If-Match and
wrote anyway, the bridge's post-write re-read caught the drift)
embeds the post-write etag in the error message — the agent's
next call is write_page(name, content, if_match="<that etag>"),
no extra round trip. The standard path (SB honored If-Match and
returned 412) embeds a literal read_page("<name>") token pointing
at the page; SB's 412 response body is empty on this build so the
bridge can't surface the etag directly, but the next call is
unambiguous. Both surfaces are byte-additive over v1.5 — an agent
that pattern-matches on the bare precondition failed or
concurrent edit detected prefix still matches; only agents that
pinned the byte-for-byte full message need to update. The T42
[concurrent_edit_hint: true] contention marker still rides as a
trailing suffix after the v1.6 wording change.
T46 silent-overwrite 412 is rare on read-modify-write tools.
v1.6 narrowed the post-write verification helper's detection
semantic: it now compares the verification-GET etag against the
PUT-response etag (the bridge's view of "what we just wrote"),
not the caller's pre-write if_match. The pre-v1.6 helper raised
a spurious "concurrent edit detected" on every read-modify-write
that grew the page (the synthesized etag is str(size_bytes)
per T44; the post-write size differs from the pre-write size on
every append / prepend / patch / move). Live reproduction on this
dev box confirmed: 76 spurious errors in 6 hours on Trading Book/ Logs/2026-W36.md, every one with current_etag - expected_etag
exactly equal to the appended content length. T46 closes that
gap — read-modify-write tools now succeed on a non-race write.
The helper still catches genuine concurrent edits that land
between the bridge's PUT and the verification GET (the narrow
window in which another writer can land a PUT on the same page).
A caller-supplied stale if_match (the agent manages its own
etag round-trip and the page drifted in the gap) no longer fires
the helper; that defense moved to the agent side — re-read on
the agent's own retry loop, or use the bridge's read-modify-write
tools (which do their own internal re-read and pass the
verification check).
T47 auto-retry is the default on read-modify-write tools.
By default, append_to_page / prepend_to_page /
patch_page_lines / patch_page_replace / check_task /
move_page retry up to max_retries=3 times when the
post-write verification helper fires concurrent edit detected. On each retry the bridge re-reads the body,
re-derives the operation against the page's current state,
and re-PUTs. Pass max_retries=0 to opt out and see the raw
412 (matches pre-v1.6 behavior). Genuine semantic errors
(find not found in body, page not found, body-size
errors) surface to the agent unchanged — the bridge retries
only on the post-write-verification race, not on
anchor-mismatch or 404. The standard-412 path (SB honored
If-Match and returned 412) is not auto-retried: an agent
that passed an explicit stale if_match should see the 412
— retrying would mask the precondition failure.
Every successful write returns the new etag. Pass it to the next
if_match on the same page and you'll never see the concurrency error.
Dry-run mode
append_to_page / prepend_to_page / patch_page_lines /
patch_page_replace / check_task accept dry_run=True to preview
a patch without committing. The read still happens (the tool needs
the body to compute the patch), if_match is validated against the
read's etag, and the response is {dry_run, original, patched, diff}.
A no-op patch returns an empty diff.
Discovery tools (journal-gated)
When list_pages(prefix=, contains=) narrows the listing but the
page you want isn't on a name match — its name doesn't contain the
phrase, but its body does — the HTTP /.fs API can't help: SB has
no built-in search endpoint, so substring search over page bodies
needs filesystem access to the SB space directory. Three tools
provide that; they live behind the journal gate, enabled by
setting both:
MCP_SILVERBULLET_SPACE_PATH— absolute path to the SB space directory (typical: same host that runs SilverBullet; rare behind a containerized split).MCP_SILVERBULLET_JOURNAL_TOOLS=1— truthy opt-in flag (1/true/yes/on).
Without either, the bridge boots cleanly without these tools and logs a single INFO/WARN line. Restart the bridge after changing either env var.
pages_touching_topic(query, prefix?)— case-insensitive name+content substring search; returns{name, match, snippet}[](matchis"name","content", or"both"). Usesrg --jsonwhen available, falls back to pure-Python otherwise.search_pages(query, prefix?, limit=20)— bounded variant ofpages_touching_topicwith alimitknob (default 20, hard cap 100). Same wire shape. Use this when you want the top N hits;pages_touching_topicfor unbounded scans.find_backlinks(target) -> [{file, line, text}]— wikilink-target backlinks for the rename-pre-flight workflow.fileis the relative path to the linking page,lineis the 1-indexed editor line number,textis the stripped line. Target normalization: leading/trailing slashes and a trailing.mdare stripped; aliases ([[target|alias]]) match the bare target. Self-links are returned (filter client-side). Empty / whitespace-onlytargetraisesToolError("target must not be empty")upfront.
Three additional journal tools (also gated, but not discovery-flavoured):
journal_histogram(prefix?)— bucket*.mdpages byYYYY-MM.tag_summary(prefix?)— count occurrences of everytags:value.recent_pages(limit?, prefix?)— newest pages by mtime.
Related MCP server: silverbullet-mcp-server
Requirements
Nix (flake) or Python 3.11–3.13 + uv
A running SilverBullet (
/.fsHTTP API)Optional: an existing Cloudflare tunnel (or nginx) in front of
127.0.0.1:8000
Boot order
Generate a token (any high-entropy string). This is
Tbelow.T=$(openssl rand -hex 32)SilverBullet on loopback, same secret if SB auth is on:
SB_AUTH_TOKEN=$T silverbullet --hostname 127.0.0.1 --port 3000 /path/to/spaceIf your SilverBullet has no auth (dev box), leave SB without a token and set
MCP_SILVERBULLET_SB_TOKENempty on the bridge (step 3).Bridge — from a checkout. The bridge defaults to JWT mode (validates tokens against an IdP's JWKS); set
MCP_SILVERBULLET_AUTH_MODE=staticfor the legacy shared-secret surface (used bymcp devand other non-IdP setups).# JWT mode (default — bridge sits behind Cloudflare Access, # Auth0, Okta, Google-IAP, …; validates per-user tokens against # the IdP's JWKS): export MCP_SILVERBULLET_JWT_ISSUER=https://<org>.cloudflareaccess.com export MCP_SILVERBULLET_JWT_AUDIENCE=<AUD-tag-from-CF-dashboard> export MCP_SILVERBULLET_JWT_JWKS_URL=https://<org>.cloudflareaccess.com/cdn-cgi/access/certs export MCP_SILVERBULLET_SB_URL=http://127.0.0.1:3000 nix run .#mcp-silverbullet # Static mode (legacy shared-secret): export MCP_SILVERBULLET_AUTH_MODE=static export MCP_SILVERBULLET_TOKEN=$T export MCP_SILVERBULLET_SB_URL=http://127.0.0.1:3000 nix run .#mcp-silverbulletCommon knobs (both modes):
# optional: empty when SB has no auth # export MCP_SILVERBULLET_SB_TOKEN= # optional: public URL stamped into WWW-Authenticate + discovery # export MCP_SILVERBULLET_RESOURCE_URL=https://<tunnel>/mcp # optional: extra Host values when nginx/cloudflared forward a public name # export MCP_SILVERBULLET_ALLOWED_HOSTS=<mcp>.local,<tunnel>.trycloudflare.comEquivalent without Nix:
uv sync && uv run mcp-silverbullet. Listens onhttp://127.0.0.1:8000/mcpby default (MCP_SILVERBULLET_HOST/MCP_SILVERBULLET_PORT).Tunnel (operator-owned; this repo does not start
cloudflared):cloudflared tunnel --url http://127.0.0.1:8000Client — in JWT mode, paste
https://<tunnel>/mcpand let the IdP handle auth (the bridge trusts whatever IdP-issued JWT reaches it). In static mode, paste the bearer too:MCP_SILVERBULLET_AUTH_MODE=static MCP_SILVERBULLET_TOKEN=$T \ mcp dev http://127.0.0.1:8000/mcpIf a quick-tunnel URL rotates, the bearer stays; re-paste the new URL.
For a full Cloudflare Access + Managed OAuth + cloudflared setup (named tunnel, public hostname, per-user JWT validation against the Cloudflare team JWKS), see
docs/cloudflare-setup.md. It covers the nginx config that copiesCf-Access-Jwt-AssertionintoAuthorization: Bearer, rewrites theHostheader so the bridge's MCP transport-security check passes, and the Access app config (Managed OAuth, DCR redirect-URI allowlist, bypass for the discovery endpoint).
Use from a Pi coding agent session
The repo ships with a project-local .mcp.json so a Pi session
running in this checkout discovers the bridge automatically (via the
pi-mcp-adapter extension). After python -m mcp_silverbullet (or
nix run .#mcp-silverbullet) is running on 127.0.0.1:8000, run
/reload in Pi and the bridge's fourteen always-on tools register as
direct Pi tools. The journal-surface tools register additionally
when MCP_SILVERBULLET_JOURNAL_TOOLS=1 and MCP_SILVERBULLET_SPACE_PATH
are both set (see
Discovery tools (journal-gated)).
In static mode, the bearer token is read at HTTP-connect time via
the !command syntax in .mcp.json, pointed at
~/.config/mcp-silverbullet/token (mode 600):
python -c 'import secrets; print(secrets.token_hex(32))' \
> ~/.config/mcp-silverbullet/token
chmod 600 ~/.config/mcp-silverbullet/tokenThe bridge is a side-car, not a daemon: it has to be running for the
tools to work, and lifecycle: lazy in .mcp.json means Pi won't
try to connect until the first tool call.
Env vars
Variable | Default | Role |
|
|
|
| (required when | Shared bearer secret. Compared constant-time against the inbound |
| (required when | Expected |
| (required when | Expected |
| (required when | JWKS endpoint URL. Cloudflare Access: |
|
| Comma-separated JWA algorithm allow-list. Pinned to |
|
| Clock-skew tolerance for |
|
| SilverBullet origin. |
| same as | Outbound SB bearer; empty string = no header. |
|
| Discovery + |
|
| Bind address. |
|
| Bind port. |
| (unset → SDK loopback default) | Extra |
| (unset) | Absolute path to the SB space directory; required to enable the journal surface (see Discovery tools (journal-gated)). |
| (unset) | Truthy ( |
| (unset) | Truthy enables per-page etag-hydration on |
|
|
|
| (unset) | Truthy ( |
WARNING: Invalid HTTP request received. from uvicorn means the first bytes on the socket were not HTTP/1. This uvicorn build does not log what they were, even at debug. Set MCP_SILVERBULLET_LOG_LEVEL=debug (or MCP_SILVERBULLET_DEBUG=1) and the bridge logs a classification + the first 200 bytes next to that warning. Typical causes behind Cloudflare Access / cloudflared: HTTP/2 to an HTTP/1 origin (http2-preface), HTTPS hitting the HTTP port (tls-clienthello), or a TCP health check with no HTTP (empty / unknown).
Dev
nix develop # editable source + pytest
pytest # Layer 1–2, no live SB
nix flake checkMCP SDK is pinned at mcp==2.1.1 (uv.lock). License: MIT.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
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.
Related MCP Connectors
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
- QuallaaOAuthcom.quallaa
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables LLMs to interact with SilverBullet notes and data through a bridge server. Provides secure access to read and manipulate your SilverBullet space via standardized MCP tools and resources.36MIT
- AlicenseNot gradedqualityBmaintenanceExposes a SilverBullet note space to Claude via MCP, with OAuth 2.1, collision-safe writes, and structured errors.MIT
- FlicenseNot gradedqualityDmaintenanceExposes your local machine's filesystem, git, shell, network, databases, and system to any MCP-compatible LLM client over HTTP.6-
- AlicenseNot gradedqualityCmaintenanceAn MCP server that connects a SilverBullet space over its HTTP filesystem API, exposing a scoped prefix of notes to AI agents with read/write/delete capabilities.MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/kesor/silverbullet-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server