Markdown Vault MCP Server

active
python

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 .md files

Works With

  • Obsidian — point VAULT_PATH at 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 .md files

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

ValueAgent can…
hiddenNothing — file is completely invisible
readView but not modify
appendAdd content only (default for notes without frontmatter)
editFreely edit

Tool Permission Requirements

ToolRequired agent_access
get_note, search_notes, list_notesread or higher (invisible if hidden)
append_to_noteappend or higher (default if no frontmatter)
update_noteedit
replace_in_noteedit
update_sectionedit

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 append
  • add_timestamp (boolean, optional, default: true) — if true, inserts a ## YYYY-MM-DD HH:MM heading 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 capture
  • append_to_note("books.md", "- The Expanse", false) — add a list item without a heading
  • append_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 in templates/.
  • 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’s agent_access frontmatter 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 root
  • old_text (string, required) — exact text to find
  • new_text (string, required) — replacement text

Errors:

  • "Text not found in note: '...'" — if old_text doesn’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 root
  • heading (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 ingredients
  • MEETING_NOTES.md — Meeting notes with attendees array (agent_access: "edit")
  • REFERENCE_DOC.md — Reference document with dynamic agent_access via {{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 instructionappend
”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:

PlaceholderExample 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 (overrides VAULT_PATH env)
  • --note — specific note path to test get_note with
  • --query — search term for search_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.