brainstem-mcp
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Basarisiz
- fs module — File system access in scripts/docker-smoke.sh
- spawnSync — Synchronous process spawning in scripts/mcp-call.ts
- network request — Outbound network request in scripts/mcp-call.ts
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Your Obsidian vault as Claude's second brain: self-hosted MCP connector for claude.ai, Claude mobile/Desktop and Claude Code. Links & backlinks, tags, frontmatter queries, daily notes, templates, canvases, safe concurrent edits. Local-first Markdown, no RAG. Docker + Cloudflare tunnel, OAuth 2.1.
brainstem-mcp
Your Obsidian vault as Claude's second brain. A self-hosted MCP connector that gives claude.ai, Claude mobile, Claude Desktop and Claude Code safe read/write access to your own notes: persistent, local-first memory that lives in your Markdown files, not in someone else's database.
What it is
A single-user, self-hosted MCP server that turns your Obsidian vault into a personal knowledge base Claude can read and write — from claude.ai web, Claude mobile, Claude Desktop and Claude Code. Your notes stay plain Markdown files on your machine; Claude gets a memory that persists across conversations, and everything it writes shows up in Obsidian as a normal note you can open and edit. It runs entirely in Docker; a Cloudflare tunnel makes it reachable from those Claude surfaces without opening any ports yourself.
30 tools cover the vault the way Obsidian sees it — a knowledge graph of linked notes, not a folder of files: reading and writing notes, sections, frontmatter and attachments; the link/tag graph (backlinks, orphans, hubs); structured queries and recency; safe concurrent edits and multi-note transactions; canvases; templates; daily notes. See "What Claude can do" below.
What it is not, so you can compare it fairly:
- Not a RAG or semantic-search layer. No embeddings, no vector database; it searches text and structure (links, tags, frontmatter).
- Not an Obsidian plugin. Nothing to install inside Obsidian; it works on the vault folder, so it also runs while Obsidian is closed.
- Not a memory system with its own schema. No new format, no sidecar database: your vault, as it is.
- Not a methodology. PARA, Zettelkasten or no system at all — the conventions live in your vault and in
_brainstem/instructions.md(see below). - Not multi-user. One owner, one vault, Claude only.
Handing this to someone non-technical — a manager, a marketer, anyone who just wants their notes to work with Claude? Give them docs/user-guide.md: what it's for, how to connect, and copy-paste prompts.
Status
v0.4.1 — beta. Built for the owner and technically comfortable colleagues who clone this repo; not (yet) a hosted product.
Verified end-to-end: Linux host · Claude Code · claude.ai web (all tools, via a live quick tunnel) · Docker smoke test in CI.
Implemented but not yet verified by a real run: Claude mobile app · cloudflare (token) tunnel mode · Windows and macOS launchers · reconnect after a tunnel restart.
Issues and pull requests are welcome — see CHANGELOG.md for what shipped and SECURITY.md for reporting vulnerabilities.
Requirements
- Docker Desktop (Windows/macOS) or Docker Engine + Compose v2 (Linux)
- Node.js 24.x
- git
Quick start
git clone https://github.com/vaneavasco/brainstem-mcp.git
cd brainstem-mcp
./brainstem start
Windows: .\brainstem start
On Windows, replace ./brainstem with .\brainstem in every command below.
start checks your prerequisites and tells you exactly what to install if something is missing. On first run it asks for your Obsidian vault folder and whether you have a Cloudflare tunnel token (see Stable URL below — say no to get a quick tunnel instead). Then it starts the stack and prints your connector URL.
Containers come from prebuilt images (ghcr.io/vaneavasco/brainstem-mcp, built by CI for the exact commit you checked out), so a first start takes seconds; if no image matches — offline, or local edits — it builds locally instead. ./brainstem start --build forces a local build.
Connect Claude
claude.ai (web or mobile): Settings → Connectors → Add custom connector → paste <your URL>/mcp → Connect → type the owner secret → Approve.
Claude Code:
claude mcp add --transport http brainstem <your URL>/mcp
then run /mcp and authenticate.
The owner secret lives in .env. Show it any time with:
./brainstem secret show
Commands
./brainstem help <command> prints the full options for any command below.
Everyday
| Command | What it does | Example |
|---|---|---|
./brainstem start |
Check prerequisites, configure on first run, then start brainstem-mcp | ./brainstem start |
./brainstem up |
Start brainstem-mcp (docker compose up) and wait until it is healthy | ./brainstem up |
./brainstem down |
Stop brainstem-mcp | ./brainstem down |
./brainstem status |
Show configuration, health and container status | ./brainstem status |
./brainstem url |
Print the connector/public URL and check it is reachable | ./brainstem url |
./brainstem logs |
Follow container logs | ./brainstem logs |
Configuration
| Command | What it does | Example |
|---|---|---|
./brainstem setup |
Create or update .env (owner secret, vault path, tunnel mode) |
./brainstem setup --vault ~/Documents/Vault |
./brainstem secret |
Show or rotate the owner secret | ./brainstem secret show |
./brainstem vault |
Show or switch the vault this instance works on | ./brainstem vault set ~/Documents/Work |
Maintenance
| Command | What it does | Example |
|---|---|---|
./brainstem update |
Pull the latest version from GitHub, reinstall dependencies and restart | ./brainstem update |
./brainstem doctor |
Check prerequisites and configuration; explain how to fix any issues | ./brainstem doctor |
./brainstem revoke-all |
Revoke all OAuth tokens — every connected client must reconnect | ./brainstem revoke-all |
After an update
A release can add tools or arguments. A connected client keeps the tool list it fetched earlier: the server lets it be cached for five minutes, and some clients keep it for their whole session. If Claude does not seem to know an argument the changelog mentions, reconnect the connector (Claude Code: /mcp → the connector → Reconnect; claude.ai: Settings → Connectors). Nothing breaks in the meantime: the arguments the client already knows keep working, and results may carry new fields without being rejected.
Stable URL (recommended)
A quick tunnel's URL changes every time the stack restarts (see below), so for anything beyond trying it out, get a Cloudflare named tunnel — free, no domain purchase required if you use a Cloudflare-provided hostname:
- Cloudflare dashboard → Zero Trust → Networks → Tunnels → create a tunnel, copy its token.
- Add a Public Hostname on that tunnel pointing to
http://app:3000. - Run:
./brainstem setup --tunnel-token <token> --public-url https://<your-hostname> ./brainstem up
The URL never changes again, and OAuth tokens survive restarts.
Quick tunnel caveat
Without a tunnel token, setup configures a quick tunnel: a random *.trycloudflare.com URL assigned on every start. Whenever the stack restarts (reboot, docker compose restart, a crash), the URL changes, existing tokens stop working (401), and the connector must be removed and re-added in claude.ai / Claude Code — the URL is part of the connector's identity, this can't be avoided.
_brainstem/connection.md, a note written inside your vault, always shows the current URL and the exact reconnect steps — and because it's in the vault, it syncs to your phone too. The app notices the URL change and restarts itself automatically; you don't need to do anything on the server side.
Teach Claude your vault's conventions
On first start the server seeds <vault>/_brainstem/instructions.md. Open it in Obsidian and write, in plain markdown, how Claude should work in your vault — where things live, which frontmatter keys you use, what it must never touch. The text is sent to Claude on every new connection (as the MCP server's instructions), on top of the built-in guidance; frontmatter and <!-- HTML comments --> in that note are not sent. Edits apply to the next connection, no restart needed; it is capped at 12,000 characters.
What Claude can do
Safe concurrent edits
Every read (vault_read, vault_batch_read, vault_daily_note_read, vault_outline) returns a content hash. Pass it back as expectedHash on a write (vault_write, vault_edit, vault_append, vault_frontmatter_update, or moving/deleting a single file): if the note changed since — another Claude session, or you editing it in Obsidian — the call fails with CONFLICT and the current hash instead of silently overwriting. To change several notes as one unit, vault_transaction (up to 20 ops) applies every op or rolls all of them back, using a journal under _brainstem/tx/ that is removed once the transaction settles.
Renames keep links working
vault_move rewrites every wikilink, Markdown link and canvas file node elsewhere in the vault that points at the moved note or folder, mirroring Obsidian's "Automatically update internal links". Links whose target is ambiguous are reported, never guessed; pass updateLinks: false to restore a plain move with no rewriting.
Query your notes
vault_query runs Bases-style structured filters (where, tags, pathPrefix, sort, groupBy) over the in-memory index, with no disk reads; where conditions on contains/startsWith also take an array value ("any of" up to 50 needles), and nonEmpty catches a field that's present but empty. eq/in are link-aware: a frontmatter value written as a wikilink matches its plain name and its full target either way. sum (up to 10 field names) totals over every match, not just the rows a result can carry, into sums/sumCounted — per group too with groupBy; groupPrefix narrows which group keys come back. format: "columns" trades one repeated field name per row for a columns list plus one values array per note, and the whole result (rows or values, groups, column names, hints) stays within 48,000 characters, with truncated and a hint that says what was cut (and, for pathPrefix, when no note exists under it at all) so a wide result never overflows a client. vault_recent lists notes by modification time. vault_tags lists every tag with counts, or every note carrying one (nested tags included). vault_links returns a note's outgoing links, backlinks and embeds, or just total with countOnly: true; filter: { pathPrefix } keeps only backlinks/embeds/unlinked mentions whose source path starts with it (applied before the result caps), so a heavily-linked note can still be checked one folder at a time — total reports the filtered counts. vault_list returns folders (every file under each listed folder, at any depth) and orders entries shallowest first, when the listing is cut by the budget, or is a long deep one (deeper than one level, no glob, over 200 entries, with sub-folders); a glob returns the paths themselves, and any other listing that fits is unchanged.
Sections
vault_read { section: "Heading > Sub-heading" } returns just that heading's text instead of the whole note, and sections: ["Summary", "Decisions"] returns several headings in one call, in document order. vault_batch_read takes the same sections (and maxChars) for up to 20 notes at once; a note that lacks a heading lists it in missingSections instead of failing the batch, and frontmatter: false leaves frontmatter out entirely (frontmatterOmitted: true) so bodies get that room instead. A path that does not exist gets up to 3 near-miss suggestions (vault_read's message, or vault_batch_read's suggestions) when an indexed path matches once a typographic apostrophe, an en/em dash, or letter case is normalised away. Every result that is a list is bounded at 48,000 characters as the client receives it: a batch (frontmatter included; a short note leaves its share to the long ones), a vault_query / vault_recent (rows and groups together), a listing, a note's links, the tags: some clients refuse a larger tool result outright, so a cut with truncated and a hint beats an answer that never arrives. maxChars cuts a read earlier, and vault_query { countOnly: true } answers "how many" without rows. Some clients (the claude.ai connector among them) never show the model the connection instructions, so the same text — server conventions plus your _brainstem/instructions.md — is also the tool brainstem_guide, which the entry tools' descriptions point to. A read that had to be cut at 120,000 characters says so (truncated: true) and carries a hint: list the headings with vault_outline, read by section, never write the cut text back. vault_append { heading, position } writes inside a section instead of at the end of the file, and unique: true makes it a no-op (reported as skipped) when the section already has a line linking to the same [[target]] — different wikilink forms of the same note (a full path, a bare name, a .md suffix, an alias) all count as one target — or unique: "line" to skip only an identical trimmed line, ignoring links entirely, for bullets that legitimately link the same note more than once. The append op of vault_transaction takes the same fields, so a note and the reciprocal bullets on the notes it links to go in as one unit. Wikilinks inside frontmatter values count as links (backlinks, graph, rename), as in Obsidian.
Attachments and file types
.base and .canvas files are read and written as plain text (YAML/JSON), so Claude can edit their structure directly; .canvas also has dedicated tools (vault_canvas_add_node, vault_canvas_update_node, vault_canvas_remove, and more) for editing nodes and edges without hand-rolling JSON. Binary attachments cover Obsidian's full accepted set — images (png/jpeg/gif/webp/avif/bmp/svg), audio (mp3/m4a/ogg/wav/flac/webm/3gp), video (mp4/mov/mkv/ogv/webm) and PDF — capped by MAX_BINARY_BYTES in .env (default 8 MiB; text writes stay capped at 1 MiB).
Templates
vault_create_from_template renders a template note into a new file, substituting {{title}}, {{date}}, {{time}} (and {{date:FMT}}/{{time:FMT}} with Moment-style tokens), plus any {{var}} placeholders from vars. uniquePrefix: true prepends a timestamp to the filename, like Obsidian's core Unique Note plugin.
Vault sync notes
The server keeps all of its own state inside <vault>/_brainstem/ (tokens, the connection note, instance info) so that whatever syncs your vault also carries that state to another machine.
- Obsidian Sync: enable Sync all other types in the sync settings — plain JSON files are not synced by default, and
_brainstem/state.jsonneeds to travel. - Syncthing / git / Dropbox: nothing to configure; they sync everything already.
- Run brainstem-mcp on one machine at a time. Two instances writing to the same synced vault concurrently is unsupported (the app logs a warning if it detects another live instance, but doesn't prevent it).
- The in-memory index self-heals: a background sweep (
VAULT_RECONCILE_MSin.env, default 5 minutes;0disables it, otherwise at least 10 seconds) re-reads any note whose size or modified time drifted from what the index has, and also runs whenever the filesystem watcher itself reports an error (never dropped: at most one pass per 30 seconds, with a trailing pass for a burst) — recovering from watcher events an OS-level queue silently dropped (e.g. thousands of files rewritten in one minute by another tool). A note is dropped from the index only after its absence is confirmed on disk, so a note written while the sweep runs is never lost../brainstem status,/health(vault.reconciledAt) andbrainstem_ping(index.reconciledAt) show when the index was last checked.brainstem_pingalso shows the index's size beside its budget (index.bytes,index.budgetBytes,index.overBudget): the budget is a warning line (one log line when crossed, a warning in./brainstem status,vault.indexOverBudgetin/health; nothing is evicted), and the process holds roughly twiceindex.bytesof heap for it — about 3.5 KB of index per long note.
Security model
- The owner secret gates the consent page for every new client; five wrong attempts lock it for 15 minutes.
- OAuth tokens are stored only as SHA-256 hashes in
_brainstem/state.json, so a synced copy of that file leaks nothing usable. - Every new client goes through a consent screen that shows the redirect hostname before granting access — no client is trusted silently.
- Client discovery (Client ID Metadata Documents) is restricted to an allowlist (
claude.ai,claude.comby default) and fetched with an SSRF-hardened client: no redirects, private/loopback addresses rejected, size and time capped. _brainstem/is a reserved folder: every tool (list, search, read, write) refuses to touch it, so it's invisible to Claude.
Troubleshooting
- 401 / "needs authentication" right after a restart, in quick-tunnel mode: expected — the tunnel URL changed. Read
_brainstem/connection.mdin your vault for the new URL and reconnect the connector. - "Docker is not running or not installed": start Docker Desktop (or the Docker daemon on Linux) and rerun the command;
./brainstem doctorexplains exactly what's missing. - Locked out of the consent page: five wrong owner-secret attempts lock it for 15 minutes; check the correct value with
./brainstem secret show. - A client won't reconnect, or you rotated the secret:
./brainstem revoke-allforces every client to go through consent again. - Something looks wrong in general:
./brainstem logs(or./brainstem logs tunnel/./brainstem logs app) to see what the containers are doing.
For developers
Working on the code with an AI coding agent? AGENTS.md is the project guide it reads (Cursor, Copilot, Codex, …); CLAUDE.md imports it for Claude Code.
The launcher (./brainstem, brainstem.cmd) is a thin wrapper: it checks Node/Docker, installs dependencies, then delegates to the TypeScript CLI.
npm install
npm test
npm run typecheck && npm run lint
npm run dev # run the server directly, without Docker
npm run brainstem -- <command> # run the CLI without the launcher's checks
npm run docker:smoke # end-to-end smoke test against the Docker image
npm run mcp:call -- --list # headless OAuth + tool calls against a running instance
The launcher reinstalls dependencies only when package-lock.json is newer thannode_modules/.package-lock.json. It installs the runtime-only tree
(npm ci --omit=dev), except when it finds node_modules/.bin/vitest — the
mark of a developer checkout — in which case it runs a plain npm ci so your
devDependencies survive. Set BRAINSTEM_SKIP_INSTALL=1 to skip the install step
altogether (the launcher tests do this, so a stale lockfile can never rewritenode_modules mid-suite).
Images. ./brainstem up resolves the checked-out commit to the tag CI published (sha-<7>), runs docker compose pull, and starts without building; a dirty working tree or a failed pull falls back to docker compose up --build, which tags the local build dev. --build skips the registry; --no-build never builds (pulls, else reuses the last local build). CI (publish-images in .github/workflows/ci.yml) pushes ghcr.io/vaneavasco/brainstem-mcp and …/brainstem-mcp-tunnel for every commit on main and every v* tag (multi-arch: amd64 + arm64).
See docs/ for the spec, ADRs and implementation plans.
License
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi