Obsidian MCP Server

Community Search and web

Remote Obsidian vault management over SSE: notes, directories, frontmatter, tags, search and link operations, deployable with Docker.

Install

The installation steps are in the documentation below. Documentation

Documentation

From the project's README on GitHub, refreshed weekly. Links open on GitHub.

Contents
  1. Obsidian MCP Server
  2. Features
  3. Quick Start
  4. AI Assistant Configuration
  5. Available Tools
  6. Architecture
  7. Project Structure
  8. Environment Variables
  9. Learn log
  10. Semantic search
  11. API Endpoints
  12. Authentication
  13. Deployment with NGINX Proxy Manager
  14. Private access with Tailscale
  15. Troubleshooting
  16. Development
  17. Contributing
  18. License
  19. Related Projects

Obsidian MCP Server

A Model Context Protocol (MCP) server that enables AI assistants like Claude to interact with your Obsidian vault. Access your notes, create content, manage tags, and search your knowledge base through natural conversation.

Features

Note Management
  • CRUD Operations: Create, read, update, and delete notes (with safety confirmation)
  • Write Modes: Overwrite, append, or prepend content
  • Batch Reading: Read multiple notes in a single request
  • File Info: Get metadata without reading content (efficient for large vaults)
  • Move/Duplicate: Reorganize your vault structure
  • Section Operations: Read, append, or replace specific sections by heading
Frontmatter & Tags
  • Frontmatter Parsing: Get/set YAML frontmatter as structured JSON (powered by gray-matter)
  • Tag Management: Add/remove tags (frontmatter or inline)
  • Tag Auditing: Find notes missing required tags
  • Tag Search: List all tags with usage counts
  • Full-Text Search: Search content and filenames with context
  • Backlinks: Find all notes linking to a specific note
  • Broken Links: Detect wiki-links that don't resolve
  • Find & Replace: Bulk text replacement with regex support
Semantic Search (optional)
  • Search by meaning: hybrid vector + full-text retrieval over heading-aware chunks, backed by Postgres + pgvector and local embeddings from Ollama
  • Similar notes: nearest notes to a given note, by whole-note embedding
  • Incremental indexer: a second process from the same image that polls the vault and re-embeds only what changed; works over NFS/SMB where inotify does not
  • Nothing leaves your network: embeddings are computed locally; the feature is off entirely unless DATABASE_URL is set
Directory Operations
  • Create/Delete/Rename: Full directory management
  • List Contents: Browse vault structure
Performance
  • Token Optimization: Optional compact response mode (40-60% smaller responses)
  • Efficient Scanning: Get file info without reading content
  • SSE Transport: Remote access without local installation

Quick Start

How clients connect: the server speaks MCP over HTTP + SSE only (there is no stdio mode). You start it (npx, Docker or from source), then point a client at http://HOST:PORT/sse. Clients that only support stdio servers connect through mcp-remote, as shown below. It listens on all network interfaces on PORT, so set an API_KEY (see Authentication) or keep it on loopback (for example -p 127.0.0.1:3001:3000 in Docker) unless you want the whole network to have read/write access to your vault.

npx (Quickest)

Requires Node.js 20.19 or newer. This starts the server in the foreground (default port 3000):

VAULT_PATH=/path/to/your/vault npx @smith-and-web/obsidian-mcp-server

With options:

VAULT_PATH=/path/to/vault PORT=3001 COMPACT_RESPONSES=true npx @smith-and-web/obsidian-mcp-server

Using the pre-built image from GitHub Container Registry:

docker run -d \
  --name obsidian-mcp \
  -v /path/to/your/vault:/vault:rw \
  -p 127.0.0.1:3001:3000 \
  -e VAULT_PATH=/vault \
  ghcr.io/smith-and-web/obsidian-mcp-server:latest

latest follows the newest release; pin a version tag (for example :1.6.0) for production. Drop 127.0.0.1: only if you set API_KEY and want other machines to connect.

Or with Docker Compose:

  1. Create a docker-compose.yml:
     services:
       obsidian-mcp:
         image: ghcr.io/smith-and-web/obsidian-mcp-server:latest
         container_name: obsidian-mcp
         restart: unless-stopped
         volumes:
           - /path/to/your/vault:/vault:rw
         ports:
           - "127.0.0.1:3001:3000"
         environment:
           - VAULT_PATH=/vault
  2. Start the server
     docker compose up -d
  3. Verify it's running
     curl http://localhost:3001/health
Local Development
  1. Install dependencies
     npm install
  2. Set environment variables
     export VAULT_PATH=/path/to/your/vault
     export PORT=3000
  3. Start the server
     npm start
     # Or with auto-reload:
     npm run dev

AI Assistant Configuration

Claude Desktop

Add to your Claude Desktop config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Option 1: Local server (via mcp-remote)

{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:3001/sse"]
    }
  }
}

Option 2: Remote server with HTTPS

{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://your-domain.com/sse"]
    }
  }
}

The server must already be running (see Quick Start). Claude Desktop launches mcp-remote, which bridges its stdio to the server's SSE endpoint; Claude Desktop cannot launch this server directly, because the server does not speak stdio.

Cursor

Add to your Cursor MCP settings (Settings → MCP):

{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:3001/sse"]
    }
  }
}
Other MCP Clients

Claude Code supports SSE servers directly:

claude mcp add --transport sse obsidian http://localhost:3001/sse
# with an API key:
claude mcp add --transport sse obsidian https://your-host/sse --header "Authorization: Bearer your-secret-key"

Any other MCP-compatible client can connect using mcp-remote:

npx mcp-remote http://localhost:3001/sse

Or connect directly to the SSE endpoint at http://localhost:3001/sse if the client supports SSE servers natively.

Available Tools

Note Operations
ToolDescription
read-noteRead note contents (supports frontmatterOnly for efficiency)
read-multiple-notesBatch read multiple notes
create-noteCreate a new note
edit-noteReplace note contents
write-noteWrite with modes: overwrite, append, or prepend
delete-noteDelete a note (requires confirmation)
move-noteMove/rename a note
duplicate-noteCopy a note to a new location
get-notes-infoGet file metadata without reading content
Directory Operations
ToolDescription
list-vaultList files and directories
create-directoryCreate a new directory
delete-directoryDelete a directory (with recursive option)
rename-directoryRename/move a directory
Frontmatter & Tags
ToolDescription
get-frontmatterGet YAML frontmatter as JSON
update-frontmatterUpdate frontmatter fields in place (untouched fields keep their exact text; invalid YAML is refused)
add-tagsAdd tags to frontmatter or inline
remove-tagsRemove tags from note
list-tagsList all tags with counts
find-notes-by-tagFind notes with a specific tag
search-missing-tagFind notes missing a tag
audit-tagsAudit folder for required tags
ToolDescription
search-vaultFull-text search with context
get-backlinksFind notes linking to a note
find-broken-linksFind unresolved wiki-links
find-replaceBulk find and replace
Semantic Search (only when DATABASE_URL is set)
ToolDescription
semantic-searchHybrid vector + full-text search; filters by directory, tags, frontmatter type and status; `mode: hybridvectortext`
similar-notesNotes nearest to a note by centroid embedding
index-statusModel, counts, last indexer run and lag
reindex-noteRe-index one note immediately
search-feedbackRecord a search rating and used or expected paths; available only with the learn log enabled

See Semantic search for setup.

Section Operations
ToolDescription
read-sectionRead content under a heading
append-to-sectionAppend to a section
replace-sectionReplace section content
append-to-fileAppend to end of file
insert-at-markerInsert at a text marker
list-headingsList all headings in a note

Architecture

┌─────────────────┐     HTTPS/SSE      ┌──────────────────┐
│  Claude Desktop │ ◄────────────────► │   MCP Server     │
└─────────────────┘                    │   (Express.js)   │
                                       └────────┬─────────┘
                                                │
                                       ┌────────▼─────────┐
                                       │   VaultManager   │
                                       │   (File System)  │
                                       └────────┬─────────┘
                                                │
                                       ┌────────▼─────────┐
                                       │  Obsidian Vault  │
                                       │   (Markdown)     │
                                       └──────────────────┘

Project Structure

obsidian-mcp-server/
├── src/
│   ├── index.ts              # Express server entry point
│   ├── vault/
│   │   ├── VaultManager.ts   # Core vault operations
│   │   └── frontmatter.ts    # YAML parsing utilities
│   ├── tools/
│   │   ├── definitions.ts    # MCP tool schemas
│   │   ├── handlers.ts       # Tool execution logic
│   │   └── index.ts          # Tool exports
│   ├── server/
│   │   ├── mcp.ts            # MCP protocol handlers
│   │   └── middleware.ts     # Express middleware
│   ├── index/                # Optional semantic search
│   │   ├── config.ts         # DATABASE_URL / OLLAMA_URL / EMBED_MODEL
│   │   ├── exclusions.ts     # What the indexer never reads
│   │   ├── chunker.ts        # Heading-aware chunking
│   │   ├── embedder.ts       # Ollama client (+ a fake for tests)
│   │   ├── ranking.ts        # Fusion, weights, per-note cap and env validation
│   │   ├── store.ts          # pgvector schema, upserts, hybrid search
│   │   ├── scanner.ts        # Vault walk and change detection
│   │   └── SearchService.ts  # What the MCP tools call
│   ├── learn/                # Learn log, redaction, configuration and reports
│   ├── learn-report.ts       # Read-only learn-report CLI
│   ├── eval/                 # Golden validation, metrics and fixed ranking grid
│   ├── eval.ts               # Read-only brain-eval CLI entry point
│   ├── indexer.ts            # Indexer entry point (second container)
│   └── types/
│       └── index.ts          # TypeScript type definitions
├── sql/001_schema.sql        # Index schema (dimension substituted at start)
├── tests/                    # Vitest unit tests
├── Dockerfile
├── docker-compose.yml
├── package.json
└── README.md

Environment Variables

VariableDefaultDescription
PORT3000Server port
VAULT_PATH/vaultPath to Obsidian vault
COMPACT_RESPONSESfalseEnable minified response keys for 40-60% smaller responses
API_KEY(none)API key for authentication. When set, requires a Bearer token (compared in constant time)
REQUIRE_TAILSCALE_LOGIN(none)Optional. Require the Tailscale-User-Login header (set by tailscale serve) to equal this login. Only trustworthy when the server is reachable solely through tailscale serve on loopback
MCP_INSTRUCTIONS_FILE(none)Optional. Path to a text/Markdown file sent to clients as the MCP instructions in the initialize result: what this vault is and when to use it. Most clients add it to the model's context. A missing file is a startup error. Keep it short (some clients truncate around 2 KB)
MCP_INSTRUCTIONS(none)Same, inline. Ignored when MCP_INSTRUCTIONS_FILE is set
DATABASE_URL(none)Postgres + pgvector connection string. Enables semantic search; unset = feature off
OLLAMA_URLhttp://localhost:11434Ollama server for embeddings
EMBED_MODELnomic-embed-textOllama embedding model; dimension is discovered from it
LEARN_LOGfalseEnable the optional learn log; invalid configuration disables logging
LEARN_REDACT_PATTERNS_FILEunsetRequired readable regex file when logging is enabled
LEARN_RETENTION_DAYS30Days before query text, embeddings and feedback notes are cleared; integer 1–3650
INDEX_COLLECTIONdefaultName of this vault's collection in the shared database
SEARCH_FUSIONrrfFusion: rrf (weighted reciprocal rank) or dbsf (distribution-based score fusion)
SEARCH_RRF_K60RRF rank constant, integer 1–1000; validated but unused under DBSF
SEARCH_W_VECTOR1Vector-leg weight, finite number 0–10
SEARCH_W_TEXT1Full-text-leg weight, finite number 0–10; both weights cannot be zero
SEARCH_PER_NOTE_CAP0Maximum chunks per note, integer 0–8; 0 means no cap
INDEX_INTERVAL_SECONDS60Indexer only: seconds between vault scans
INDEX_VERBOSEfalseIndexer only: log every file indexed or removed
EMBED_BATCH_SIZE16Texts per Ollama request
EMBED_PROVIDERollamaollama, or fake (deterministic hash embeddings for tests and smoke runs; not for real search)
EMBED_DIM(auto)Embedding dimension for the fake provider (default 64); ignored with Ollama, where it is discovered from the model

Learn log

The optional measurement layer requires semantic search and is off by default. It records the three search tools (semantic-search, similar-notes, search-vault), successful reads (read-note, read-multiple-notes, read-section), explicit feedback and SSE session connect/close with client names. Search ranking is unchanged; search never reads these logs.

Set LEARN_LOG=true and LEARN_REDACT_PATTERNS_FILE to a readable UTF-8 file. Blank lines and comments are ignored, whitespace is trimmed, and each other line is a JavaScript regex source compiled case-insensitively. An example file is:

# comment
\bexample-term\b

Eight built-in patterns also detect private-key headers, access-key shapes, JWTs, hosted Git tokens, secret assignments, bearer credentials, URL token parameters and 64-character hex strings. Sensitive queries (including similar-note paths) and their embeddings are cleared before storage. Sensitive string parameters become [redacted]; sensitive feedback notes are cleared, sensitive feedback paths are dropped, and sensitive read paths are never logged. Site-specific patterns belong in the external file. An invalid configuration disables logging, never search, and is reported at startup.

Logged searches return an additive requestId. Pass it to a read's optional requestId argument, then call search-feedback with requestId, rating (positive, negative, neutral) and usedPaths. expectedPaths and a short note are optional. source defaults to agent; use user only for explicit human feedback. Unknown ids return { recorded: false, reason: 'unknown requestId' }. Reads with unknown ids are recorded without a link.

Search, read and session writes are fire and forget: they never delay tool responses and failures are logged with learn-log:. Feedback is awaited. The indexer loop applies retention at most daily, excluding --once. After LEARN_RETENTION_DAYS (default 30), query text, query embeddings and feedback notes are cleared. Paths, ranks, timestamps, checked parameters, client names and feedback paths remain. sql/002_learning.sql creates the four learn tables even when logging is off; the embedding column is recreated after a dimension reset. Keep learn-table data out of database dumps and exclude raw database files from backups when query retention is required.

Run the read-only report against an existing database:

node dist/learn-report.js --days 30 --out /existing-directory/learn-report.json

--days defaults to 30 (range 1–365); the output directory must already exist. It uses a read-only connection, ignores ranking and logging environment settings, and outputs counts and rates without query text. Stdout lists dotted keys in report order, followed by VOLUME GATE: pass|fail.

The report covers the selected collection and time window. Sessions include counts by client and the median closed duration in minutes. Searches include counts by tool and redacted share. Linked semantic reads use the minimum rank of the read path: usedAt1, usedAt3, usedAt8 are shares of linked reads, and missRate is the share absent from results. linkedSearchRate is the share of semantic searches with a linked read, and linkedSemanticPerDay is their daily count. Reformulation and fallback rates count session-linked semantic searches followed within 120 seconds by another semantic search or text search. Feedback coverage is the share of semantic searches with feedback; source counts and negative rates include all feedback. Zero-denominator rates and empty medians are null; numbers are rounded to four decimals. The volume gate passes at a rounded 30 linked semantic searches per day. Feedback from agents measures compliance; user corrections provide independent labels.

Off by default. Setting DATABASE_URL turns it on in both the server (four extra tools) and the indexer (a second process that keeps the index current). Everything runs locally: Postgres with the pgvector extension, and Ollama for embeddings.

How it works
  • The vault stays the source of truth. The index is a derivative you can drop and rebuild (--reindex).
  • Chunking is heading-aware. Notes split on H1–H3; sections longer than ~800 tokens split further with overlap; each chunk carries its breadcrumb (Title › H2 › H3). Chunk 0 is the title plus the frontmatter summary, so a one-line summary is the strongest signal for "which note answers this".
  • Retrieval is hybrid. Cosine similarity over an HNSW index and Postgres full-text search run side by side and are fused with configurable weighted reciprocal rank fusion (the default) or distribution-based score fusion (DBSF), so paraphrases and exact identifiers (hostnames, ticket numbers) both work.
  • Change detection is a poll, not a watcher, because inotify does not propagate over NFS or SMB. The indexer stats every file each interval, hashes only files whose mtime or size moved, and re-embeds only files whose content changed.
  • Exclusions are enforced by path before any read: .obsidian/, .trash/, .git/, Synology #recycle/ @eaDir/ @tmp/, AppleDouble ._*, dotfiles, non-.md files, any directory containing a .noindex file, and any note with frontmatter index: false.
  • Links resolve like Obsidian does. [[Areas/note]] resolves by path; [[note]] resolves if exactly one note in the vault has that basename, and stays unresolved (recorded, NULL target) when two do. Deleting one of two duplicates re-resolves the other the next time it is indexed.
  • The model is a schema fact. The embedding model and dimension are recorded in the database on first run; a different model refuses to index until --reindex.
Evaluating search quality (brain-eval)

Build with npm run build, then run with the same database, collection and embedding configuration as the index:

node dist/eval.js --golden /private/golden.json --grid --out /private/runs/grid.json
# Or evaluate one configuration alongside the baseline:
node dist/eval.js --golden /private/golden.json --config '{"wText":1.5,"perNoteCap":2}' --out /private/runs/candidate.json
# npm run eval -- <the same arguments> also works.

The output directory must already exist. The CLI opens the database read-only: it never applies schema changes, reindexes or writes to the index. The embedding model and dimension must match the stored metadata, and every expected note must already exist in the collection. Every query is embedded once and its vector reused across configurations. The fixed grid contains 40 configurations, starting with the v1.8.0 baseline rrf-k60-v1-t1-cap0. The CLI ignores ambient SEARCH_* ranking settings; --config merges only the supplied fields over the baseline. Invalid ranking environment settings fail server startup.

Golden files are private evaluation data, with exactly these keys:

{
  "version": 1,
  "created": "2026-01-01",
  "notes": "Synthetic example only",
  "queries": [
    {
      "id": "example-1",
      "category": "exact-id",
      "query": "synthetic search phrase",
      "expected": ["Example.md"]
    }
  ]
}

Query ids must be unique, query strings non-empty, and expected lists non-empty with unique .md paths. Categories are exact-id, paraphrase, decision-why, multi-hop, and recent, reported in that order. Unknown keys are rejected. Keep golden sets and evaluation reports outside a public code repository.

Metrics use unique note paths in result order, after requesting eight chunks:

  • mrr: mean reciprocal rank of the first expected note, or zero for a miss.
  • recall5: a hit rate at 5, meaning any expected note in the first five notes.
  • ndcg8: binary-relevance normalised discounted cumulative gain at eight notes.
  • distinctNotes: mean number of distinct notes returned. Metrics are also broken down by category, and the JSON report rounds them to four decimal places.

A candidate must improve MRR by at least 0.0300, preserve every category's hit rate, and preserve every query whose baseline hit was within five. Comparisons round metrics to integer ten-thousandths. Passing candidates are ordered by higher MRR, hit rate, then NDCG; ties prefer fewer changed ranking fields, then the lexicographically smaller id. If none passes, the baseline wins with changed: false. The JSON includes the golden file's SHA-256, per-query note lists, gate reasons, and the winning environment variables; stdout prints the ranking table, category comparison and final WINNER line.

Exit codes: 0 for a completed report (including no ranking change), 2 for input problems (arguments, golden validation, model mismatch or missing expected paths), and 1 for other runtime failures (database, embedding service or output writes).

Searches require pgvector 0.8.0 or newer: each transaction uses strict-order iterative HNSW scans and filtered searches raise hnsw.ef_search to 100. Default RRF scores match v1.8.0, with exact ties ordered by chunk position then id. DBSF scores range from zero to the sum of the two leg weights.

Frontmatter the index understands
---
title: ubuntubox — NFS mount gate     # else first H1, else file name
type: area                            # filterable: semantic-search { type }
status: active                        # filterable: semantic-search { status }
tags: [infra, nas]                    # filterable: semantic-search { tags }
summary: One line that answers "what is this note about".
index: true                           # false = never indexed
---

Invalid frontmatter is reported, not hidden. A note whose --- block is not valid YAML (the usual cause is an unquoted value containing :, such as summary: Finished it: all done; quote it: summary: "Finished it: all done") is still indexed by its body, but without its metadata. The indexer logs WARN invalid frontmatter in <path>: <YAML error> when it indexes such a note and again at every start, each pass line carries invalid_frontmatter=<count>, and index-status returns invalidFrontmatter: { count, notes: [{ path, error }] } (first 20). reindex-note returns a warning. Fix the note and the flag clears on the next pass.

Setup
  1. Database: any Postgres 16+ with pgvector. The schema is created on first start from sql/001_schema.sql; nothing to run by hand. HNSW index builds need shared memory: give the Postgres container shm_size: 1g.
  2. Embeddings: ollama pull nomic-embed-text (768-d, 8k context) or bge-m3 (1024-d).
  3. Server: add DATABASE_URL, OLLAMA_URL, EMBED_MODEL (and optionally INDEX_COLLECTION) to the server's environment. The /health endpoint reports semanticSearch: enabled.
  4. Indexer: run the same image again with command: ["node", "dist/indexer.js"], the same environment, and the vault mounted read-only. node dist/indexer.js --once --verbose does a single pass and exits non-zero if any file failed.
services:
  obsidian-mcp:
    image: ghcr.io/smith-and-web/obsidian-mcp-server:latest
    environment:
      VAULT_PATH: /vault
      API_KEY: ${API_KEY}
      DATABASE_URL: postgres://brain:${DB_PASSWORD}@brain-db:5432/brain
      OLLAMA_URL: http://ollama:11434
      EMBED_MODEL: nomic-embed-text
      INDEX_COLLECTION: secondbrain
    volumes:
      - /path/to/vault:/vault:rw
    ports: ["127.0.0.1:3001:3000"]

  obsidian-indexer:
    image: ghcr.io/smith-and-web/obsidian-mcp-server:latest
    command: ["node", "dist/indexer.js"]
    environment:
      VAULT_PATH: /vault
      DATABASE_URL: postgres://brain:${DB_PASSWORD}@brain-db:5432/brain
      OLLAMA_URL: http://ollama:11434
      EMBED_MODEL: nomic-embed-text
      INDEX_COLLECTION: secondbrain
      INDEX_INTERVAL_SECONDS: "60"
    volumes:
      - /path/to/vault:/vault:ro

  brain-db:
    image: pgvector/pgvector:0.8.2-pg16
    shm_size: 1g
    environment:
      POSTGRES_DB: brain
      POSTGRES_USER: brain
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./brain-db:/var/lib/postgresql/data

  ollama:
    image: ollama/ollama:latest
    volumes:
      - ./ollama:/root/.ollama
Indexer CLI
CommandDoes
node dist/indexer.jsRun forever, one pass every INDEX_INTERVAL_SECONDS; stops cleanly on SIGTERM
node dist/indexer.js --onceOne pass; exit 1 if any file errored
node dist/indexer.js --reindexDrop this collection and rebuild (required after changing EMBED_MODEL)
npx -p @smith-and-web/obsidian-mcp-server obsidian-mcp-indexerThe same indexer from the npm package (also accepts --once, --reindex, --verbose)
--verbose / INDEX_VERBOSE=trueLog every file indexed or removed
Security notes
  • The indexer needs only read access to the vault; mount it :ro.
  • Chunk text is stored in Postgres in clear. Treat the database as you treat the vault: same network boundary, same backup policy, and don't index material that should not share an access key with the rest of the vault (use index: false or a .noindex directory).
  • Embeddings never leave the Ollama host. No external API is involved anywhere in this feature.

API Endpoints

EndpointMethodDescription
/healthGETHealth check
/sseGETSSE endpoint for MCP
/ssePOSTDirect MCP protocol calls
/messagePOSTSSE transport messages

Authentication

The server supports optional API key authentication. When API_KEY is set, all /sse and /message endpoints require authentication. The /health endpoint remains public.

Enabling Authentication

Set the API_KEY environment variable:

# Docker
docker run -d \
  --name obsidian-mcp \
  -v /path/to/vault:/vault:rw \
  -p 3001:3000 \
  -e VAULT_PATH=/vault \
  -e API_KEY=your-secret-key \
  ghcr.io/smith-and-web/obsidian-mcp-server:latest

# npx
API_KEY=your-secret-key VAULT_PATH=/path/to/vault npx @smith-and-web/obsidian-mcp-server
Authentication Method

Clients send the key in the Authorization header:

Authorization: Bearer your-secret-key

The ?api_key= query parameter was removed in 1.6.0 (it ends up in access logs and client history). Clients that used it must switch to the header, for example mcp-remote with --header "Authorization: Bearer your-secret-key".

Tailscale login (optional second factor)

Behind tailscale serve on loopback, set REQUIRE_TAILSCALE_LOGIN=you@example.com. Requests then also need the Tailscale-User-Login header that serve adds for tailnet users; anything else gets 403. Do not rely on this if the port is reachable any other way, because the header can be forged by a direct client.

Client Configuration with API Key

Claude Desktop / Cursor (via mcp-remote):

{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://your-domain.com/sse",
        "--header", "Authorization: Bearer your-secret-key"
      ]
    }
  }
}

Claude Code:

claude mcp add --transport sse obsidian https://your-domain.com/sse --header "Authorization: Bearer your-secret-key"
Security Considerations
  • Generate strong keys: Use a random string of at least 32 characters
  • HTTPS required: Always use HTTPS when exposing the server remotely to prevent key interception
  • File Access: The server has full read/write access to the mounted vault, but every path is confined to it (paths that escape the vault, such as ../../etc/passwd, are rejected; fixed in 1.4.1)
  • Bind address: the server listens on all interfaces. In Docker, publish to loopback (127.0.0.1:3001:3000) unless remote access is intended
  • CORS: Currently allows all origins. Restrict in production if needed
  • Key in logs: only send the key in the Authorization header; the old ?api_key= query parameter was removed because URLs end up in access logs
  • Network security: Consider additional layers like VPN or firewall rules for sensitive vaults

Deployment with NGINX Proxy Manager

For remote access with SSL:

  1. Add a Proxy Host:
    • Domain: obsidian.yourdomain.com
    • Forward Hostname: obsidian-mcp (container name)
    • Forward Port: 3000
    • Websockets are not required (SSE is plain HTTP); the Advanced block below is what matters
  2. SSL Tab: Request Let's Encrypt certificate
  3. Advanced Tab (for SSE):
     proxy_buffering off;
     proxy_cache off;
     proxy_set_header Connection '';
     proxy_http_version 1.1;
     chunked_transfer_encoding off;

Private access with Tailscale

If your clients are on a Tailscale tailnet you can skip the public reverse proxy entirely. Keep the container on loopback and let tailscale serve publish it with a tailnet-only HTTPS certificate:

docker run -d --name obsidian-mcp -v /path/to/vault:/vault:rw -p 127.0.0.1:3001:3000 \
  -e API_KEY=your-secret-key -e REQUIRE_TAILSCALE_LOGIN=you@example.com \
  ghcr.io/smith-and-web/obsidian-mcp-server:latest
sudo tailscale serve --bg --https=443 http://127.0.0.1:3001

Clients then use https://<machine>.<tailnet>.ts.net/sse with the Authorization: Bearer header. Never enable Tailscale Funnel for this server. See Tailscale login for what REQUIRE_TAILSCALE_LOGIN does and does not protect against.

Troubleshooting

Container Issues
# View logs
docker-compose logs -f obsidian-mcp

# Restart
docker-compose restart

# Rebuild
docker-compose up -d --build
Vault Access
# Check host mount
ls -la /path/to/your/vault

# Check container access
docker exec obsidian-mcp ls -la /vault
Connection and auth errors
  • 401 Unauthorized: missing or wrong Authorization: Bearer <key> header (the ?api_key= query parameter is no longer accepted).
  • 403 Forbidden: REQUIRE_TAILSCALE_LOGIN is set and the request did not arrive through tailscale serve as that user.
  • Path escapes vault: the requested path resolves outside VAULT_PATH; use a path relative to the vault.
  • Search results are missing a note's summary, tags or type filters: check index-status for invalidFrontmatter (or the indexer log for WARN invalid frontmatter); the note's YAML is invalid and was indexed without its metadata.
  • SSE connection drops behind a reverse proxy: turn proxy buffering off (see the NGINX block above), check the certificate, and verify the firewall allows the port.
  • Claude Desktop starts but shows no tools: the server must already be running and the config must use mcp-remote (the server has no stdio mode).

Development

Quick Start
# Clone and install
git clone https://github.com/smith-and-web/obsidian-mcp-server.git
cd obsidian-mcp-server
make install

# Run with example vault
make dev

# Test connection
make test-connection
Available Commands

Run make help to see all available commands:

Development:
  make install         Install dependencies
  make dev             Run server with hot-reload
  make start           Run server in production mode
  make test-connection Test server connectivity

Docker:
  make docker-build    Build Docker image
  make docker-up       Start Docker container
  make docker-down     Stop Docker container
  make docker-logs     View container logs
  make docker-restart  Restart container
  make docker-shell    Open shell in container
API Testing with Bruno

The repository includes a Bruno collection for testing the core tools (the four semantic-search tools need DATABASE_URL and a running index).

  1. Install Bruno (free, open-source API client)
  2. Open the collection from ./bruno/obsidian-mcp
  3. Select the local or remote environment
  4. Run requests to test each tool

The collection is organized by category:

  • health/ - Server health and tool listing
  • notes/ - Note CRUD operations
  • directories/ - Directory operations
  • frontmatter/ - Frontmatter operations
  • tags/ - Tag management
  • search/ - Search and find-replace
  • links/ - Backlinks and broken links
  • sections/ - Section-based operations
Example Vault

An example vault is included in ./examples/test-vault/ for development and testing. It includes sample notes with tags, links, and sections to test all features.

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE for details.

Frequently asked questions

What is Obsidian MCP Server?

Remote Obsidian vault management over SSE: notes, directories, frontmatter, tags, search and link operations, deployable with Docker.

How do I install Obsidian MCP Server?

Follow the installation steps in its documentation, shown on this page and in its repository.

Is Obsidian MCP Server open source?

Yes. Its code is on GitHub under the MIT license.

Who maintains Obsidian MCP Server?

It is published by smith-and-web. AI.tn lists it but does not maintain or endorse it.

From the community awesome-mcp-servers lists and the project's GitHub repository. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.