Skip to main content

some-vault-some-mcp

MCP server that gives AI models read/write access to Obsidian vaults with semantic search, link graph analysis, canvas manipulation, and incremental indexing.

Built on FastMCP + LanceDB. Embeds notes locally with fastembed (nomic-embed-text-v1.5-Q, 768 dims) by default — no server required. Optionally supports Ollama or OpenAI embeddings. Watches the vault filesystem and re-indexes on change.

Warning: This is a hot vibe coded mess, user beware

What it does

  • Hybrid search - vector similarity (70%) + full-text keyword (30%), with overlap boost
  • Semantic search - pure embedding-based retrieval
  • Exact search - literal substring matching, optional case sensitivity and regex
  • Note CRUD - create, read, append, prepend, move, delete (soft delete to .trash by default)
  • Frontmatter parsing - YAML extraction, tag collection (frontmatter + inline #hashtags), property filtering
  • Wikilink graph - backlinks, outlinks, orphan detection, broken link detection, BFS neighbor traversal (depth 1-5)
  • Canvas CRUD - create, read, add/update/remove nodes and edges, grid auto-layout, dangling edge cleanup
  • Daily notes - Moment.js-style date formatting, template support, reads Obsidian's daily-notes config
  • Incremental indexing - filesystem watcher with 2s debounce and SHA-256 content change detection
  • Atomic writes - temp file + POSIX rename, per-path asyncio locks
  • Tool overrides - rename or disable any tool via YAML config (per-agent customization)

Requirements

  • Python 3.11+
  • uv (for uvx)

Quick start

Add this to your MCP client config (Cursor, Windsurf, Claude Code, etc.):

{
  "mcpServers": {
    "obsidian-vault": {
      "command": "uvx",
      "args": ["some-vault-some-mcp", "serve", "--transport", "stdio"],
      "env": {
        "VAULT_PATH": "/path/to/your/obsidian/vault"
      }
    }
  }
}

That's it. uvx installs the package from PyPI on first run and keeps it cached.

For alternative embedding providers, add --from with extras:

{
  "command": "uvx",
  "args": ["--from", "some-vault-some-mcp[ollama]", "some-vault-some-mcp", "serve", "--transport", "stdio"],
  "env": {
    "VAULT_PATH": "/path/to/your/obsidian/vault"
  }
}

Replace [ollama] with [openai] or [gpu] as needed.

SSE mode

If running the server separately (Docker, remote, etc.), use the SSE URL instead:

{
  "mcpServers": {
    "obsidian-vault": {
      "url": "http://localhost:3789/sse"
    }
  }
}

Running directly

VAULT_PATH=/path/to/vault uvx some-vault-some-mcp serve

First run builds a staged index generation and publishes it only after schema, vector, and full-text validation. Subsequent starts scan content hashes and publish incremental changes through staged generations too, so the last valid generation remains available on failure. Readers hold a cross-process shared lease while materializing results; publication and live cleanup take the exclusive side, so completed databases retain only the active and previous generations without dropping an in-flight reader's table.

CLI flags

some-vault-some-mcp serve [--transport sse|stdio] [--host 127.0.0.1] [--port 3789] [--reindex-force]

--reindex-force builds and validates a replacement generation without dropping the active index first. It is required when embedding dimensions change. The server also rebuilds automatically (one time) when it detects an index written by an older storage format.

CLI flags override env vars.

Environment variables

Variable Default Notes
VAULT_PATH (required) Absolute path to Obsidian vault
LANCE_DB_PATH ./data/vault.lance Vector index location. Use a trusted absolute path outside the vault and workspace; unsafe locations produce a warning.
MCP_TRANSPORT sse sse or stdio
MCP_HOST 127.0.0.1 SSE bind address. Defaults to loopback — set 0.0.0.0 to expose on the network (the Docker image does this explicitly). Binding non-loopback without VAULT_API_KEY logs a warning.
MCP_PORT 3789 SSE port
EMBEDDING_PROVIDER fastembed fastembed, ollama, openai, or mock
FASTEMBED_MODEL nomic-ai/nomic-embed-text-v1.5-Q Any fastembed-supported model. On Apple Silicon, auto-detects and uses the non-quantized variant (v1.5 instead of v1.5-Q) since the quantized ONNX ops are x86-optimized
FASTEMBED_DIMENSIONS (auto-detected) Override dimension auto-detection
OLLAMA_URL http://localhost:11434 Ollama API endpoint (requires --extra ollama)
OPENAI_API_KEY Required if provider is openai (requires --extra openai)
VAULT_API_KEY Enables Bearer token auth on SSE transport (constant-time compare; covers /sse and websocket connections)
VAULT_ALLOW_UNAUTH_SSE false true exempts the /sse GET from auth — only for clients that cannot send an Authorization header on the SSE stream (Cursor/Claude Code/Claude Desktop all can, so leave false)
VAULT_SOFT_DELETE_IS_PERMANENT false true makes delete_note do hard deletes
some_vault_some_mcp_OVERRIDES Path to YAML override file

Docker

docker build -t some-vault-some-mcp .

docker run -p 3789:3789 \
  -v /path/to/vault:/opt/vault:ro \
  -v /data:/opt/data \
  some-vault-some-mcp

Runs as non-root user (vault:vault, UID 1000). Index data persists in /opt/data. Vault mounted read-only. Container is self-contained — fastembed runs in-process, no Ollama sidecar needed.

To use Ollama instead: docker run ... -e EMBEDDING_PROVIDER=ollama -e OLLAMA_URL=http://ollama:11434 some-vault-some-mcp

Tools (27)

search

Find notes by text, meaning, or exact string.

Param Type Default Notes
query string (required) Search query text
mode string "hybrid" hybrid, semantic, or exact
top_k int 10 Max results to return
tags string[] null Pre-filter by tags
folder string null Pre-filter by folder path prefix
case_sensitive bool false Exact mode only - match case

Read

get_note

Read a single note with parsed frontmatter and tags.

Param Type Default Notes
path string (required) Vault-relative path to the note. Extension-agnostic — todo and todo.md both resolve to todo.md. A bare basename resolves Obsidian-style to a matching note anywhere in the vault.

list_notes

Enumerate vault notes with optional metadata filters. Index-backed fields (tags, projects, status, area) query LanceDB when available. Frontmatter property filtering scans files directly.

Param Type Default Notes
folder string null Filter by folder path prefix
tags string[] null Filter by tags (index-backed)
projects string[] null Filter by projects (index-backed)
status string null Filter by status field (index-backed)
area string null Filter by area field (index-backed)
frontmatter_property string null Arbitrary frontmatter key to filter on
frontmatter_value string null Value to match for frontmatter_property (case-insensitive)
include_content bool false Include note content in results
limit int 50 Max results to return

Write

create_note

Create a new note. Fails if it already exists.

Param Type Default Notes
path string (required) Vault-relative path for the new note
content string (required) Note body text
frontmatter string null JSON string of frontmatter fields, e.g. '{"title":"My Note","tags":["idea"]}'

append_to_note

Append text to the end of an existing note.

Param Type Default Notes
path string (required) Vault-relative path
content string (required) Text to append

prepend_to_note

Insert text after frontmatter, before the note body.

Param Type Default Notes
path string (required) Vault-relative path
content string (required) Text to prepend

update_frontmatter

Merge key-value pairs into YAML frontmatter. Unlisted keys preserved.

Param Type Default Notes
path string (required) Vault-relative path
properties string (required) JSON string of key-value pairs, e.g. '{"status":"done","tags":["review"]}'

move_note

Move or rename a note. Rewrites wikilinks vault-wide by default. Moves are no-replace operations: an existing distinct destination is never overwritten. Filesystems without the platform's native exclusive-rename capability are rejected rather than falling back to an overwriting rename. A separately named hard-link destination also fails closed because it cannot be removed safely under a concurrent destination replacement.

Param Type Default Notes
old_path string (required) Current vault-relative path
new_path string (required) Destination vault-relative path
update_links bool true Rewrite wikilinks in all referencing notes

delete_note

Delete a note. Moves to .trash by default; use permanent for hard delete.

Param Type Default Notes
path string (required) Vault-relative path
permanent bool false true for hard delete, false for soft delete to .trash

Daily notes

get_daily_note

Read today's or a specific date's daily note. Uses Obsidian's daily-notes config for folder and date format. Returns a text message (not an error) if no note exists for the requested date.

Param Type Default Notes
date string null Date string (e.g. "2024-03-15"). Omit for today

create_daily_note

Create a daily note for today or a given date. Fails if one exists. Supports {{date}} placeholder in templates.

Param Type Default Notes
date string null Date string. Omit for today
content string null Note body text
template_path string null Vault-relative path to a template note

Tags

get_tags

List all unique tags in the vault with per-note usage counts.

Param Type Default Notes
sort_by string "count" count (descending) or name (alphabetical)

Find notes that link to a given note.

Param Type Default Notes
path string (required) Vault-relative path of the target note

List all outgoing wikilinks from a note. Shows valid, broken, and embed links separately.

Param Type Default Notes
path string (required) Vault-relative path

find_orphans

Find disconnected notes - no inbound links, no outbound links, or both.

Param Type Default Notes
include_outlinks_check bool true Also report notes with no outgoing links
max_results int 200 Cap per category

Find wikilinks pointing at notes that don't exist.

Param Type Default Notes
folder string null Limit scan to a folder
max_results int 200 Max broken links to return

get_graph_neighbors

Walk the link graph outward from a note via BFS.

Param Type Default Notes
path string (required) Starting note path
depth int 1 BFS depth, 1-5
direction string "both" inbound, outbound, or both

Canvas

list_canvases

List all .canvas files in the vault.

Param Type Default Notes
folder string null Filter by folder path prefix

read_canvas

Read canvas structure (nodes + edges).

Param Type Default Notes
path string (required) Vault-relative path to .canvas file

create_canvas

Create a new .canvas file with optional initial nodes/edges. Fails if it already exists.

Param Type Default Notes
path string (required) Vault-relative path for the new canvas
nodes string null JSON array of node objects, e.g. '[{"type":"text","text":"Hello"}]'
edges string null JSON array of edge objects

add_canvas_node

Add a node (text/file/link/group) to a canvas. Position auto-computed via grid layout if omitted.

Param Type Default Notes
canvas_path string (required) Path to the canvas
node_type string (required) text, file, link, or group
text string null Text content (for text nodes)
file string null Vault-relative file path (for file nodes). For markdown notes the .md extension is optional and a bare name resolves Obsidian-style; the full path is stored. Attachments (images/PDFs) must be given with their extension.
url string null URL (for link nodes)
label string null Display label
x int null X position. Auto-placed if omitted
y int null Y position. Auto-placed if omitted
width int 250 Node width in pixels
height int 60 Node height in pixels
color string null Obsidian preset "1"-"6" (red, orange, yellow, green, cyan, purple) or hex "#FF0000"

update_canvas_node

Update any property of an existing canvas node by ID. Only provided fields are changed.

Param Type Default Notes
canvas_path string (required) Path to the canvas
node_id string (required) ID of the node to update
x int null New X position
y int null New Y position
width int null New width
height int null New height
color string null Preset "1"-"6" or hex "#FF0000"
text string null New text content
file string null New file reference
url string null New URL
label string null New label

remove_canvas_nodes

Remove nodes by ID. Auto-removes dangling edges.

Param Type Default Notes
canvas_path string (required) Path to the canvas
node_ids string (required) JSON array of node ID strings, e.g. '["id1","id2"]'

add_canvas_edge

Add an edge between two canvas nodes with full property support.

Param Type Default Notes
canvas_path string (required) Path to the canvas
from_node string (required) Source node ID
to_node string (required) Target node ID
from_side string null Anchor side on source: top, bottom, left, right
to_side string null Anchor side on target
from_end string null End style on source side (e.g. arrow, none)
to_end string null End style on target side
color string null Preset "1"-"6" or hex "#FF0000"
label string null Edge label text

update_canvas_edge

Update properties of an existing canvas edge by ID. Only provided fields are changed.

Param Type Default Notes
canvas_path string (required) Path to the canvas
edge_id string (required) ID of the edge to update
from_side string null New source anchor side
to_side string null New target anchor side
from_end string null New source end style
to_end string null New target end style
color string null Preset "1"-"6" or hex "#FF0000"
label string null New label

remove_canvas_edges

Remove edges from a canvas by ID.

Param Type Default Notes
canvas_path string (required) Path to the canvas
edge_ids string (required) JSON array of edge ID strings

Index management

vault_index_status

Check search index health and statistics, including rebuild progress, degraded fallback serving, pending changes, and the last rebuild error. No arguments.

vault_reindex

Reindex one note incrementally, or build and atomically publish a new generation when the path is omitted.

Param Type Default Notes
path string null Vault-relative path to reindex a single note. Omit for full vault

MCP resources

  • obsidian://note/{path} - read a note by path
  • obsidian://tags - all tags
  • obsidian://daily - today's daily note

Tool name overrides

When you need to change the tool names and descriotions, a YAML file can be used. Create a YAML file, and set some_vault_some_mcp_OVERRIDES to its path:

tools:
  search:
    name: "vault_search"
    description: "Custom description for this agent"
  get_note:
    name: "read_note"
disabled:
  - get_tags
  - find_orphans

Overrides apply at registration time. Useful for per-agent tool namespacing or disabling tools an agent shouldn't use if your agent doesn't support tool disabling.

How search works

Hybrid (default) - runs both semantic and FTS (LanceDB full-text search, BM25-style) at 2x requested top_k, normalizes scores, combines with semantic * 0.7 + FTS * 0.3. Results appearing in both get a 1.2x boost. Returns top_k from merged set.

Semantic - embeds the query, runs vector similarity search against LanceDB.

Exact - scans raw vault files for literal substring matches. Not index-backed. Optional case sensitivity.

All search modes accept tag and folder pre-filters. Tags use SQL LIKE against comma-separated tag strings in the index. Folders use path prefix matching.

Chunking

Splits markdown by heading hierarchy first, then by paragraph for oversized sections. Tracks heading breadcrumbs through the split - a chunk under # A / ## B / ### C gets heading field "A > B > C". Metadata header prepended to embedding text: [Title: X | Section: A > B | Tags: foo, bar].

Matches Obsidian's behavior in 4 steps:

  1. Exact relative-path match (case-insensitive)
  2. Path-suffix match (if link contains /)
  3. Basename match with proximity tie-break (deepest shared path prefix with source)
  4. Alias match (frontmatter aliases)

First matching step wins.

Security boundaries

  • Tool paths are validated against the vault root and reject ../, null bytes, and static symlink escapes. These pathname checks do not close the concurrent symlink-swap race described as SR-04 in SECURITY_REVIEW.md.
  • .obsidian, .git, .trash denied at the tool boundary and excluded from indexing
  • Any folder whose name starts with . (for example .claude/) is skipped by the indexer and by note listings. Add more folder names with VAULT_EXCLUDED_DIRS=external,archive or with an excluded_dirs: [external, archive] list in the override file. These folders are hidden, not denied: search, listings, backlinks and link rewrites skip them, but a tool call with the exact path still works. resolve_vault_path still denies only .obsidian, .git and .trash.
  • Optional Bearer token auth on SSE transport
  • Docker container runs as non-root
  • Frontmatter normalization rejects recursive aliases, excessive depth, excessive node or scalar-payload expansion, invalid timestamps, non-string mapping keys, and unsupported scalar types. Invalid metadata is discarded while the note body remains indexable.

Index recovery

Full rebuilds retain the previous valid generation. If a newly published index must be rolled back, stop every server process using that LANCE_DB_PATH, back up the complete index directory, then swap the active and previous table names in _index_manifest.json. Write the edited JSON to a sibling temporary file and atomically replace the manifest; do not edit it in place. Restart the server and confirm vault_index_status reports the expected file and chunk counts. Keep the backup until search results have been checked. If either named table is missing or the manifest is malformed, restore the backup instead of deleting tables manually.

Developing

Setup

git clone https://github.com/russellsch/some_obsidian_some_mcp.git
cd some_obsidian_some_mcp
uv sync                           # default (fastembed CPU)
uv sync --extra gpu               # GPU-accelerated embeddings
uv sync --extra ollama            # Ollama provider
uv sync --extra openai            # OpenAI provider

Running from source

VAULT_PATH=/path/to/vault uv run some-vault-some-mcp serve

To use a local checkout in your MCP client config instead of the PyPI package:

{
  "mcpServers": {
    "obsidian-vault": {
      "command": "uv",
      "args": ["run", "--project", "/path/to/some_obsidian_some_mcp", "some-vault-some-mcp", "serve", "--transport", "stdio"],
      "env": {
        "VAULT_PATH": "/path/to/your/obsidian/vault"
      }
    }
  }
}

Tests

uv run pytest tests/unit           # unit tests (fast, no external deps)
uv run pytest tests/integration    # integration tests (real LanceDB, may download ~130MB model)
uv run pytest                      # everything

Test fixtures live in tests/fixtures/vault/.

Releasing

See RELEASING.md.

Metadata

Release files for some-vault-some-mcp 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for some-vault-some-mcp 0.2.1
File Size Uploaded
some_vault_some_mcp-0.2.1.tar.gz 302.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for some-vault-some-mcp 0.2.1
File Interpreter ABI Platform
some_vault_some_mcp-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 381.8 kB

Release files / some_vault_some_mcp-0.2.1.tar.gz

Download URL some_vault_some_mcp-0.2.1.tar.gz
Size 302.3 kB
Tags Source
SHA-256 checksum
How to use checksums
89431e7108f1215423cca293562fe3c4a69800cc716df492ca0f0598532ffb88
BLAKE2b-256 checksum
How to use checksums
53bc82523dda98ed84493642a15d26e46f304cefd3c1b7a70115ece503b39b0a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / some_vault_some_mcp-0.2.1-py3-none-any.whl

Download URL some_vault_some_mcp-0.2.1-py3-none-any.whl
Size 79.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2ad5559a1aac284a4397ce57857ac14e8eadbe8e4d6c5b4924f0f7c851d71854
BLAKE2b-256 checksum
How to use checksums
9cf62a5421fc877f0b790e6d8a2afd75381b9e651317f6371b88cf5138189beb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page