Skip to main content
Glama
obsidian-secondbrain-mcp

obsidian-secondbrain-mcp

by alex106

obsidian-secondbrain-mcp

A local-only MCP server that turns an Obsidian vault into a working second brain. No network calls, no API keys, no embeddings, no model calls anywhere in this package. Every tool either moves bytes on local disk or hands raw material back to the agent with an explicit next_step — the thinking is the agent's job, and the results come back through the same tools.

Install

cd obsidian-mcp
python -m venv .venv
.venv/Scripts/python.exe -m pip install -e .          # Windows
# .venv/bin/python -m pip install -e .                # macOS/Linux

# register with Claude Code (server -> ~/.claude.json, hooks -> ~/.claude/settings.json;
# both backed up first)
.venv/Scripts/python.exe scripts/install.py --vault "C:/path/to/YourVault"

Then restart Claude Code and run /mcp to confirm obsidian-secondbrain is connected. Add --dry-run to see the config without writing it, --no-hooks to skip automatic session capture.

First thing in a fresh vault: ask the agent to call init_vault.

Related MCP server: Obsidian MCP Assistant

Quick setup: the secbrain-init skill

Copy skills/secbrain-init/ into ~/.claude/skills/, restart Claude Code, and run /secbrain-init in any project. It creates or reuses a vault there, registers the server and hooks for that project, fires the capture hook and verifies it wrote a note, then reports.

The skill drives the CLI below rather than editing config by hand, so setup is tested code and behaves the same every time.

CLI

PY=.venv/Scripts/python.exe    # or .venv/bin/python

$PY -m obsidian_secondbrain.cli detect    --project .   # OS, runtime, which client
$PY -m obsidian_secondbrain.cli doctor    --project .   # what is / isn't set up
$PY -m obsidian_secondbrain.cli init      --project .   # create or reuse the vault
$PY -m obsidian_secondbrain.cli install   --project .   # register server + hooks
$PY -m obsidian_secondbrain.cli test-hook --project .   # fire the hook, verify, clean up

Each prints one JSON object and exits non-zero on failure. init and install are idempotent: existing notes are never touched, config files are backed up before rewriting, unrelated entries are preserved, and re-running replaces this hook rather than stacking duplicates.

Which clients work

The server is plain stdio MCP, so any MCP client can use all 27 tools; the vault is plain markdown that Obsidian itself reads. What differs is the automation around it.

Claude Code

Codex

Copilot / VS Code

Cursor

27 MCP tools

yes

yes

yes

yes

capture_session (manual compact)

yes

yes

yes

yes

MCP prompts

yes

varies

yes

yes

Automatic capture hooks

yes

yes

no

no

/secbrain-init skill

yes

no

no

no

Automatic capture now works on both Claude Code and Codex, but not the same way. Claude Code's hook parses the PreCompact/SessionEnd transcript directly. Codex documents its rollout transcript format as unstable, so its hooks avoid it: UserPromptSubmit/Stop buffer each turn's stable fields (the prompt, the final assistant message) to <vault>/.secondbrain/buffer/<session_id>.jsonl, and PreCompact/SessionEnd turn that buffer into the same kind of inbox note. Codex also skips every hook until it is reviewed and trusted in /hooks — install and doctor both say so, and re-trust is needed after any re-install that changes a command. Copilot and Cursor have neither event, so capture stays manual there — capture_session is an ordinary tool, so an agent writing its own summary and filing it works anywhere. install --with-instructions writes .github/copilot-instructions.md / .cursor/rules/secbrain.md for those two; Codex gets AGENTS.md by default now, the same way Claude Code gets CLAUDE.md, no flag needed.

Reading the vault at the start of a session

Two mechanisms cover this, and they are deliberately not the same thing.

A SessionStart hook (hooks/session_start.py, Claude Code and Codex) fires unconditionally on every session start/resume/clear/compact and injects a small, passive digest as additionalContext — the undistilled backlog count and the tail of the most recent log. It never searches anything; it can't, because the user's actual request doesn't exist yet at that point.

install also writes a marked block into the project's rule file — CLAUDE.md for Claude Code, AGENTS.md for Codex, with no flag needed for either — whose first instruction is: before acting on the first request of a session, search the vault. This is the active half: a targeted search_notes/find_notes once the request is known, which a hook firing before any user text exists structurally cannot do.

Every client gets the rule, Claude Code and Codex included, because a capture hook only writes the vault when a session ends — without the rule (or, on top of the hook's passive digest), the vault only ever fills up, and a session starts by re-deriving what is already written down. doctor reports project_rules.has_session_start_rule and hooks.missing (which lists any of PreCompact/SessionEnd/SessionStart not yet registered, plus UserPromptSubmit/Stop on the Codex path), and --no-instructions opts out of the rule file specifically.

detect identifies the client from evidence and distinguishes an agent signal (we are running as it) from a host signal (it is merely the editor hosting the terminal) — Claude Code inside a VS Code terminal makes both look active.

$PY -m obsidian_secondbrain.cli install   --project . --client codex  --scope user
$PY -m obsidian_secondbrain.cli install   --project . --client vscode --with-instructions
$PY -m obsidian_secondbrain.cli install   --project . --client all
$PY -m obsidian_secondbrain.cli test-hook --project . --client codex
$PY -m obsidian_secondbrain.cli doctor    --project . --client codex

test-hook and doctor default to --client claude; pass --client codex to check the Codex-side wiring (hooks.json, the turn buffer, AGENTS.md) instead. Neither command can see whether Codex has actually trusted the hooks in /hooks — that state lives only in Codex, keyed by each hook's hash — so both report the caveat explicitly rather than implying a passing check means capture is live.

Config shapes are not interchangeable, and a wrong shape is silently ignored with no error: VS Code uses root key servers and requires "type": "stdio"; Claude Code, Copilot and Cursor use mcpServers; Codex uses TOML [mcp_servers.NAME]. The Codex writer patches only its own table so comments and other servers survive.

install --scope project (default) writes .mcp.json plus .claude/settings.local.json — hooks go in the local file because they embed absolute machine paths. --scope user registers one vault globally instead.

test-hook is the one that matters: it writes a synthetic transcript, runs the command exactly as registered with OBSIDIAN_VAULT stripped from the environment — what a real session gives a hook — asserts seven properties of the note it produced, then restores the vault byte-for-byte.

That last detail is not pedantry. A hook does not inherit the env block of an MCP server config, so a hook registered without --vault finds no vault, exits 0 and silently writes nothing. install therefore passes the vault on the command line, and test-hook runs the registered command under a stripped environment so a no-op hook cannot pass.

Configuration

Exactly one input: the OBSIDIAN_VAULT environment variable, set in the MCP server config. Everything else — folder names, date formats, the frontmatter key that marks a note distilled — lives in <vault>/.secondbrain/config.json, created by init_vault. Reshape the method by editing that file; no code changes needed.

{
  "folders": {
    "inbox": "00-Inbox", "log": "10-Log", "sessions": "20-Sessions",
    "notes": "30-Notes", "maps": "40-Maps", "archive": "90-Archive"
  },
  "daily_note_format": "%Y-%m-%d",
  "distilled_key": "distilled",
  "exclude": [".obsidian", ".trash", ".git", ".secondbrain"]
}

The method

Raw capture is append-only and never rewritten. Distillation is a separate, deliberate pass that turns raw material into atomic notes — one idea each, titled as a claim, linked into the existing graph. Review fights the write-only vault.

INJECT   →  the SessionStart hook (Claude Code, Codex) + the context-first rule
CAPTURE  →  log_entry, append_to_note, the PreCompact/SessionEnd hooks
COMPACT  →  capture_session   (agent writes the summary, server files it)
DISTILL  →  distill_queue → create_concept_note → mark_distilled
REVIEW   →  vault_health, resurface_notes, related_notes, stale_notes

Tools (27)

Vault — vault_info, init_vault, list_notes, read_note, create_note, append_to_note, patch_section, update_frontmatter, move_note, archive_note

Find & search — find_notes (by name/glob), search_notes (inside content, literal or regex), search_by_tag (nested tags match), search_frontmatter, backlinks, related_notes (scored graph neighbours with reasons), recent_notes

Session capture — capture_session, log_entry, read_daily_log

Distillation & review — distill_queue, create_concept_note, mark_distilled, vault_health, build_map, resurface_notes, stale_notes

Prompts

compact_to_vault · distill · review_brain — surfaced as slash commands in Claude Code once the server is connected.

Session capture, and why the hook files raw text

capture_session is the /compact step: the agent writes the durable summary (what, why, decisions, open questions, artifacts) and the server files it as a session note marked undistilled, plus a pointer in the day's log.

The PreCompact/SessionEnd hook runs outside the model, so it cannot summarise anything. It files raw material — user prompts and assistant text only, no tool spam, most recent ~20k chars — into the inbox as an undistilled note. On Claude Code that material is the transcript at transcript_path. On Codex, whose transcript format is documented as unstable, it is instead whatever hooks/codex_turn_buffer.py already appended from UserPromptSubmit.prompt and Stop.last_assistant_message — two fields Codex does treat as stable — to <vault>/.secondbrain/buffer/<session_id>.jsonl; the buffer is consumed (deleted) once its note is written, so a PreCompact capture followed by a SessionEnd capture never files the same turns twice. The next distill_queue call hands the note to the agent, which does the thinking. Automatic capture without ever needing a model behind the server's back.

The SessionStart hook (hooks/session_start.py) is the same idea run in reverse: it can't summarise either, so it just counts and quotes — the undistilled backlog size and the tail of the latest log — and hands that back as additionalContext. Injection without a model in the hook, same as capture.

Every hook always exits 0. A failed capture never blocks your session. On Codex this matters doubly: SessionEnd gets 1 s by default and 3 s at the hard ceiling, so its capture only ever has to rename a buffer file into a note — the actual buffering already happened earlier, in UserPromptSubmit and Stop, where there's no shared time budget to blow.

Safety

Every path is resolved and checked against the vault root, so ../ escapes are refused. archive_note moves rather than deletes; nothing in this package deletes a note. Tool errors come back as {"error": ..., "message": ...} data so the agent can recover instead of the call blowing up.

Layout

src/obsidian_secondbrain/
  config.py    vault path + data-driven taxonomy
  vault.py     safe paths, frontmatter, read/append/patch, wikilinks
  search.py    content, tag and frontmatter search
  ops.py       find by name, move/archive, related, recent/stale/resurface
  capture.py   daily log, session notes
  distill.py   distill queue, concept notes, health, maps
  server.py    MCP tool + prompt surface
  cli.py       detect / doctor / init / install / test-hook bootstrap
  environment.py  evidence-based OS + client detection
  clients.py      per-client config writers (json / toml shapes)
hooks/capture_session.py    PreCompact / SessionEnd raw capture (Claude Code + Codex)
hooks/session_start.py      SessionStart digest injection (Claude Code + Codex)
hooks/codex_turn_buffer.py  Codex UserPromptSubmit / Stop: buffers each turn's stable fields
hooks/_common.py            shared vault resolution + the Codex buffer path, for all hook scripts
scripts/install.py          user-scope registration (~/.claude.json + settings.json)
skills/secbrain-init/       Claude Code skill: /secbrain-init
tests/test_lifecycle.py     34 assertions over a real stdio MCP client
tests/test_hook_wiring.py   proves test-hook rejects an env-only hook
tests/test_clients.py       pins each client's config shape
tests/test_codex_hooks.py   install/test-hook/doctor --client codex, end to end

Available Tools

27 tools
append_to_noteA

Append to a note, creating it if absent.

The append-only primitive: prefer this over rewriting raw capture notes.

Inputs: path (required), text (required), heading.

archive_noteA

Move a note into the archive folder. Nothing is ever deleted.

Inputs: path (required).

build_mapB

Write a Map-of-Content index note over notes you have curated.

Inputs: topic (required), note_paths (required), intro.

capture_sessionA

Persist the current session into the vault -- the /compact step.

YOU write the summary; this tool only files it. Before calling, compact the session yourself into: what was being done and why, decisions taken with their reasons, what is still open, which files/artifacts were touched. Write durable facts, not a transcript -- a reader six months from now should not need the conversation.

Creates a note in the sessions folder marked undistilled, and drops a pointer into today's log.

Inputs: title (required), summary (required), tags, project, +4 more.

create_concept_noteA

Write one atomic note: one idea, stated as a claim.

title should read as an assertion you could disagree with, not a topic label. claim is the idea in one or two sentences. links are titles of existing notes -- call find_notes or search_notes first so you link into the graph instead of creating an island.

Inputs: claim (required), title (required), body, tags, +3 more.

create_noteC

Create a note. Refuses to clobber an existing note unless overwrite=true.

Inputs: body (required), path (required), overwrite, frontmatter.

distill_queueA

Raw captures not yet distilled, oldest first, with their content.

This is the core loop and it is YOUR job, not the server's: read each item, pull out the ideas that will still matter later, write each one as a single atomic note via create_concept_note (linking to existing notes with [[Title]]), then call mark_distilled on the source listing what you produced. Skip anything ephemeral -- mark it distilled with an empty produced list.

Inputs: limit, include_body.

find_notesA

Find notes by NAME or path. */? globs work; a bare word matches any name containing it.

Use this when you know roughly what a note is called. Use search_notes when you need to look inside the content.

Inputs: pattern (required), limit, folder.

init_vaultA

Create the folder taxonomy and config file in an empty or new vault.

Idempotent: existing folders and notes are left untouched.

Inputs: seed_readme.

list_notesB

List note paths, optionally restricted to one folder.

Inputs: limit, folder, with_frontmatter.

log_entryA

Append a timestamped entry to the day's append-only log.

For in-flight capture: a fact, a link, a half-formed idea. Never rewrite the log -- it is the raw stream distillation feeds on.

Inputs: text (required), date, source.

mark_distilledA

Mark a raw capture as distilled, recording which notes came out of it.

Inputs: path (required), produced.

move_noteC

Move or rename a note inside the vault.

Inputs: src (required), dest (required), overwrite.

patch_sectionA

Insert text at the end of the section under heading, adding the section at the end of the note if it is missing.

Inputs: path (required), text (required), heading (required).

read_daily_logB

Read one day's log (ISO date, default today).

Inputs: date.

read_noteB

Read one note: frontmatter, body, tags and outgoing links.

Inputs: path (required).

recent_notesC

Notes modified in the last N days, newest first.

Inputs: days, limit, folder.

resurface_notesC

Random notes pulled up for re-reading -- the anti-write-only-vault move.

Re-read them, then either link them somewhere new, sharpen the claim, or archive them if they no longer hold.

Inputs: count, folder, with_body.

search_by_tagB

Find notes carrying a tag (frontmatter tags or inline #tag). Nested tags match: ml also matches ml/rl.

Inputs: tag (required), limit.

search_frontmatterA

Find notes by a frontmatter key, optionally matching an exact value.

Inputs: key (required), limit, value.

search_notesA

Literal or regex search across note CONTENT, with surrounding lines.

Plain text matching, no embeddings. Read the excerpts, judge relevance yourself, then read_note the ones that matter.

Inputs: query (required), limit, regex, folder, +1 more.

stale_notesC

Notes untouched for N+ days, oldest first.

Inputs: days, limit, folder.

update_frontmatterB

Merge keys into a note's YAML frontmatter.

Inputs: path (required), updates (required).

vault_healthA

Where the graph is fraying: undistilled backlog, orphan notes, unresolved [[links]], most-connected hubs, per-folder counts.

Use it to pick the next maintenance job -- an unresolved link is usually a note worth writing; an orphan is usually a note worth linking.

Inputs: no named parameters.

vault_infoA

Describe the vault: root path, folder taxonomy, note counts, method.

Call this first in a session so you know the structure you are writing into. The taxonomy is data (.secondbrain/config.json), not code.

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.4/5.0

    Scored across 27 tools

    Disambiguation5/5

    Every tool targets a distinct operation: listing paths vs. searching by name/content/tag/frontmatter, editing via distinct primitives (create/append/patch/frontmatter), and graph-specific tools (related, backlinks, stale, recent). The search tools are clearly separated by what they search (name vs. content vs. tag vs. frontmatter), minimizing confusion. Even the distillation workflow tools (capture_session, log_entry, distill_queue, mark_distilled) each have non-overlapping responsibilities.

    Naming Consistency4/5

    Most tools follow verb_noun snake_case (create_note, move_note, search_notes, build_map). A few are noun-first (vault_info, vault_health, backlinks) and one adjective_noun (distill_queue). The convention is predominantly consistent, and any deviations are clear from context. The minor inconsistency keeps it from a perfect score.

    Tool Count4/5

    At 27 tools, this exceeds the typical 15-tool threshold, but the server manages a full Personal Knowledge Management system: CRUD, multiple search modes, graph analytics, session capture, distillation workflow, health checks, and map building. Each tool addresses a distinct need, so the count feels justified rather than bloated. It is on the heavy side but appropriate for the scope.

    Completeness5/5

    The tool surface is remarkably complete for a second-brain vault: full note lifecycle (create, read, append, patch, move, archive without deletion), comprehensive search (name, content, tag, frontmatter), graph operations (backlinks, related, stale, recent), a distillation pipeline (capture, log, distill, mark_distilled), and maintenance utilities (health, map, resurface). The only missing operation is permanent deletion, which is a deliberate design choice. This is a fully realized workflow.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      A
      maintenance
      Turns your Obsidian vault into an MCP-enabled workspace with tools for reading/writing notes, managing folders, running semantic searches, and maintaining long-term memory—all while keeping data local to your vault.
      309,216 npm
      158
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables local hybrid search over Obsidian and Markdown vaults via MCP, combining vector retrieval, full-text search, reranking, graph navigation, and safe CRUD while keeping data local.
      43
      1
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      A local Obsidian Vault authoring MCP for building high-quality knowledge inputs for OKC, enabling note creation, search, and audit without the Obsidian app.
      MIT