brain-mcp
Provides tools for reading and capturing notes in an Obsidian-compatible PARA-structured markdown vault, including a constrained capture tool that writes new notes to the vault's inbox.
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., "@brain-mcpCapture this idea: write a blog post about MCP servers"
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.
brain-mcp
Icon design credit: wenmeiZhou on iStock.
An MCP server that exposes a markdown, PARA-structured second-brain vault
(Obsidian-compatible) as a set of read tools plus a single constrained write
tool (capture, which only ever creates new notes in 00-inbox/). Runs as a
Docker container, consumed over streamable-HTTP by Claude Code, opencode, and
other MCP clients.
Full behavioural spec: see brain-mcp-spec.md in this repo (or wherever you
keep it) if you need the "why" behind a design choice.
Notes on the CONVENTIONS.md parser
brain_structure() reads the type/status/domain enums live out of
your vault's 90-meta/CONVENTIONS.md rather than hardcoding them (see
_extract_enum_values in src/brain_mcp/vault.py). It's been checked
against a real vault's file directly: that file states the three enums as
bold inline labels under one ## Enums heading (**type:** \note`,
`project`, ...) rather than each getting its own subheading, so _extract_enum_values tries that shape first (bounded to the label's own paragraph, so it doesn't pick up unrelated backtick-quoted words in the prose below — e.g. "domainis the primary query axis..."), falling back to a heading-based heuristic for vaults that document enums differently. If you restructureCONVENTIONS.md`'s Enums section later, re-check this
parser — the server fails loudly at startup rather than falling back to bad
defaults if it can't parse a non-empty value list for all three fields.
uid format: always a UUIDv4, generated by capture() at note
creation and never changed afterward — never a timestamp-shaped id like
YYYYMMDD-HHmm. This is deliberate: it's what keeps read_note's
short-prefix lookup (≥8 chars) meaningful, which it wouldn't be against a
low-entropy, date-based scheme where every note from the same day shares a
prefix. If a vault's own CONVENTIONS.md documents a different uid
scheme, that's the vault's documentation that needs updating to match —
this server doesn't special-case it.
Related MCP server: Obsidian MCP Server
Requirements
The vault's PARA structure and frontmatter schema as described in the vault's own
90-meta/CONVENTIONS.md.Docker (or Docker Compose) on the Unraid box, or Python 3.12 +
uvlocally for development.ripgrepon PATH (bundled in the container image; install separately for local dev).
Configuration
All configuration is via environment variables — nothing about a specific vault (path, name, token) is hardcoded, so the same image serves any number of sibling vaults as separate containers.
Variable | Required | Default | Meaning |
| yes | — | Absolute path to the vault root in-container |
| yes | — | Instance name, e.g. |
| yes | — | Bearer token required on every MCP request |
| no |
| Listen port |
| no |
| Listen address |
| no |
|
|
Copy .env.example to .env and fill in BRAIN_NAME, BRAIN_VAULT_PATH
(the host path to your vault), and BRAIN_TOKEN (a random secret —
openssl rand -hex 32 works well) before running Compose.
Local development
uv sync --dev # or: python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
uv run pytest # or: .venv/bin/python -m pytestTests run entirely against a synthetic fixture vault built in
tests/conftest.py — never against real data.
To run the server locally against a real (or scratch) vault directory:
export BRAIN_ROOT=/path/to/vault
export BRAIN_NAME=personal
export BRAIN_TOKEN=dev-token
uv run brain-mcpRunning on Unraid
cp .env.example .env # fill in BRAIN_NAME, BRAIN_VAULT_PATH, BRAIN_TOKEN
docker compose up -d --build # or: docker compose pull && docker compose up -d
curl http://<unraid-host>:3100/health--build builds from this checkout; pull instead fetches the same
image prebuilt from GHCR (see "Installing as an Unraid app" below) —
either produces the ghcr.io/sipho102/brain-mcp:latest tag locally.
The compose file mounts BRAIN_VAULT_PATH read-only and re-mounts just
00-inbox/ read-write on top of it:
volumes:
- ${BRAIN_VAULT_PATH}:/vault:ro
- ${BRAIN_VAULT_PATH}/00-inbox:/vault/00-inbox:rwThis is intentional and load-bearing — even a bug in the write path can't touch anything outside the inbox, regardless of what the Python code thinks it's doing. Don't simplify it to a single read-write mount.
Installing as an Unraid app instead
If you'd rather manage this from Unraid's Docker tab like any other app —
a form instead of editing .env, a Start/Stop/Update button afterward —
there's a template for that in unraid/brain-mcp.xml. .github/workflows/ publish.yml builds this repo's image and publishes it to GHCR
(ghcr.io/sipho102/brain-mcp:latest) on every push to main, and the
template pulls that directly — no cloning or building on the Unraid box
at all.
Make the template available to Unraid:
Recommended — in the Docker tab, Add Container and paste this repo's raw template URL directly into the template field:
https://raw.githubusercontent.com/sipho102/brain-mcp/main/unraid/brain-mcp.xmlNothing gets written to Unraid's local templates folder this way, so there's no stray file left behind to conflict with later — see the caveat below on the alternative method.Or copy it into Unraid's local templates folder over SSH first:
curl -o /boot/config/plugins/dockerMan/templates-user/brain-mcp.xml \ https://raw.githubusercontent.com/sipho102/brain-mcp/main/unraid/brain-mcp.xmlIt'll then show up under Docker → Add Container → template dropdown — but see the note right after Add Container about deleting this file once the container exists.
Either way, you'll get a form for the vault path, the inbox path (must be
<vault path>/00-inbox — the template can't derive it for you), instance
name, and bearer token; everything else is pre-filled with sane defaults
under "advanced view".
If you used the local-copy method above, delete that seed file once the container's been added:
rm /boot/config/plugins/dockerMan/templates-user/brain-mcp.xmlWhen you click Apply on Add Container, Unraid saves a second file with
your actual values — my-brain-mcp.xml, alongside the blank one you
downloaded — and both declare the same container name. With two templates
claiming that name, Update can end up recreating the container from the
blank original instead of your saved one, wiping BRAIN_NAME/BRAIN_TOKEN/
the paths and leaving it unable to start. Once my-brain-mcp.xml exists
(check ls /boot/config/plugins/dockerMan/templates-user/), the seed file
has done its job and isn't needed — remove it so there's no ambiguity.
Clicking Update afterward pulls whatever's newest on
ghcr.io/sipho102/brain-mcp:latest using your saved config, as expected.
Serving a second vault
One container serves one vault — there's deliberately no multi-vault
service list in docker-compose.yml, and no multi-vault form in the
Unraid template either. On the Unraid-app path above, that just means
running Add Container again from the same template with a different
name/paths/token/port. On the Compose path, copy this deployment directory
(or just docker-compose.yml + .env) elsewhere, fill in that copy's
.env with a different BRAIN_NAME,
BRAIN_VAULT_PATH, BRAIN_TOKEN, and PORT, and run docker compose up -d --build from there too. Same image (brain-mcp:latest), independent
containers.
Container user / permissions
The container runs as a non-root user, UID:GID 99:100 by default (Unraid's
nobody:users) — override at build time with BRAIN_UID/BRAIN_GID in
.env if your share needs different ownership. This user must have write
access to 00-inbox/ on the host share.
Connecting a client
Claude Code
claude mcp add --transport http --scope user brain \
http://<unraid-host>:3100/mcp \
--header "Authorization: Bearer <token>"Then /mcp in a session should list all six tools.
Known quirk: Claude Code has had recurring bugs where headers set via
--header aren't sent during session establishment, producing 401s even
though curl with the same token works fine. If you hit that, write the
headers object directly into the JSON config instead
(~/.claude/mcp_servers.json or the relevant scope file):
{
"mcpServers": {
"brain": {
"type": "http",
"url": "http://<unraid-host>:3100/mcp",
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}(type also accepts streamable-http as an alias for http in JSON
configs.)
opencode
opencode attempts OAuth discovery on remote MCP servers by default and will ignore a static bearer token unless you disable that explicitly:
{
"mcp": {
"brain": {
"type": "remote",
"url": "http://<unraid-host>:3100/mcp",
"oauth": false,
"headers": {
"Authorization": "Bearer <token>"
}
}
}
}Without oauth: false, opencode will try (and fail) an OAuth handshake
instead of using the header.
Clients with no header field at all
Some MCP-client UIs only take a name, transport, and URL — no way to set a
custom Authorization header. For those, put the token in the URL instead:
http://<unraid-host>:3100/mcp?token=<token>The server checks the Authorization header first and falls back to a
?token= query parameter, so this works anywhere the header-based config
above does too. Worth knowing before you rely on it: a token in a URL can
end up in more places than a header would — the client's saved config, a
browser's history if the URL is ever opened directly, shell history if
you've pasted it into a terminal. Access logs aren't a concern here
(uvicorn's access log is off), but treat the URL itself as carrying the
secret, the same as you would the token itself.
Tools
Six tools, kept deliberately small (tool schemas cost client context):
brain_structure()— orientation: PARA folders + counts, live enums fromCONVENTIONS.md, frontmatter schema, full conventions text, note count. Call this first in a session.search_notes(query, domain, type, status, para, tag, limit)— full-text search (ripgrep) with frontmatter filtering. Returns metadata + a ~200 char snippet, never full bodies.read_note(identifier)— full note by vault-relative path, fulluid, or an unambiguousuidprefix (≥8 chars).list_notes(para, domain, status, type, limit)— metadata-only browsing, no content search.get_backlinks(identifier)— notes that link to this one, with the context line.capture(title, body, domain, tags, source, links)— the only write: creates a new note in00-inbox/. Never overwrites, never touches anything outside the inbox.
What this deliberately doesn't do
No semantic search/embeddings, no write access outside 00-inbox/, no
Obsidian Local REST API dependency (reads the filesystem directly), no
paperless-ngx document fetching (returns document IDs from frontmatter for a
client to chain to a separate paperless MCP server), no git operations. See
brain-mcp-spec.md §2 for the reasoning.
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 Servers
- AlicenseAqualityAmaintenanceA generic Markdown vault MCP server with FTS5 full-text search, semantic vector search, frontmatter-aware indexing, incremental reindexing, and non-markdown attachment support that exposes search, read, write, and edit tools.3831MIT
- AlicenseNot gradedqualityCmaintenanceExposes an Obsidian notes vault as MCP services, enabling AI assistants to search, read, create, update, and delete notes and folders.241MIT
- AlicenseNot gradedqualityBmaintenanceExposes a personal markdown-based second brain (Obsidian-style) as an MCP server, enabling agents to search, read, and write notes with privacy controls.MIT
- AlicenseNot gradedqualityAmaintenanceMCP server that turns a Markdown folder (e.g. Obsidian vault) into a second brain, capturing readings and ideas, connecting them as concepts, and resurfacing related notes on demand.235MIT
Related MCP Connectors
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
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/sipho102/brain-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server