Markdown Vault MCP Server
Agent-controlled markdown files with frontmatter-based permissions.
Works with Obsidian vaults, static site generators (Hugo, Jekyll, Astro), note-taking apps (Bear, Typora, iA Writer), or any folder of .md files.
Note: if you want a simple, read-only version with no file-write capabilities, see the 1.0 Release.
Quick Start
# 1. Clone and install
git clone https://github.com/mikesusz/markdown-vault-mcp
cd markdown-vault-mcp
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
# 2. Set your markdown folder path
cp .env.example .env
# Edit .env and set VAULT_PATH=/path/to/your/markdown/files
# 3. Add to your MCP client configuration
MCP Client Config (Claude Desktop, etc.):
{
"mcpServers": {
"markdown-vault": {
"command": "/path/to/markdown-vault-mcp/.venv/bin/python",
"args": ["-m", "markdown_vault_mcp.server"],
"env": {
"VAULT_PATH": "/path/to/your/markdown/files"
}
}
}
}
Use the full path to the Python binary inside your virtualenv. The VAULT_PATH can be set in .env or passed via the client config (client takes precedence).
Requirements
- Python 3.10+
- A folder containing
.mdfiles
Works With
- Obsidian — point
VAULT_PATHat your vault folder - Hugo / Jekyll / Astro — point at your
content/folder - Bear / Typora / iA Writer — point at your notes folder
- Plain markdown — point at any folder of
.mdfiles
The server doesn’t care about your note-taking app — it just needs markdown files with optional YAML frontmatter.
Permission System
Control agent access by adding agent_access to any note’s frontmatter:
---
agent_access: edit # Agent can freely edit
agent_access: append # Agent can only add content (safe default)
agent_access: read # Agent can view but not modify
agent_access: hidden # Completely invisible to agents
---
Notes without frontmatter default to append. No configuration files. No hardcoded lists. Just frontmatter.
Permission Hierarchy
| Value | Agent can… |
|---|---|
hidden | Nothing — file is completely invisible |
read | View but not modify |
append | Add content only (default for notes without frontmatter) |
edit | Freely edit |
Tool Permission Requirements
| Tool | Required agent_access |
|---|---|
get_note, search_notes, list_notes | read or higher (invisible if hidden) |
append_to_note | append or higher (default if no frontmatter) |
update_note | edit |
replace_in_note | edit |
update_section | edit |
If a note lacks the required permission, edit tools return:
Error: Insufficient permissions. This note has agent_access: 'append', but this operation requires: 'edit'. Add agent_access: 'edit' to the note's frontmatter to enable this operation.
Available Tools
search_notes
Search markdown notes by title or content (case-insensitive). Returns up to 10 results with a short excerpt around each match.
Input:
query(string, required) — term to search for
Example result:
[
{
"title": "Meeting Notes",
"path": "work/Meeting Notes.md",
"snippet": "…discussed the new project architecture…",
"relevance_score": 7.0
}
]
get_note
Retrieve the full content and metadata of a specific note.
Input:
note_path(string, required) — path relative to vault root, e.g."folder/My Note.md"
Example result:
{
"title": "My Note",
"path": "folder/My Note.md",
"content": "# My Note\n\nBody text here…",
"frontmatter": { "tags": ["idea"], "created": "2024-01-01" },
"modified": "2024-06-15T10:30:00",
"size": 1024
}
list_notes
List all markdown notes in the vault (or a subfolder).
Input:
folder(string, optional) — subfolder path relative to vault root
Example result:
[
{ "title": "Home", "path": "Home.md", "size": 512, "modified": "2024-06-01T09:00:00" },
{
"title": "Daily Note",
"path": "Daily/2024-01-15.md",
"size": 256,
"modified": "2024-01-15T08:00:00"
}
]
list_writable_notes
Show all notes that agents can append to — those with agent_access: append or edit, plus notes without any frontmatter (which default to append).
Input: none
Example result:
{
"writable_notes": [
{ "path": "__INBOX.md", "access_level": "append" },
{ "path": "projects/status.md", "access_level": "edit" }
]
}
append_to_note
Append content to the end of a note. Works on any note with agent_access: append or edit, or any note without frontmatter. Never overwrites existing content. Creates the file if it doesn’t exist yet.
Input:
note_path(string, required) — path relative to vault root, e.g."inbox.md"or"folder/notes.md"content(string, required) — text to appendadd_timestamp(boolean, optional, default:true) — if true, inserts a## YYYY-MM-DD HH:MMheading before the content
Example result:
{
"success": true,
"note_path": "inbox.md",
"appended_content": "\n## 2026-03-24 10:30\nOrder furnace filters\n",
"message": "Successfully appended to 'inbox.md'."
}
Example usage:
append_to_note("inbox.md", "Order furnace filters")— timestamped captureappend_to_note("books.md", "- The Expanse", false)— add a list item without a headingappend_to_note("new-note.md", "Some content", false)— creates the file if it doesn’t exist
list_templates
List all .md files in your vault’s templates/ directory.
Input: none
Example result:
{
"templates": [
{
"name": "PROJECT",
"path": "templates/PROJECT.md",
"size": 100,
"description": "General project template"
},
{
"name": "JOURNAL",
"path": "templates/JOURNAL.md",
"size": 96,
"description": "Daily journal entry"
}
]
}
create_note_from_template
Create a new note from an existing template. Automatically replaces placeholders like {{TODAY}} and {{AGENT_ACCESS}}, and pre-fills frontmatter or heading-based fields.
Input:
template_name(string, required) — name of the template (e.g."PROJECT","New Book"). Must match a file intemplates/.note_suffix(string, optional) — appended to the filename, e.g."deck replacement"→"PROJECT deck replacement.md". Letters, numbers, spaces, hyphens, underscores only.field_values(object, optional) — pre-fill template fields. Keys match YAML frontmatter field names (e.g.{"title": "My Book", "authors": "Jane Smith"}). Keys not found in frontmatter will be matched against# KEY:headings in the body instead.agent_access(string, optional) — permission level for agent access after creation:"edit","append"(default),"read", or"hidden". Overrides any value already in the template’sagent_accessfrontmatter field. Infer from the user’s phrasing (see Templates section below).
Example result:
{
"success": true,
"file_path": "PROJECT deck replacement.md",
"message": "Created new note from template",
"template_used": "templates/PROJECT.md",
"fields_applied": { "title": "deck replacement" }
}
update_note
Replace the entire body of a note with new content. Frontmatter (including agent_access) is always preserved. Requires agent_access: "edit" in the note.
Input:
note_path(string, required) — path relative to vault root, e.g."My Note.md"new_content(string, required) — full replacement body text
replace_in_note
Find and replace a specific piece of text in a note body. Exact match, case-sensitive. Only operates on body content — frontmatter is never touched. Requires agent_access: "edit".
Input:
note_path(string, required) — path relative to vault rootold_text(string, required) — exact text to findnew_text(string, required) — replacement text
Errors:
"Text not found in note: '...'"— ifold_textdoesn’t appear in the body
update_section
Replace the content beneath a specific heading, preserving the heading line itself. Content is replaced up to (but not including) the next heading, or end of file. Requires agent_access: "edit".
Input:
note_path(string, required) — path relative to vault rootheading(string, required) — exact heading text, e.g."## Next Steps"or"SYNOPSIS:"new_content(string, required) — replacement content for that section
Errors:
"Heading '...' not found in note"— if the heading doesn’t exist
Templates
Adding templates
Drop any .md file into your vault’s templates/ directory — no configuration needed:
templates/
├── PROJECT.md
├── New Book.md
├── JOURNAL.md
└── RECIPE.md
Add an optional description comment at the top to make it show up nicely in list_templates:
%% My recipe template %%
---
date: "{{TODAY}}"
source: ""
---
# TITLE:
# SOURCE:
## Ingredients
## Steps
The templates/examples/ directory in this repo contains ready-to-use templates you can copy into your vault:
JOURNAL.md— Daily journal with date auto-filled (agent_access: "append")RECIPE.md— Recipe with source and ingredientsMEETING_NOTES.md— Meeting notes with attendees array (agent_access: "edit")REFERENCE_DOC.md— Reference document with dynamicagent_accessvia{{AGENT_ACCESS}}
Agent access in templates
Templates can include an agent_access frontmatter field to declare how freely an agent should edit the note after creation.
Hardcoded in template (e.g. JOURNAL.md always appends):
---
date: '{{TODAY}}'
agent_access: 'append'
---
Dynamic via placeholder (e.g. REFERENCE_DOC.md — set at creation time):
---
title: '{{TITLE}}'
agent_access: '{{AGENT_ACCESS}}'
---
When using the dynamic placeholder, pass agent_access to create_note_from_template and the value is inferred from the user’s phrasing:
| User says… | Inferred value |
|---|---|
| ”you can edit/update/modify” or “fully editable” | edit |
| ”you can add to” or “append-only” | append |
| No specific instruction | append |
| ”read-only”, “I’ll edit this myself”, “just create it” | read |
| ”this is private”, “keep this hidden”, “don’t show this” | hidden |
If agent_access is passed explicitly to create_note_from_template, it overrides whatever value the template has (hardcoded or placeholder).
Template placeholders
The following {{PLACEHOLDER}} tokens are automatically replaced when a note is created:
| Placeholder | Example output |
|---|---|
{{TODAY}} | 2026-03-24 |
{{DATE}} | 2026-03-24 (alias for TODAY) |
{{NOW}} | 2026-03-24 15:30:45 |
{{TIME}} | 15:30:45 |
{{TIMESTAMP}} | 1742820645 (Unix timestamp) |
{{YEAR}} | 2026 |
{{MONTH}} | 03 |
{{DAY}} | 24 |
{{AGENT_ACCESS}} | "append" (resolved from the agent_access parameter, default "append") |
Placeholders are expanded after field_values are applied, so caller-supplied values always take precedence.
Array fields
If a field_values value for an array-typed frontmatter field contains and, it’s automatically split into a list:
"William Gibson and Bruce Sterling" → ["William Gibson", "Bruce Sterling"]
Testing
Run the end-to-end test harness (requires a real vault):
python test_server.py --vault /path/to/your/vault
Options:
--vault— path to vault (overridesVAULT_PATHenv)--note— specific note path to testget_notewith--query— search term forsearch_notes(default:"the")
Troubleshooting
VAULT_PATH environment variable is not set
Copy .env.example to .env and set the path, or pass VAULT_PATH in your MCP client config.
Templates directory not found in vault
Create a templates/ folder in your vault root, or copy templates from templates/examples/ in this repo.
Insufficient permissions
The note has agent_access: read or hidden. Add agent_access: append or agent_access: edit to its frontmatter.
Changes to server code aren’t taking effect Your MCP client keeps the server process alive. Restart the client (e.g. quit and relaunch Claude Desktop) to pick up code changes.
field_values not updating frontmatter
Check that the key names exactly match the YAML frontmatter keys in the template (case-sensitive). Use fields_applied in the response to confirm what was actually written.
Running the server manually
python -m markdown_vault_mcp.server
# or
markdown-vault-mcp
The server communicates over stdio and is normally launched automatically by your MCP client.
Migrating from obsidian-mcp v2
See MIGRATION.md for upgrade steps.
License
GNU Affero General Public License v3.0 — see LICENSE for the full text.
You are free to use, modify, and distribute this software under the AGPL v3 terms. If you run a modified version as a network service, you must make the modified source available to users of that service.
Feedback
This project is a work in progress, and may have bugs. You can submit a Github Issue if you encounter any problems, and I will probably fix it! Because I don’t want to have that problem, either.