使用说明
markdown-vault-mcp
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 with a single frontmatter field. Notes without frontmatter default to append — no configuration files, no hardcoded lists.
Permission Hierarchy
| Value | Agent can… |
|---|---|
hidden |
Nothing — file is completely invisible |
read |
View but not modify |
append |
Add content only (default if no frontmatter) |
edit |
Full read + write access |
Usage
Add agent_access to any note's frontmatter:
---
agent_access: edit # or: append, read, hidden
---
Tool Requirements
| Tools | Minimum Permission |
|---|---|
| Read operations | read |
append_to_note |
append |
| Edit operations | edit |
Read operations: get_note, search_notes, list_notes
Edit operations: update_note, replace_in_note, update_section
Notes with insufficient permissions return a clear error:
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:
{
"results": [
{
"title": "Meeting Notes",
"path": "work/Meeting Notes.md",
"snippet": "…discussed the new project architecture…",
"relevance_score": 7.0
}
]
}
Plus a warnings array if any files had to be skipped.
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
}
If the note's frontmatter can't be parsed, get_note returns a structured error naming the problem instead of the note:
{
"error": "Could not parse this note's frontmatter",
"detail": "YAML parse error: mapping values are not allowed in this context (line 3)",
"path": "Reference - House.md"
}
list_notes
List all markdown notes in the vault (or a subfolder).
Input:
folder(string, optional) — subfolder path relative to vault root
Example result:
{
"notes": [
{ "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"
}
]
}
Plus a warnings array if any files had to be skipped.
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" }
]
}
Plus a warnings array if any files had to be skipped.
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"
}
]
}
Plus a warnings array if any files had to be skipped.
Skipped files
Every vault-scanning tool — list_notes, search_notes, list_writable_notes,
and list_templates — skips any file it can't read rather than failing the whole
scan. When that happens, it reports the skipped files alongside the normal
results:
{
"notes": [ "…normal results, unchanged…" ],
"warnings": [
{
"path": "Reference - House.md",
"error": "YAML parse error: mapping values are not allowed in this context (line 3)"
}
]
}
- The
warningskey is absent entirely when nothing was skipped — it never appears as an empty array. - Each entry carries only the path and the parse error. A note's title, content, frontmatter, and access level are never included, since an unparseable note may well have been meant to stay hidden.
- Skipped files are also logged to stderr, visible in your MCP client's logs.
- Hidden notes (
agent_access: hidden) are excluded silently and never appear inwarnings— that's a deliberate setting, not a problem to report.
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")
Run the robustness suite, which builds a throwaway vault full of empty and malformed notes and asserts that no single bad file breaks vault-wide operations (no real vault needed):
python test_vault_robustness.py
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.
A note is missing from list_notes or search_notes
A note whose YAML frontmatter can't be parsed is skipped rather than exposed,
so one bad file never breaks results for the rest of the vault. The response
names it in a warnings array (and the server logs it to
stderr) — fix the --- block at the top of that file and it reappears.
get_note on the same file returns an explicit error naming the problem.
If a note is missing with no warning, check whether it's agent_access: hidden.
Empty (0-byte) .md files
Handled normally — treated as a note with no frontmatter and no content, which
means the default agent_access: append. They appear in list_notes and
list_writable_notes, never match a search, and don't affect other notes.
Frontmatter with a non-string agent_access
Values like agent_access: [append] or agent_access: true resolve to an
unknown level: the note stays visible but is not writable. Quote the value or
use one of hidden / read / append / edit.
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.
