Obsidian MCP Server

Source-linked search and explicit writes to a Markdown vault.

A Python MCP server that returns paths and line numbers, reads original notes and makes overwrite behavior explicit. Local integration through stdio.

Python · MCP · Markdown · Docker

Role

Project author: filesystem service, MCP tools, testing and Docker packaging.

Open source · Python · MCP. This case examines published revision 4ac3c0f3. Its primary workflow uses a local stdio connection.

Notes as evidence an agent can cite

An Obsidian vault already has useful structure: Markdown, folders and meaningful filenames. I built an MCP server so an agent can find notes, read them and prepare new material while preserving that structure. This workflow requires neither moving the vault into a separate database nor placing every document in the initial prompt.

My contribution covers the file service, six MCP tools and packaging. The agent chooses operations and composes an answer; the server performs concrete filesystem work. That boundary makes it possible to inspect the source of a quotation and identify the file a proposed edit would change.

Local workflow boundaries
  1. AI clientUser request and tool selection
  2. MCP serverPython, stdio and VaultService
  3. Markdown vaultFiles and folders mounted into the container

The server does not select a model. The client receives note contents; using a cloud model can send that text to its provider.

Find a decision, then inspect its source

Consider a request to find the database decision for a history feature. When the folder is known, vault_glob identifies matching paths. vault_search finds text across the vault, and vault_read opens the selected document. A matching line locates evidence; the agent still needs the surrounding context.

{
  "name": "vault_search",
  "arguments": {"query": "PostgreSQL"}
}
{
  "matches": [{
    "path": "Projects/decision.md",
    "line": 2,
    "content": "Use PostgreSQL for history."
  }],
  "total_files": 2
}

This result came from a check using two synthetic notes. The second line of Projects/decision.md contains the displayed text. total_files counts processed files, not matches. Line numbers are one-based. These details let the agent point to something the reader can verify.

Three implementation choices

Separate filesystem behavior from MCP

VaultService implements read, write, listing, glob, tree and text search. Thin MCP handlers pass arguments and return results. Filesystem behavior can be checked with temporary directories without an AI client. Missing-file errors become short messages that do not expose absolute host paths.

Use inspectable text search

Search reads Markdown and returns the path, line number and matching content. It supports case-sensitive matching, but does not compute embeddings or rank semantic similarity. There is no separate index to operate. The tradeoff is repeated file reading: this implementation does not replace a dedicated search engine for large vaults.

Make write behavior explicit

vault_write creates parent directories and writes the complete document, overwriting an existing file. It provides no automatic merge, version check or undo. For a first connection I use a vault copy with a read-only Docker mount. The write tool remains advertised, but the filesystem rejects modifications. Editing requires a deliberate writable mount and an independent file-history strategy.

Evidence in the implementation

While preparing this case, I called the current vault_glob, vault_search, vault_read and vault_write handlers on temporary notes in a container without network access. Search returned the line above, reading preserved its content, and writing created a separate draft. This checks handlers, not every desktop client or the network authorization flow.

The repository contains the MCP handlers, search implementation and result-shape test. Other tests describe Unicode, nested folders and overwrite behavior. I have not measured time saved or reductions in tool calls.

Current boundaries

This is a tool for a trusted local environment, not isolation between unrelated users. Path checks are not a substitute for handling symlinks and filesystem races. An SSE transport exists, but its authorization endpoint automatically approves requests, without a user login or consent screen. Public access requires further implementation and security review.

Try a separate vault

Build the image and prepare a test folder containing a few notes:

git clone https://github.com/tenqz/obsidian-agent.git
cd obsidian-agent
docker build -t obsidian-agent-mcp .

Use these process settings in a client supporting stdio, replacing the absolute path with your test folder. The read-only mount deliberately limits this example to reading:

{
  "command": "docker",
  "args": [
    "run", "--rm", "-i", "--network", "none",
    "-e", "MCP_TRANSPORT=stdio",
    "--mount", "type=bind,src=/absolute/path/demo-vault,dst=/vault,readonly",
    "obsidian-agent-mcp"
  ]
}

Start with: “Find PostgreSQL, show its path and line, then read the document; change nothing.” The Obsidian MCP setup article explains client integration. After checking read access, decide separately whether the workflow needs writes.

GitHub Setup guide

Let's make the complex understandable.

Architecture, engineering leadership, and AI in development — when the system is too important to simplify, and too expensive not to own.

LinkedIn Telegram Email