# π Markdown Vault MCP Server
**Headless semantic [MCP](https://modelcontextprotocol.io/) server for Obsidian, Logseq, Dendron, Foam, and any folder of markdown files.**
`npm install` and point it at a folder. Hybrid search, AST editing, zero-config embeddings. No app, no plugins, no API keys.
[](https://github.com/wirux/mcp-markdown-vault/actions/workflows/release.yml)
[](https://github.com/wirux/mcp-markdown-vault/actions/workflows/pr-check.yml)
[](https://www.npmjs.com/package/@wirux/mcp-markdown-vault)
[](https://github.com/wirux/mcp-markdown-vault/pkgs/container/mcp-markdown-vault)
[](LICENSE)
[](https://www.typescriptlang.org/)
[](https://nodejs.org/)
[](#-testing)
[](https://glama.ai/mcp/servers/wirux/mcp-markdown-vault)

---
## π‘ Why this server?
> **TL;DR** β One `npx` command. No running app. No plugins. No vector DB. Semantic search works out of the box.
| | Differentiator | Details |
|---|---|---|
| π« | **No app or plugins required** | Most Obsidian MCP servers ([mcp-obsidian](https://github.com/MarkusPfundstein/mcp-obsidian), [obsidian-mcp-server](https://github.com/cyanheads/obsidian-mcp-server)) need Obsidian running with the Local REST API plugin. This server reads and writes `.md` files directly β point it at a folder and go. |
| π§ | **Built-in semantic search, zero setup** | Hybrid search: cosine-similarity vectors + TF-IDF + word proximity. Local embeddings (`@huggingface/transformers`, `all-MiniLM-L6-v2`, 384d) download on first run. No API keys, no external services. Ollama optional for higher quality. |
| π¬ | **Surgical AST-based editing** | `remark` AST pipeline patches specific headings or block IDs without touching the rest of the file. Freeform line-range & string replace as fallback. Levenshtein fuzzy matching handles LLM typos. |
| π | **Tool-agnostic** | Obsidian vaults, Logseq graphs, Dendron workspaces, Foam, or any plain folder of `.md` files. If it's markdown, it works. |
| π¦ | **Single package, no infrastructure** | Unlike Python alternatives that need ChromaDB or other vector stores, everything runs in one Node.js process. `npx @wirux/mcp-markdown-vault` and you're running. Docker image available. |
π **Obsidian** Β· π **Logseq** Β· π³ **Dendron** Β· π«§ **Foam** Β· π **Any `.md` folder**
---
## β¨ Features
| | Feature | Description |
|---|---|---|
| ποΈ | **Headless vault ops** | Read, create, update, edit, delete `.md` notes with strict path traversal protection |
| π | **Read by heading** | Read a single section by heading title β returns only content under that heading (up to the next same-level heading), saving context window space |
| π¦ | **Bulk read** | Read multiple files and/or heading-scoped sections in a single call β reduces MCP round-trips with per-item fault tolerance |
| π¬ | **Surgical editing** | AST-based patching targets specific headings or block IDs β never overwrites the whole file |
| π | **Fragment retrieval** | Heading-aware chunking + TF-IDF + proximity scoring returns only relevant sections |
| π | **Scoped search** | Optional directory filter for `global_search` and `semantic_search` β restrict results to specific folders to reduce noise |
| π§ | **Semantic search** | Hybrid vector + lexical search with background auto-indexing |
| β‘ | **Zero-setup embeddings** | Built-in local embeddings via `@huggingface/transformers` β Ollama optional |
| π | **Workflow tracking** | Petri net state machine with contextual LLM hints |
| π | **Dual transport** | Stdio (single client) or SSE over HTTP (multi-client, Docker-friendly) |
| βοΈ | **Freeform editing** | Line-range replacement and string find/replace as AST fallback |
| π·οΈ | **Frontmatter management** | AST-based read and update of YAML frontmatter β safely manage tags, statuses, and metadata without corrupting file structure |
| π | **Dry-run / diff preview** | Preview any edit operation as a unified diff without saving β set `dryRun=true` on any edit action |
| π | **Templating / scaffolding** | Create new notes from template files with `{{variable}}` placeholder injection β refuses to overwrite existing files |
| πΊοΈ | **Self-orienting vault context** | Assisted or manual `meta/overview.md` with host-visible `vault_scope` and live `vault://overview` context for connected agents |
| π¦ | **Batch edit** | Apply multiple edit operations in a single call β sequential execution, stops on first error, supports `dryRun`, max 50 ops |
| π | **Backlinks index** | Find all notes linking to a given path β supports wikilinks and markdown links with line numbers and context snippets |
| π― | **Typo resilience** | Levenshtein-based fuzzy matching for edit operations |
---
## π οΈ MCP Tools
| Tool | Actions | Description |
|---|---|---|
| π **vault** | `list` `read` `create` `update` `delete` `stat` `create_from_template` | Full CRUD for vault notes + template scaffolding |
| βοΈ **edit** | `append` `prepend` `replace` `delete` `line_replace` `string_replace` `frontmatter_set` + `operations[]` batch mode | AST-based patching + freeform fallback + frontmatter update + batch edit (supports `dryRun` diff preview) |
| ποΈ **view** | `search` `global_search` `semantic_search` `outline` `read` `frontmatter_get` `bulk_read` `backlinks` | Fragment retrieval, cross-vault search, hybrid semantic search, read by heading, frontmatter read, bulk read, backlinks |
| π **workflow** | `status` `transition` `history` `reset` | Petri net state machine control |
| βοΈ **system** | `status` `reindex` `overview` `overview_status` `prepare_overview` `save_overview` | Server health, indexing info, vault structure overview, assisted overview rebuild |
> All tool responses include contextual hints based on the current workflow state.
---
## π‘ Operational Guidance
### π οΈ Safe Editing
- **`dryRun=true`**: Highly recommended before destructive operations like `delete` or `replace` with `replaceMode="section"`.
- **Heading Disambiguation**: If multiple identical headings are found, the server returns `AMBIGUOUS_HEADING_TARGET` with a list of candidates. Use `blockId` to target specific elements if headings are not unique.
- **AST vs Freeform**: Always prefer AST operations (`append`, `prepend`, `replace`, `delete`) as they are structural. Use `string_replace` only as a last resort; it requires exact literal matches including whitespace and newlines.
- **`replaceMode`**: `replace` defaults to `body` (preserves the heading, replaces content). Set `replaceMode: "section"` to replace the heading node and all its child headings.
- **`returnContent`**: Set to `section` or `file` to see the results of your edit immediately in the tool response (max 8KB).
### π Performance & Consistency
- **`bulk_read`**: Use this to read 2 or more files/sections concurrently. It is significantly faster than multiple sequential `view.read` calls.
- **Workflow State**: The `workflow` tool manages session-specific state used for contextual hints. It does **not** modify vault data or search indexes.
- **`system.reindex`**: Only use this for recovery or after making out-of-band file changes (e.g., via external scripts). Normal MCP edits automatically update backlinks and queue vector indexing.
- **`view.outline`**: Supports a `directory` parameter to get a flat list of headings across multiple files in a folder.
### π§ͺ Batch Edits
- **Sequential Execution**: Operations in a batch are executed one by one. If one fails, the remaining are skipped.
- **Dry-run Asymmetry**: In `dryRun=false`, each operation sees the file state after previous operations. In `dryRun=true`, the file is never written, so sequential dependent operations (e.g., editing the same line twice) may produce different results than a live run.
---
## π Quick Start
### Prerequisites
- [Node.js](https://nodejs.org/) >= 22
- *(Optional)* [Ollama](https://ollama.com/) for higher-quality embeddings
### π¦ Install from NPM
```bash
npm install -g @wirux/mcp-markdown-vault
```
Then run directly:
```bash
VAULT_PATH=/path/to/your/vault markdown-vault-mcp
```
### π MCP Client Configuration
Add to your MCP client config (e.g. Claude Desktop, Claude Code):
```json
{
"mcpServers": {
"markdown-vault": {
"command": "npx",
"args": ["-y", "@wirux/mcp-markdown-vault"],
"env": {
"VAULT_PATH": "/path/to/your/vault"
}
}
}
}
```
> `npx -y` auto-installs the package if not already present β no global install needed.
> **Try it in the browser:** You can test this server directly at [Glama Inspector](https://glama.ai/mcp/servers/@wirux/mcp-markdown-vault) β no local install required.
### π³ Docker
Pull the pre-built multi-arch image from GitHub Container Registry:
```bash
docker pull ghcr.io/wirux/mcp-markdown-vault:latest
```
Or use Docker Compose:
```bash
docker compose up
```
Edit `docker-compose.yml` to point at your markdown vault directory. The default compose file uses SSE transport on port 3000.
### π οΈ Development (from source)
```bash
git clone https://github.com/wirux/mcp-markdown-vault.git
cd mcp-markdown-vault
npm install
npm run build
VAULT_PATH=/path/to/your/vault node dist/index.js
```
---
## π Transport Modes
| Mode | Use case | How it works |
|---|---|---|
| π‘ `stdio` *(default)* | Single-client desktop apps (Claude Desktop) | Reads/writes stdin/stdout; 1:1 connection |
| π `sse` | Multi-client setups (Docker, Claude Code) | HTTP server with SSE streams; one connection per client |
**SSE** starts an HTTP server on `PORT` (default `3000`):
- `GET /sse` β establishes an SSE stream (one per client)
- `POST /messages?sessionId=...` β receives JSON-RPC messages
```bash
MCP_TRANSPORT_TYPE=sse PORT=3000 VAULT_PATH=/path/to/vault npx @wirux/mcp-markdown-vault
```
Each SSE client gets its own workflow state. Shared resources (vault, vector index, embedder) are reused across all connections.
---
## π§ Embedding Providers
The server selects an embedding provider automatically:
| `OLLAMA_URL` set? | Ollama reachable? | Provider used |
|---|---|---|
| β No | β | π Local (`@huggingface/transformers`, `all-MiniLM-L6-v2`, 384d) |
| β
Yes | β
Yes | π¦ Ollama (`nomic-embed-text`, 768d) |
| β
Yes | β No | π Local *(fallback with warning)* |
> No configuration needed for local embeddings β the model downloads on first use and is cached automatically.
---
## βοΈ Configuration
| Variable | Default | Description |
|---|---|---|
| `VAULT_PATH` | `/vault` | Markdown vault directory |
| `VAULT_CONTEXT_MODE` | `assisted` | Vault orientation mode: `assisted` (host LLM/agent calls `prepare_overview` to gather evidence, then generates prose and calls `save_overview`) or `manual` (you author `meta/overview.md` yourself and the server does not overwrite it). `auto` is a deprecated alias for `assisted`. |
| `VAULT_CONTEXT` | *(deprecated)* | Deprecated and ignored. Use `VAULT_CONTEXT_MODE` instead. |
| `MCP_TRANSPORT_TYPE` | `stdio` | `stdio` (single client) or `sse` (multi-client HTTP) |
| `PORT` | `3000` | HTTP port (SSE mode only) |
| `OLLAMA_URL` | *(unset)* | Set to enable Ollama embeddings |
| `OLLAMA_MODEL` | `nomic-embed-text` | Ollama embedding model name |
| `OLLAMA_DIMENSIONS` | `768` | Ollama embedding vector dimensions |
| `VECTOR_STORE_URL` | *(unset)* | Set to use Qdrant (e.g. `http://localhost:6333`). If unset, local persisted flat store is used. |
| `VECTOR_STORE_COLLECTION` | `markdown_vault` | Qdrant collection name when `VECTOR_STORE_URL` is set. |
| `VECTOR_STORE_RESET` | `false` | Set to `true` to auto-delete a mismatched vector index on startup and rebuild from scratch. |
| `MCP_AUTH_TOKEN` | *(unset)* | Bearer token for SSE transport auth. If set, all SSE endpoints require `Authorization: Bearer `. |
| `HOST_BIND_ADDRESS` | `127.0.0.1` | Bind address for the SSE HTTP server. |
| `BODY_LIMIT_BYTES` | `1mb` | Max JSON request body size for SSE `POST /messages`. |
> **Note**: When using the default local vector store, a `.markdown_vault_mcp` directory will be created in your vault. It's recommended to add this directory to your `.gitignore`.
Use `assisted` mode when you want the connected host LLM/agent to generate and refresh vault context from server-provided evidence. Use `manual` mode when you want to write and maintain `meta/overview.md` yourself; in manual mode, the server creates the file if missing but does not overwrite it.
---
## ποΈ Architecture
Clean Architecture with strict layer separation:
```
src/
βββ domain/ π· Errors, interfaces (ports), value objects
βββ use-cases/ πΆ Business logic (AST, chunking, search, workflow)
βββ infrastructure/ π’ Adapters (file system, Ollama, vector store)
βββ presentation/ π£ MCP tool bindings, transport layer (stdio/SSE)
```
See [CLAUDE.md](CLAUDE.md) for detailed architecture docs and [CHANGELOG.md](CHANGELOG.md) for implementation history.
---
## πΊοΈ Self-Orienting Context Layer
Connected agents automatically discover **when** to query this vault and **how** to use its tools β no explicit user instructions needed.
> **Quick start:** Run the **`rebuild-overview`** MCP prompt after adding notes to your vault. This generates context that helps agents route queries to the right vault.
### How it works
The server delivers vault context through multiple mechanisms (graceful degradation across clients):
| Mechanism | When | What the agent sees |
|---|---|---|
| `instructions` field | MCP handshake | `vault_scope` + tool summary |
| MCP Resources | On-demand | `vault://overview` (stats + overview + conventions) |
| First-call priming | First tool call per session | `vault_scope` + hint to read `vault://overview` |
| Tool descriptions | Tool listing | `vault_scope` string for routing |
### Modes
| Mode | How overview is managed |
|---|---|
| `assisted` *(default)* | Host agent calls `system.prepare_overview` β generates prose β calls `system.save_overview` |
| `manual` | You author `meta/overview.md` yourself; server creates stub but never overwrites |
To rebuild in assisted mode: invoke the **`rebuild-overview`** MCP prompt, or ask your agent to call `system.prepare_overview` then `system.save_overview`.
### Vault meta files
On first startup, the server creates two files in `/meta/`:
| File | Purpose | Managed by |
|---|---|---|
| `meta/overview.md` | Vault description + `vault_scope` routing hint | Host agent (assisted) or you (manual) |
| `meta/contract.md` | Tool usage conventions (frontmatter schema, search hints, naming) | Created once, never overwritten |
> **Tip:** Keep `vault_scope` short and specific β it tells MCP hosts what information this vault can answer.
---
## π’ CI/CD & Release
Fully automated via GitHub Actions and [Semantic Release](https://semantic-release.gitbook.io/):
| Workflow | Trigger | What it does |
|---|---|---|
| **PR Check** | Pull request to `main` | Lint β Build β Test |
| **Release** | Push to `main` | Lint β Test β Semantic Release (NPM + GitHub Release) β Docker build & push to `ghcr.io` |
- Versioning follows [Conventional Commits](https://www.conventionalcommits.org/) β `feat:` = minor, `fix:` = patch, `feat!:` / `BREAKING CHANGE:` = major
- Docker images are built for `linux/amd64` and `linux/arm64` via QEMU
- NPM package published as [`@wirux/mcp-markdown-vault`](https://www.npmjs.com/package/@wirux/mcp-markdown-vault)
- Docker image available at [`ghcr.io/wirux/mcp-markdown-vault`](https://github.com/wirux/mcp-markdown-vault/pkgs/container/mcp-markdown-vault)
---
## π§ͺ Testing
**568 tests** across 49 files, written test-first (TDD).
```bash
npm test # Run all tests
npx vitest run src/use-cases/ast-patcher.test.ts # Single file
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
```
> Tests use real temp directories for file system operations and in-memory MCP transport for integration tests. No external services required.
---
## π Security
- π‘οΈ All file paths validated through `SafePath` value object before any I/O
- π« Blocks path traversal: `../`, URL-encoded (`%2e%2e`), double-encoded (`%252e`), backslash, null bytes
- βοΈ Atomic file writes (temp file + rename) prevent partial writes
- π€ Docker container runs as non-root user
---
## π License
[MIT](LICENSE)