Skip to main content

notes-vault-mcp

An MCP server for a vault of markdown notes — the kind Obsidian keeps: a folder of .md files with YAML frontmatter. The vault lives either in a local directory or in an S3 bucket (MinIO included), and the server gives an agent a cheap, indexed way to read and write it.

The point is that an agent should be able to answer "what do we already know about this?" in one call, and should be told when the vault has drifted away from the code. So the server does more than read and write files:

  • A local SQLite index. Every tool call refreshes it, fetching only the notes whose version changed. Search never downloads the vault.
  • Full-text search with BM25 ranking, folder weights, recency decay and a status factor, so the living system note outranks a two-year-old archived plan on the same words.
  • A schema. The frontmatter contract lives in the vault as .vault/schema.yml: which folders exist and what each is for, which fields are required, which statuses and kinds are legal, which folders must link an area. Writes are validated against it and refused when they do not hold.
  • A lifecycle. close archives a finished note and stamps its status; log_append writes one dated line per repo per session; lint reports every kind of drift it can see.
  • Session hooks for Claude Code: session-start hands the agent the system notes for the repo it is about to touch — plus the commits made since each note was last updated — and stop refuses to end a session that left commits unlogged or notes stale.

Swedish or English notes both work: the index folds diacritics, and the schema carries a synonym list so bokning finds booking.

Install

As a Claude Code plugin

claude plugin marketplace add https://github.com/gronare/claude-plugins
claude plugin install vault@gronare

The plugin asks for the vault settings and passes them as CLAUDE_PLUGIN_OPTION_* environment variables, which this server reads as if they were the bare names.

As an MCP server, straight from PyPI

claude mcp add vault -s user \
  -e VAULT_PATH=$HOME/vault \
  -- uvx notes-vault-mcp

Or against S3 / MinIO:

claude mcp add vault -s user \
  -e S3_ENDPOINT=https://minio.example.com \
  -e S3_ACCESS_KEY=... \
  -e S3_SECRET_KEY=... \
  -e S3_BUCKET=vault \
  -- uvx notes-vault-mcp

As a container

claude mcp add vault -s user -- \
  docker run --rm -i \
  -e S3_ENDPOINT -e S3_ACCESS_KEY -e S3_SECRET_KEY -e S3_BUCKET \
  ghcr.io/gronare/notes-vault-mcp:latest

Configuration

Every variable is also read from CLAUDE_PLUGIN_OPTION_<NAME>, which is how the Claude Code plugin passes its user config. The bare name wins when both are set.

Variable Required Meaning
VAULT_PATH for a local vault Directory holding the vault. Selects the local backend.
S3_ENDPOINT for an S3 vault Endpoint URL, e.g. https://minio.example.com.
S3_ACCESS_KEY for an S3 vault Access key.
S3_SECRET_KEY for an S3 vault Secret key.
S3_BUCKET for an S3 vault Bucket holding the vault.
S3_PREFIX no Key prefix inside the bucket.
S3_REGION no Region, default us-east-1.
VAULT_CACHE_DIR no Where the index lives, default ~/.cache/notes-vault-mcp.
VAULT_SCHEMA no Local path to a schema file, overriding the one in the vault.
VAULT_TOKEN for HTTP Bearer token. Required by --transport http.
VAULT_STOP_HOOK no off disables the stop hook.

Set VAULT_PATH or the four S3_* variables. With neither, the server exits with one line saying so.

First run

uvx notes-vault-mcp init

init writes into the vault, and refuses to overwrite anything without --force:

  • .vault/schema.yml — the frontmatter contract, copied from the built-in default so you can edit it.
  • Areas.base, Open tasks.base, Resources.base — Obsidian Bases views over the same structure.

It then prints a CLAUDE.md snippet to stdout: the workflow rules an agent needs on its side of the conversation.

The schema

.vault/schema.yml is deep-merged over the built-in default, so it only needs to carry what differs. The default lays out five folders:

Folder Kind Weight Role
Areas/ system 3.0 One living note per system. Current state only. The hubs of the graph.
Resources/ reference 2.0 Traps, how-tos and decisions with their reasons.
Projects/ task 1.0 Open work spanning sessions. Closed with close.
Log/ log 1.0 Append-only log per repo, one note per repo.
Archive/ archive 0.3 History. Searched only on request.

and the contract for a note:

frontmatter:
  required: [title, date, updated, tags, status]
  optional: [kind, area, summary, path, superseded_by]
  area_required_in: [Projects, Resources, Log]
  status_values: [draft, active, complete, superseded]
  kind_values: [system, task, trap, howto, decision, reference, log]

path is what ties a note to code: a comma-separated list of directories (~ is kept as written and also indexed expanded). That is what context and the session hook match against.

The repo log is one note per repo, and both its filename and its line format are schema settings:

log:
  folder: Log
  file_format: "{repo}-log.md"
  entry_format: "- [{date}] {line} | commits: {commits} | {area}"

file_format takes a single {repo} placeholder, and the default suffix is what keeps the log clear of the hub note: with Areas/greenhouse.md and Log/greenhouse.md both in the vault, Obsidian cannot resolve [[greenhouse]]. Every place that builds the log path reads this setting — log_append, the log tail in context, the stop hook's unlogged-commit check, changelog and lint — so changing it moves all of them at once. Rename the existing files to match when you change it.

Also configurable: the tag vocabulary and whether it is enforced, the synonym groups search expands, stale_after_days, and the search weights.

Tools

Every call refreshes the index first, throttled to at most once every 20 seconds.

Tool Cost What it does
search cheap Full-text over the index. Title, summary, tags and body, with synonyms, prefixes, quoted phrases and folded diacritics. A bare commit sha finds the notes that mention it. Hides archive and superseded notes and says how many.
context cheap The session-start call: the system notes covering a path, the open tasks, the reference notes and the tail of the repo log, in one answer.
list_files cheap Paths only.
read_file moderate One note, prefixed with etag: <version>. A superseded note carries a warning callout.
lint moderate Reads every note and reports drift.
write_file write Validates against the schema and refuses the write if it does not hold. Stamps updated, fills date. Pass expected_etag to make the write conditional.
append_file write Appends and bumps updated. Creates the note when missing.
close write Sets status complete (or superseded, with superseded_by, when merged_into is given) and moves the note into the archive.
log_append write One dated line in the repo log, with the commits it produced. Creates the log when missing.
move_file write Moves or renames.
delete_file write Deletes for good. Prefer close.

search filters: folder, status, tag, kind, area, path_prefix, since, include_archive, include_superseded, limit.

What lint reports

broken_frontmatter, missing_required (per field), missing_area, unknown_tags (only when the vocabulary is strict), unresolved_links, orphans (no inbound wikilink; log and archive ignored), stale_active, archive_status_mismatch, duplicate_stems, superseded_target_missing.

uvx notes-vault-mcp lint
uvx notes-vault-mcp lint --write "Log/lint-$(date +%F).md"

Hooks

Two Claude Code hooks, both reading the hook JSON on stdin and both exiting 0 whatever happens.

session-start prints the context bundle for the working directory, then — for each system note it returned — the commits touching that note's path since the note was last updated. That is the answer to "is this note still true?" before the agent believes it.

stop blocks the end of a session that left work unrecorded: commits from the last 24 hours whose sha does not appear in the repo log, and open task notes older than 14 days. It returns {"decision": "block", "reason": ...}, or nothing at all when the vault is up to date. Set VAULT_STOP_HOOK=off to silence it.

{
  "hooks": {
    "SessionStart": [
      { "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook session-start" }] }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "uvx notes-vault-mcp hook stop" }] }
    ]
  }
}

Other commands

notes-vault-mcp serve --transport stdio          # the default
notes-vault-mcp sync --rebuild                   # drop the index and read every note again
notes-vault-mcp search "bokning" --limit 5       # the same ranking, from a shell
notes-vault-mcp changelog greenhouse 2026-08 --repo-path ~/projects/greenhouse

changelog prints the log lines, the git commits grouped by day, and the notes dated inside the period. It writes nothing; it is input for an agent compiling a month page.

HTTP transport

VAULT_TOKEN=$(openssl rand -hex 32) notes-vault-mcp serve --transport http --host 0.0.0.0 --port 8765

Streamable HTTP on /mcp. Every request must carry Authorization: Bearer $VAULT_TOKEN; anything else gets 401 before it reaches the server. VAULT_TOKEN is mandatory in this mode — the command refuses to start without it.

Development

uv sync
uv run pytest
uv run ruff check .

The test suite runs against a fixture vault under tests/fixtures/vault/ and a moto-mocked S3 bucket. It never touches a real bucket.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

notes_vault_mcp-0.2.0.tar.gz (118.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

notes_vault_mcp-0.2.0-py3-none-any.whl (37.1 kB view details)

Uploaded Python 3

File details

Details for the file notes_vault_mcp-0.2.0.tar.gz.

File metadata

  • Download URL: notes_vault_mcp-0.2.0.tar.gz
  • Upload date:
  • Size: 118.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for notes_vault_mcp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 0da041c81fc5a7f794ddecc46bbcf09365fe587e3c7b29ce448e6ed82fde8416
MD5 4728aec54c58ac6b5efab0cc6bc51875
BLAKE2b-256 dc2977b9d3cb0ddecdbe7a84b1d6ad7345256ee63bf8b7f2969e930c0887167b

See more details on using hashes here.

Provenance

The following attestation bundles were made for notes_vault_mcp-0.2.0.tar.gz:

Publisher: release.yml on gronare/notes-vault-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file notes_vault_mcp-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for notes_vault_mcp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88d6d1850e9679ed6f75c6a88bfb60bdc4f5bcd43a4442d4eb6f0b5e85fc3801
MD5 83490c806f1222a3697d4d499be22c94
BLAKE2b-256 7b01c95d2710db0cb5b7165efa42766582c2cb5496246779dc7b1644f84877a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for notes_vault_mcp-0.2.0-py3-none-any.whl:

Publisher: release.yml on gronare/notes-vault-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.6

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

This release

0.2.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page