MCP server for interacting with an Obsidian vault
Project description
Obsidian MCP
A Python MCP (Model Context Protocol) server that lets AI agents interact with an Obsidian vault directly via the filesystem — no Obsidian app required.
No vault structure imposed: the server works with any folder organisation.
Table of contents
- How it works
- Requirements
- Installation
- Running the server
- Transports: SSE vs stdio
- Network architecture
- Claude Desktop integration
- Claude Code integration
- Cursor integration
- ChatGPT Desktop integration
- Continue (VS Code / JetBrains) integration
- Ollama / Open WebUI integration
- Remote LLM integration (SSE + Bearer token)
- SSE integration (other clients)
- Available MCP tools
- Vector index (ChromaDB)
- File watcher
- Troubleshooting
- Project architecture
How it works
On startup, the server:
- Loads the embedding model
BAAI/bge-m3viasentence-transformers(100% local, no external API) - Syncs the vault into a persistent ChromaDB database — only notes modified since the last run are re-processed
- Starts a
watchdogwatcher in a background thread that automatically re-indexes every note created, modified, or deleted - Exposes MCP tools to the agent via the official Anthropic SDK (
FastMCP)
Agents can then read, search, and write notes with no dependency on Obsidian.
Requirements
- Python ≥ 3.11
- uv ≥ 0.4 (recommended for installation and environment management)
- ~1 GB of free disk space for the embedding model and ChromaDB database
Installation
# From PyPI (recommended)
uv tool install obsidian-mnemo
# Or from source
git clone https://github.com/prometek/obsidian-mnemo.git
cd obsidian-mnemo
uv tool install .
The BAAI/bge-m3 model (~570 MB) is downloaded from Hugging Face on first launch, then cached locally by sentence-transformers.
Running the server
obsidian-mnemo --vault "/path/to/your/vault"
Available options:
| Option | Default | Env var | Description |
|---|---|---|---|
--vault PATH |
— | VAULT_PATH |
Absolute path to the vault (required) |
--chroma PATH |
~/.obsidian-mnemo/chroma/ |
CHROMA_PATH |
ChromaDB persistence directory |
--transport |
sse |
MCP_TRANSPORT |
Transport: sse or stdio (see next section) |
--host |
127.0.0.1 |
MCP_HOST |
Bind address (SSE transport only) |
--port |
8765 |
MCP_PORT |
Listening port (SSE transport only) |
--log-level LEVEL |
INFO |
— | Log verbosity (DEBUG, INFO, WARNING, ERROR) |
--delete-ttl SECONDS |
300 |
DELETE_TTL |
Seconds before a pending delete confirmation expires |
--search-max-results N |
100 |
SEARCH_MAX_RESULTS |
Hard cap on semantic_search results |
CLI options take precedence over environment variables.
Transports: SSE vs stdio
The server supports two communication modes.
SSE — default, recommended for most use cases
The server starts and listens on an HTTP port. Any MCP client can connect by pointing to http://localhost:8765/sse.
Bearer token — SSE always requires authentication, regardless of bind address. On first start, a token is generated and stored in ~/.obsidian-mnemo/config.json (mode 0600). It is printed to stderr on every start:
obsidian-mnemo: bearer token = <64-char hex> (stored in ~/.obsidian-mnemo/config.json)
Pass it as an Authorization: Bearer <token> header in your client config (see integration sections below).
obsidian-mnemo --vault ~/Notes # listens on 127.0.0.1:8765
obsidian-mnemo --vault ~/Notes --port 9000
This is the mode used by the Obsidian plugin: Obsidian spawns the process on startup, stops it on shutdown, and LLM clients connect to the URL.
stdio — for clients that manage the process lifecycle themselves
With stdio, the LLM client spawns the process itself. The server does not run independently — it lives in a stdin/stdout pipe managed by the client. Claude Desktop, Claude Code, Cursor, and most MCP-compatible tools work this way natively.
obsidian-mnemo --vault ~/Notes --transport stdio
Why not use stdio by default? A stdio process cannot be shared between multiple clients — each client spawns its own process. If you want Obsidian to manage the server lifecycle, you need an independent process, hence SSE. If you only use Claude Desktop or Claude Code, stdio is perfectly fine.
Logs
Logs are written to stderr:
2026-05-11 21:00:01 [INFO] indexer: Loading embedding model BAAI/bge-m3 on device=mps …
2026-05-11 21:00:08 [INFO] indexer: Embedding model ready.
2026-05-11 21:00:08 [INFO] server: Starting initial vault sync …
2026-05-11 21:00:42 [INFO] indexer: Sync complete: 487 indexed, 0 skipped.
2026-05-11 21:00:42 [INFO] watcher: Vault watcher started on /Users/you/Notes…
The first sync can take several minutes depending on vault size. Subsequent starts are near-instant (only modified notes are re-indexed).
Network architecture
The MCP server must run on the same machine as the Obsidian vault. It accesses the vault directly via the filesystem — there is no intermediate API. The LLM client, however, can be anywhere.
Local setup (default)
The typical setup has everything on one machine:
[Your machine]
├── Obsidian vault (/path/to/Notes)
├── obsidian-mcp (bound to 127.0.0.1:8765)
└── LLM client (Claude Desktop, Cursor, etc.)
127.0.0.1 is the loopback address — only processes running on the same machine can connect. No traffic leaves the host. A Bearer token is still required when using SSE transport (see SSE section).
Remote LLM setup
If your LLM runs on a remote server (a self-hosted instance, a cloud agent, etc.), the server must be reachable over the network:
[Your machine]
├── Obsidian vault (/path/to/Notes)
└── obsidian-mcp (bound to 0.0.0.0:8765, protected by Bearer token)
│
│ network
▼
[Remote server]
└── LLM / agent (connects with Authorization: Bearer <token>)
To enable this mode, bind to 0.0.0.0 and protect the server with a secret token:
obsidian-mcp --vault ~/Notes --host 0.0.0.0 --port 8765
Security warning: exposing the server on
0.0.0.0without any firewall or authentication gives any client on your network full read/write access to your vault. Always use a Bearer token and restrict access at the firewall level when running in this mode.
Claude Desktop integration
Claude Desktop spawns the MCP process itself — use --transport stdio.
Config file location:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mnemo",
"args": ["--vault", "/path/to/your/vault", "--transport", "stdio"]
}
}
}
Restart Claude Desktop — the MCP server will appear in the list of available tools.
Claude Code integration
Claude Code also spawns the process — same logic, --transport stdio.
Add to .claude/mcp.json at the root of your project:
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mnemo",
"args": ["--vault", "/path/to/your/vault", "--transport", "stdio"]
}
}
}
Or via the CLI:
claude mcp add obsidian -- obsidian-mnemo --vault /path/to/your/vault --transport stdio
Cursor integration
Cursor spawns the MCP process itself — use --transport stdio.
Edit ~/.cursor/mcp.json (global) or .cursor/mcp.json at the project root:
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp",
"args": ["--vault", "/path/to/your/vault", "--transport", "stdio"]
}
}
}
Restart Cursor — the MCP server will appear in the list of available tools.
ChatGPT Desktop integration
ChatGPT Desktop spawns the MCP process itself — use --transport stdio.
Config file location:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/ChatGPT/mcp.json |
| Windows | %APPDATA%\ChatGPT\mcp.json |
ChatGPT Desktop is not officially available on Linux.
Edit the file for your OS:
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mnemo",
"args": ["--vault", "/path/to/your/vault", "--transport", "stdio"]
}
}
}
Restart ChatGPT Desktop — the MCP server will appear as available tools in the conversation.
Continue (VS Code / JetBrains) integration
Continue supports MCP via its config.json. Start the server in SSE mode first:
obsidian-mcp --vault ~/Notes
Then add the MCP server to ~/.continue/config.json (replace YOUR_TOKEN with the value printed on server startup):
{
"experimental": {
"modelContextProtocolServers": [
{
"transport": {
"type": "sse",
"url": "http://127.0.0.1:8765/sse",
"requestOptions": {
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}
]
}
}
Ollama / Open WebUI integration
Ollama does not have native MCP support, but Open WebUI — the most popular Ollama frontend — supports MCP tools natively via SSE.
Start the server in SSE mode (default):
obsidian-mnemo --vault ~/Notes
Then in Open WebUI → Settings → Tools → Add connection:
| Field | Value |
|---|---|
| URL | http://127.0.0.1:8765/sse |
| Type | MCP (SSE) |
| Auth header | Authorization: Bearer YOUR_TOKEN |
Replace YOUR_TOKEN with the value printed on server startup (also stored in ~/.obsidian-mnemo/config.json).
If Open WebUI runs in Docker and your vault server runs on the host, replace
127.0.0.1withhost.docker.internal(Mac/Windows) or the host IP (Linux).
The Obsidian tools will then be available to any model running through Open WebUI, including local Ollama models.
Remote LLM integration (SSE + Bearer token)
If your LLM agent runs on a remote machine, start the server bound to all interfaces:
obsidian-mcp --vault ~/Notes --host 0.0.0.0 --port 8765
Then configure your agent to pass the Bearer token in the Authorization header.
LangChain / LangGraph
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"obsidian": {
"url": "http://YOUR_IP:8765/sse",
"transport": "sse",
"headers": {"Authorization": "Bearer YOUR_TOKEN"},
}
})
tools = await client.get_tools()
LlamaIndex
from llama_index.tools.mcp import BasicMCPClient, McpToolSpec
mcp_client = BasicMCPClient(
"http://YOUR_IP:8765/sse",
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
tool_spec = McpToolSpec(client=mcp_client)
tools = tool_spec.to_tool_list()
OpenAI Agents SDK
from agents.mcp import MCPServerSse
server = MCPServerSse(
url="http://YOUR_IP:8765/sse",
headers={"Authorization": "Bearer YOUR_TOKEN"},
)
async with server:
tools = await server.list_tools()
Claude Desktop (remote vault)
{
"mcpServers": {
"obsidian": {
"url": "http://YOUR_IP:8765/sse",
"transport": "sse",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}
Replace YOUR_IP with the IP address of the machine running the vault, and YOUR_TOKEN with the secret token you defined.
SSE integration (other clients)
For any MCP client that supports HTTP/SSE transport, start the server in default mode and point to the URL:
obsidian-mnemo --vault ~/Notes # listens on http://127.0.0.1:8765/sse
Example config (generic MCP over HTTP format):
{
"mcpServers": {
"obsidian": {
"url": "http://127.0.0.1:8765/sse"
}
}
}
Available MCP tools
read_note
Reads a note by its vault-relative path. Returns the YAML frontmatter and Markdown content.
Parameters
| Name | Type | Description |
|---|---|---|
path |
string |
Vault-relative path, e.g. "AI/RAG/chroma-db.md" |
Response
{
"path": "AI/RAG/chroma-db.md",
"modified": "2026-05-01T10:00:00",
"frontmatter": {
"tags": ["AI", "RAG"],
"aliases": ["ChromaDB"]
},
"content": "# ChromaDB\n\nLocal vector database..."
}
list_notes
Lists notes in the vault, with an optional folder filter.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
folder |
string? |
— | Vault-relative folder path to filter by |
recursive |
bool |
true |
Include sub-folders |
Examples
list_notes() # all notes in the vault
list_notes(folder="AI") # notes in AI/ and sub-folders
list_notes(folder="AI", recursive=False) # notes directly in AI/
Response (list)
[
{
"path": "AI/RAG/chroma-db.md",
"title": "chroma-db",
"folder": "AI/RAG",
"modified": "2026-05-01T10:00:00"
}
]
list_folders
Returns the folder tree of the vault (or a sub-folder) as a nested dictionary.
Parameters
| Name | Type | Description |
|---|---|---|
folder |
string? |
Root sub-folder (default: vault root) |
Examples
list_folders()
list_folders(folder="AI")
Response
{
"AI": {
"RAG": {},
"Agents": {}
},
"Dev": {
"Python": {}
}
}
semantic_search
Semantic search by vector similarity. Embeddings are generated locally (BAAI/bge-m3), no external API.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
query |
string |
— | Natural language search query |
folder |
string? |
— | Filter by folder (and sub-folders) |
limit |
int |
5 |
Maximum number of results |
Examples
semantic_search("RAG architecture with reranking")
semantic_search("monthly budget", folder="Finance", limit=3)
semantic_search("autonomous LLM agents", folder="AI")
Response (list sorted by descending score)
[
{
"path": "AI/RAG/chroma-db.md",
"title": "chroma-db",
"folder": "AI/RAG",
"score": 0.8921,
"excerpt": "ChromaDB is an open-source vector database optimised..."
}
]
The score is a cosine similarity between 0 and 1 — the higher, the more relevant the note.
create_note
Creates a new note in the vault and indexes it immediately in ChromaDB.
Parameters
| Name | Type | Description |
|---|---|---|
path |
string |
Vault-relative path (intermediate folders are created automatically) |
content |
string |
Note body in Markdown |
frontmatter |
dict? |
Optional YAML metadata (tags, aliases, etc.) |
Example
create_note(
path="AI/RAG/new-note.md",
content="# New note\n\nContent...",
frontmatter={"tags": ["AI", "RAG"], "aliases": ["my note"]}
)
Response
{ "created": "AI/RAG/new-note.md" }
append_to_note
Appends content to the end of an existing note and re-indexes it.
Parameters
| Name | Type | Description |
|---|---|---|
path |
string |
Vault-relative path |
content |
string |
Markdown text to append |
Example
append_to_note(
path="Projects/MyProject/notes.md",
content="## Update — May 11\n\n- Point 1\n- Point 2"
)
Response
{ "updated": "Projects/MyProject/notes.md" }
Vector index (ChromaDB)
Location
Default: ~/.obsidian-mnemo/chroma/
Configurable via --chroma or CHROMA_PATH:
obsidian-mnemo --vault ~/Notes --chroma /data/obsidian-chroma
Metadata stored per note
{
"folder": "AI/RAG",
"title": "chroma-db",
"path": "AI/RAG/chroma-db.md",
"tags": "AI RAG",
"modified": "2026-05-01T10:00:00",
}
Incremental re-indexing
On each startup, the server compares the modified timestamp of each file with the one stored in ChromaDB. Only modified notes are re-embedded — others are skipped. This makes subsequent starts very fast even on vaults with hundreds of notes.
Indexed text
For each note, the embedded text is built as follows:
[title or alias if present in frontmatter]
[tags if present]
[Markdown content of the note]
If the frontmatter is invalid (malformed YAML), the entire file is indexed as plain text.
File watcher
The watchdog watcher monitors the vault in real time and reacts to the following events:
| Event | Action |
|---|---|
.md file created |
Immediate indexing |
.md file modified |
Re-indexing |
.md file deleted |
Removal from ChromaDB |
.md file moved/renamed |
Old entry removed + new entry indexed |
The watcher runs in a daemon thread — it does not block the MCP server and stops automatically when the process exits.
Troubleshooting
--vault not provided
ERROR server: --vault is required (or set VAULT_PATH).
Pass the vault path via --vault or the VAULT_PATH environment variable.
Notes with invalid frontmatter
WARNING indexer: Frontmatter parse error in Notes/my-note.md, indexing as plain text: ...
Some Obsidian notes use non-standard YAML syntax (e.g. multi-line tags). These notes are indexed as plain text — they remain searchable but without frontmatter metadata.
Slow first launch
Downloading the BAAI/bge-m3 model (~570 MB) and fully indexing a 500-note vault takes several minutes. Subsequent launches are fast.
Corrupted ChromaDB
rm -rf ~/.obsidian-mnemo/chroma/
The server will rebuild the full index on next startup.
Tests
uv run pytest
Project architecture
obsidian-mnemo/
├── src/
│ └── obsidian_mnemo/
│ ├── server.py # CLI entry point: init, sync, watcher, FastMCP
│ ├── obsidian_client.py # Filesystem read/write + frontmatter parsing
│ ├── indexer.py # Vault sync → ChromaDB (BAAI/bge-m3 embeddings)
│ ├── vector_store.py # ChromaDB wrapper (upsert / delete / query)
│ ├── watcher.py # watchdog file watcher (background thread)
│ └── tools/
│ ├── read.py # MCP tools: read_note, list_notes, list_folders
│ ├── search.py # MCP tool: semantic_search
│ ├── write.py # MCP tools: create_note, append_to_note, …
│ ├── assets.py # MCP tools: list_assets, move_asset, delete_asset
│ ├── folders.py # MCP tools: move_folder, delete_empty_folders
│ └── status.py # MCP tool: vault_status
├── tests/
├── pyproject.toml
└── README.md
Data flow
Vault (.md) ──► ObsidianClient ──► Indexer ──► VectorStore (ChromaDB)
▲
SentenceTransformer
(BAAI/bge-m3, local)
Agent ──► FastMCP ──► tools/read.py ──► ObsidianClient
──► tools/search.py ──► VectorStore + Indexer.embed_query()
──► tools/write.py ──► ObsidianClient + Indexer
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file obsidian_mnemo-0.1.0.tar.gz.
File metadata
- Download URL: obsidian_mnemo-0.1.0.tar.gz
- Upload date:
- Size: 213.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
789b31fc223b1d65b6710e599959606361ad40a58d88d30e1eebc9e24884204a
|
|
| MD5 |
eafc7bddfdf018d6aa966e1f3b3ce622
|
|
| BLAKE2b-256 |
c23b07bf5dd17ce84b0d901b1f5bd5e682b18e572a8718645b7a7d3d27882d62
|
Provenance
The following attestation bundles were made for obsidian_mnemo-0.1.0.tar.gz:
Publisher:
ci.yml on prometek/obsidian-mnemo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
obsidian_mnemo-0.1.0.tar.gz -
Subject digest:
789b31fc223b1d65b6710e599959606361ad40a58d88d30e1eebc9e24884204a - Sigstore transparency entry: 1572655316
- Sigstore integration time:
-
Permalink:
prometek/obsidian-mnemo@acee34db2afd6b9b657d73f49ccb7dc8fd1472d2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/prometek
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@acee34db2afd6b9b657d73f49ccb7dc8fd1472d2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file obsidian_mnemo-0.1.0-py3-none-any.whl.
File metadata
- Download URL: obsidian_mnemo-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
636010b1bfe7ffcf38926946377ccd1bbea0b0f614bfdae018360bec68b3b5fc
|
|
| MD5 |
44e8e559185da412f81db868bf97d5a1
|
|
| BLAKE2b-256 |
4f09c8e9c2a9ded291af8e5f8868fa4980dd797f43f4f74ce20265ce53bead27
|
Provenance
The following attestation bundles were made for obsidian_mnemo-0.1.0-py3-none-any.whl:
Publisher:
ci.yml on prometek/obsidian-mnemo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
obsidian_mnemo-0.1.0-py3-none-any.whl -
Subject digest:
636010b1bfe7ffcf38926946377ccd1bbea0b0f614bfdae018360bec68b3b5fc - Sigstore transparency entry: 1572655349
- Sigstore integration time:
-
Permalink:
prometek/obsidian-mnemo@acee34db2afd6b9b657d73f49ccb7dc8fd1472d2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/prometek
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@acee34db2afd6b9b657d73f49ccb7dc8fd1472d2 -
Trigger Event:
release
-
Statement type: