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.
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`)
{
"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, 2026markdown-notes-mcp
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-mcpindex, etc. are skipped by search/list (a top-level dot file like.draft.mdstays visible). - File-size cap — reads/writes above
NOTES_MAX_FILE_BYTES(default 10 MB) are refused. - Read-only switch —
NOTES_READ_ONLY=truedisables all writes. - Write allowlist — optional
NOTES_WRITE_SUBDIRSrestricts 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
| Variable | Default | Meaning |
|---|---|---|
NOTES_VAULT_PATH | required | Absolute path to the vault root. |
NOTES_READ_ONLY | false | Global kill-switch disabling all writes. |
NOTES_MAX_FILE_BYTES | 10485760 | Per-file size cap (bytes). |
NOTES_WRITE_SUBDIRS | (unset) | Comma-separated subfolders; when set, writes are allowed only within them. |
NOTES_SEMANTIC_ENABLED | true | Enable the semantic-search tools (no effect without the [semantic] extra). |
NOTES_EMBED_MODEL | BAAI/bge-small-en-v1.5 | FastEmbed model for embeddings. For Spanish/multilingual vaults, use paraphrase-multilingual-MiniLM-L12-v2. |
NOTES_INDEX_PATH | <vault>/.notes-mcp/index.sqlite | Location 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
| Tool | Purpose |
|---|---|
search_notes | Full-text search (substring/regex), with line numbers + context. |
search_by_frontmatter | Query by tags / created,updated,date ranges / folders. |
list_notes | List notes with size + mtime. |
read_note | Content, frontmatter, merged tags, outgoing links, stat. Filename-only wikilink resolution. |
create_note | Create a note; refuses to clobber unless overwrite=true. |
update_note | Replace a whole note, or one heading section (PATCH-style). |
append_to_note | Append content; can create if missing. |
list_tags | Every tag with note counts (hierarchical parents included). |
get_backlinks | Notes that link to a given note. |
get_related_notes | Notes connected via shared tags + in/out links (set use_semantic=true to also blend in embedding similarity). |
delete_note | Write-gated delete; requires confirm=true, backs up first. |
semantic_search | Meaning-based retrieval via local embeddings (see below). |
reindex_vault | Build/update the semantic index (force=true rebuilds). |
index_status | Inspect 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 withsemantic_search, answer with citednote://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 asBLOB+ 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
mtimeand 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 fromsearch_notes/list_notesand 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_MODELwith an existing index is detected (model/dims mismatch) and refused with a guiding error rather than returning garbage — runreindex_vault(force=true)to rebuild. NOTES_SEMANTIC_ENABLED=falseis a kill-switch:semantic_search/reindex_vaultrefuse with a clear message, whileindex_statusstays inspectable ("enabled": false).
Markdown conventions
- Wikilinks:
[[Note]],[[Note|Alias]],[[Note#Heading]],![[embed]]. - Markdown links:
[text](relative/path.md)(external/httplinks are ignored for the graph). - Tags: inline
#tagand frontmattertags:(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 viaruamel.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 whenonnxruntime/fastembedaren'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.