Skip to main content

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

On startup, the server:

  1. Loads the embedding model BAAI/bge-m3 via sentence-transformers (100% local, no external API)
  2. Syncs the vault into a persistent ChromaDB database — only notes modified since the last run are re-processed
  3. Starts a watchdog watcher in a background thread that automatically re-indexes every note created, modified, or deleted
  4. 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.0 without 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.1 with host.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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

obsidian_mnemo-0.1.0.tar.gz (213.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

obsidian_mnemo-0.1.0-py3-none-any.whl (31.5 kB view details)

Uploaded Python 3

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

Hashes for obsidian_mnemo-0.1.0.tar.gz
Algorithm Hash digest
SHA256 789b31fc223b1d65b6710e599959606361ad40a58d88d30e1eebc9e24884204a
MD5 eafc7bddfdf018d6aa966e1f3b3ce622
BLAKE2b-256 c23b07bf5dd17ce84b0d901b1f5bd5e682b18e572a8718645b7a7d3d27882d62

See more details on using hashes here.

Provenance

The following attestation bundles were made for obsidian_mnemo-0.1.0.tar.gz:

Publisher: ci.yml on prometek/obsidian-mnemo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for obsidian_mnemo-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 636010b1bfe7ffcf38926946377ccd1bbea0b0f614bfdae018360bec68b3b5fc
MD5 44e8e559185da412f81db868bf97d5a1
BLAKE2b-256 4f09c8e9c2a9ded291af8e5f8868fa4980dd797f43f4f74ce20265ce53bead27

See more details on using hashes here.

Provenance

The following attestation bundles were made for obsidian_mnemo-0.1.0-py3-none-any.whl:

Publisher: ci.yml on prometek/obsidian-mnemo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page