Metadata-Version: 2.4
Name: obsidian-mnemo
Version: 0.1.0
Summary: MCP server for interacting with an Obsidian vault
Author-email: Guillaume Fassot <contact@gfassot.com>
License: MIT
License-File: LICENSE
Keywords: ai,llm,mcp,obsidian,rag,vector-search
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.11
Requires-Dist: chromadb>=0.5.0
Requires-Dist: mcp[cli]>=1.0.0
Requires-Dist: python-frontmatter>=1.1.0
Requires-Dist: rank-bm25>=0.2.2
Requires-Dist: sentence-transformers>=3.0.0
Requires-Dist: typer>=0.12.0
Requires-Dist: watchdog>=4.0.0
Description-Content-Type: text/markdown

# Obsidian MCP

A Python [MCP](https://modelcontextprotocol.io) (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](#how-it-works)
- [Requirements](#requirements)
- [Installation](#installation)
- [Running the server](#running-the-server)
- [Transports: SSE vs stdio](#transports-sse-vs-stdio)
- [Network architecture](#network-architecture)
- [Claude Desktop integration](#claude-desktop-integration)
- [Claude Code integration](#claude-code-integration)
- [Cursor integration](#cursor-integration)
- [ChatGPT Desktop integration](#chatgpt-desktop-integration)
- [Continue (VS Code / JetBrains) integration](#continue-vs-code--jetbrains-integration)
- [Ollama / Open WebUI integration](#ollama--open-webui-integration)
- [Remote LLM integration (SSE + Bearer token)](#remote-llm-integration-sse--bearer-token)
- [SSE integration (other clients)](#sse-integration-other-clients)
- [Available MCP tools](#available-mcp-tools)
- [Vector index (ChromaDB)](#vector-index-chromadb)
- [File watcher](#file-watcher)
- [Troubleshooting](#troubleshooting)
- [Project architecture](#project-architecture)

---

## 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](https://docs.astral.sh/uv/getting-started/installation/) ≥ 0.4 (recommended for installation and environment management)
- ~1 GB of free disk space for the embedding model and ChromaDB database

---

## Installation

```bash
# 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

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

```bash
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.

```bash
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](#sse--default-recommended-for-most-use-cases)).

### 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:

```bash
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` |

```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:

```json
{
  "mcpServers": {
    "obsidian": {
      "command": "obsidian-mnemo",
      "args": ["--vault", "/path/to/your/vault", "--transport", "stdio"]
    }
  }
}
```

Or via the CLI:

```bash
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:

```json
{
  "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:

```json
{
  "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](https://www.continue.dev/) supports MCP via its `config.json`. Start the server in SSE mode first:

```bash
obsidian-mcp --vault ~/Notes
```

Then add the MCP server to `~/.continue/config.json` (replace `YOUR_TOKEN` with the value printed on server startup):

```json
{
  "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](https://github.com/open-webui/open-webui) — the most popular Ollama frontend — supports MCP tools natively via SSE.

Start the server in SSE mode (default):

```bash
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:

```bash
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

```python
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

```python
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

```python
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)

```json
{
  "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:

```bash
obsidian-mnemo --vault ~/Notes  # listens on http://127.0.0.1:8765/sse
```

Example config (generic MCP over HTTP format):

```json
{
  "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**

```json
{
  "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**

```python
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)

```json
[
  {
    "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**

```python
list_folders()
list_folders(folder="AI")
```

**Response**

```json
{
  "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**

```python
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)

```json
[
  {
    "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**

```python
create_note(
  path="AI/RAG/new-note.md",
  content="# New note\n\nContent...",
  frontmatter={"tags": ["AI", "RAG"], "aliases": ["my note"]}
)
```

**Response**

```json
{ "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**

```python
append_to_note(
  path="Projects/MyProject/notes.md",
  content="## Update — May 11\n\n- Point 1\n- Point 2"
)
```

**Response**

```json
{ "updated": "Projects/MyProject/notes.md" }
```

---

## Vector index (ChromaDB)

### Location

Default: `~/.obsidian-mnemo/chroma/`

Configurable via `--chroma` or `CHROMA_PATH`:

```bash
obsidian-mnemo --vault ~/Notes --chroma /data/obsidian-chroma
```

### Metadata stored per note

```python
{
    "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

```bash
rm -rf ~/.obsidian-mnemo/chroma/
```

The server will rebuild the full index on next startup.

---

## Tests

```bash
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
```
