Skip to content
Conduit

mcp server

markdown-notes-mcp

App-agnostic MCP server over a local folder of Markdown notes (Obsidian/Logseq/Foam/plain) with optional 100% local semantic search. No plugin required.

QuantumBBoy/markdown-notes-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "quantumbboy-markdown-notes-mcp": {
      "command": "uvx",
      "args": [
        "markdown-notes-mcp"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 23, 2026

markdown-notes-mcp

CI

An app-agnostic MCP server over a local folder of Markdown notes. Works with any Markdown vault — Obsidian, Logseq, Foam, or plain .md files — because it reads and writes the files directly. No app plugin or REST API required (unlike mcp-obsidian, which needs the Obsidian Local REST API plugin).

Your vault is "just a folder." Point the server at it and let an LLM search your knowledge base, read and summarise notes, create daily/meeting notes, update or append to existing notes, and traverse links, tags, and frontmatter — all locally and privately.

Safety model

The vault is a sandbox. Every file operation is constrained:

  • Vault root — all access is confined to NOTES_VAULT_PATH.
  • Path-traversal prevention — every resolved path must stay inside the vault; .., absolute paths, and escaping symlinks are rejected.
  • .md-only — non-Markdown files are not accessible.
  • Dot-directories ignored — notes inside .git, .obsidian, Obsidian's .trash, the .notes-mcp index, etc. are skipped by search/list (a top-level dot file like .draft.md stays visible).
  • File-size cap — reads/writes above NOTES_MAX_FILE_BYTES (default 10 MB) are refused.
  • Read-only switch — NOTES_READ_ONLY=true disables all writes.
  • Write allowlist — optional NOTES_WRITE_SUBDIRS restricts writes to specific folders.
  • Round-trip safety — frontmatter is re-serialised and re-parsed before writing, prior bytes are backed up to <name>.md.bak, writes are atomic (temp file + rename), and null bytes are stripped.

Configuration

VariableDefaultMeaning
NOTES_VAULT_PATHrequiredAbsolute path to the vault root.
NOTES_READ_ONLYfalseGlobal kill-switch disabling all writes.
NOTES_MAX_FILE_BYTES10485760Per-file size cap (bytes).
NOTES_WRITE_SUBDIRS(unset)Comma-separated subfolders; when set, writes are allowed only within them.
NOTES_SEMANTIC_ENABLEDtrueEnable the semantic-search tools (no effect without the [semantic] extra).
NOTES_EMBED_MODELBAAI/bge-small-en-v1.5FastEmbed model for embeddings. For Spanish/multilingual vaults, use paraphrase-multilingual-MiniLM-L12-v2.
NOTES_INDEX_PATH<vault>/.notes-mcp/index.sqliteLocation of the persistent vector index.

The vault path can also be passed as --vault-path, and --read-only forces read-only mode.

Install & run

uv sync
uv run notes-mcp --vault-path /path/to/your/vault
# or: NOTES_VAULT_PATH=/path/to/vault uv run notes-mcp

New here? Point it at the bundled examples/demo-vault (5 notes) and follow examples/README.md to try literal vs. semantic search, backlinks, and the RAG prompt without touching your own notes.

Claude Desktop

See examples/claude_desktop_config.json. Add the server to your Claude Desktop config with the vault path in env.

Inspect locally

npx @modelcontextprotocol/inspector uv run notes-mcp --vault-path /path/to/vault

Tools

ToolPurpose
search_notesFull-text search (substring/regex), with line numbers + context.
search_by_frontmatterQuery by tags / created,updated,date ranges / folders.
list_notesList notes with size + mtime.
read_noteContent, frontmatter, merged tags, outgoing links, stat. Filename-only wikilink resolution.
create_noteCreate a note; refuses to clobber unless overwrite=true.
update_noteReplace a whole note, or one heading section (PATCH-style).
append_to_noteAppend content; can create if missing.
list_tagsEvery tag with note counts (hierarchical parents included).
get_backlinksNotes that link to a given note.
get_related_notesNotes connected via shared tags + in/out links (set use_semantic=true to also blend in embedding similarity).
delete_noteWrite-gated delete; requires confirm=true, backs up first.
semantic_searchMeaning-based retrieval via local embeddings (see below).
reindex_vaultBuild/update the semantic index (force=true rebuilds).
index_statusInspect the index: counts, model, dims, path, last build.

Resources

  • note://{path} — direct read of a note by vault-relative path.
  • vault://index — generated index (titles + tags) of the whole vault.

Search/list/read results include note://... resource links so an agent can open a hit without re-searching.

Prompts

  • daily_note_summary(date) — synthesise notes touched on a date.
  • find_orphans() — notes with no backlinks or outgoing links.
  • weekly_review(tags) — review recent notes carrying given tags.
  • ask_my_notes(question) — RAG: retrieve with semantic_search, answer with cited note:// sources.
  • find_forgotten(topic) — resurface notes semantically close to a topic but edited long ago.

Semantic search (optional)

search_notes finds literal matches — but you rarely remember the exact words you wrote. semantic_search indexes your notes with local embeddings and retrieves by meaning, so a query like "decisions under uncertainty" can surface a note titled "betting with incomplete information" even though they share no words:

semantic_search(query="decisions under uncertainty", top_k=5)
→ [{ "path": "betting-incomplete-info.md", "heading": "Rational wagering",
      "score": 0.78, "excerpt": "...", "note": "note://betting-incomplete-info.md" }, ...]

It's the RAG pattern, applied to your own private notes.

100% local

Embeddings run via FastEmbed — an ONNX model on CPU. No network, no API keys, no credentials. Same privacy-first stance as the rest of the server. The default model BAAI/bge-small-en-v1.5 (384 dims) is fast and English-first; for Spanish/multilingual vaults set NOTES_EMBED_MODEL=paraphrase-multilingual-MiniLM-L12-v2.

Install

uv sync --extra semantic           # or: pip install "markdown-notes-mcp[semantic]"

Without the extra, the rest of the server works unchanged and the semantic tools return a clear install hint instead of failing obscurely — onnxruntime is only pulled in if you want RAG.

How it works (design decisions)

  • Vector store: one SQLite file, not pgvector. The index lives at <vault>/.notes-mcp/index.sqlite (vectors as BLOB + metadata). pgvector would demand a running Postgres and break the "it's just a folder" model. For a personal vault (hundreds to a few thousand notes), a brute-force cosine scan with numpy is instantaneous — an ANN index (FAISS/hnswlib) would be over-engineering at this scale. SQLite gives transactional, single-file, per-note invalidation.
  • Chunking by heading. Long notes are split along Markdown headings (with a token cap + small overlap) so each section gets its own focused embedding; results point at the relevant section, not just the file.
  • Persistent, incremental indexing. Unlike the literal scan (free on demand), embeddings are expensive — so the index is persistent and updated only for notes whose mtime and content hash changed. Re-embedding the whole vault on every query would be unworkable; this is a deliberate, documented departure from the rest of the server's on-demand model.

Index lifecycle

semantic_search reindexes changed notes automatically (auto_index=true). Use reindex_vault for a big first build or after editing many notes outside Claude, and index_status to inspect state:

  • The .notes-mcp/ directory is excluded from search_notes / list_notes and respects the vault sandbox.
  • In read-only mode the index is never written: if it's missing, the tool says so instead of writing; if it's stale, it searches what exists and reports stale.
  • Changing NOTES_EMBED_MODEL with an existing index is detected (model/dims mismatch) and refused with a guiding error rather than returning garbage — run reindex_vault(force=true) to rebuild.
  • NOTES_SEMANTIC_ENABLED=false is a kill-switch: semantic_search / reindex_vault refuse with a clear message, while index_status stays inspectable ("enabled": false).

Markdown conventions

  • Wikilinks: [[Note]], [[Note|Alias]], [[Note#Heading]], ![[embed]].
  • Markdown links: [text](relative/path.md) (external/http links are ignored for the graph).
  • Tags: inline #tag and frontmatter tags: (list or comma/space string). Hierarchical tags (project/active) also count toward their parents (project).
  • Frontmatter: YAML between leading --- fences, round-trip-safe (key order + comments preserved via ruamel.yaml).

Development

uv sync --extra dev --extra semantic
uv run pytest

The semantic tests are skipped automatically if the [semantic] extra isn't installed.

Continuous integration

.github/workflows/ci.yml runs on every push to main and every pull request, in two jobs:

  • core — the suite across Python 3.10–3.13 without the [semantic] extra. The semantic tests skip themselves, so this job proves the server runs and degrades gracefully when onnxruntime / fastembed aren't installed.
  • semantic — the full suite with the extra on Python 3.12, so the embeddings path actually executes (chunking, the SQLite store, real FastEmbed retrieval). The ONNX model download is cached between runs.

Both jobs install from the committed uv.lock with --locked, so a stale lockfile fails CI. After changing dependencies, run uv lock and commit the result.

License

MIT — see LICENSE.

↑↓ navigate openesc close