Skip to main content
Glama
mikesusz

markdown-vault-mcp

by mikesusz

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).

Related MCP server: 50 First Tapes MCP Server

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 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 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"
		}
	]
}

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 warnings key 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 in warnings — 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 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 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 (overrides VAULT_PATH env)

  • --note — specific note path to test get_note with

  • --query — search term for search_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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to save, search, and manage markdown-based research articles through a complete CRUD interface. Supports creating, reading, updating, and deleting articles with frontmatter metadata in a self-hosted file-based system.
    2
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables AI agents to read, write, append, and delete content in Markdown files using structural selectors, without regex or string hacking.
    7
    7 npm
    5
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to securely read and write to an Obsidian-compatible Markdown vault with per-agent access control, audit logging, and conflict resolution.
    Apache 2.0