Skip to main content
Glama
aytacmehmet

Obsidian MCP Server

by aytacmehmet

Obsidian MCP Server

An Obsidian plugin that runs an embedded Model Context Protocol server (Streamable HTTP) inside Obsidian, exposing your vault — plus an optional GitHub integration — to any MCP client (Claude Desktop, Claude Code, the MCP Inspector, etc.).

Written from scratch against the MCP TypeScript SDK; it does not reuse any code from obsidian-local-rest-api, though it targets a superset of that plugin's MCP capabilities (see "What this adds" below).

What's verified vs. what's assumed

This was built in an environment with no real Obsidian install, so nothing here was tested inside an actual vault. What is verified, and how:

Claim

How it was checked

The whole project compiles under TypeScript strict mode

npx tsc --noEmit — exits 0, and a deliberately-broken canary line was confirmed to actually fail the check first (so this isn't a silently-noop config)

esbuild produces a valid Obsidian plugin bundle

npm run build → inspected main.js (654KB, correct banner, require("obsidian")/require("electron") left external as expected, Node built-ins (node:http, node:crypto, node:events) resolved)

All 21 tools, the resource template, and all 3 prompts register with correct names/schemas/annotations; pagination, URI encoding, path validation, batch partial-failure, and embedding-response validation behave as documented

npm run verifyverify-tools.ts — spins up the real McpServer from src/server/mcpServer.ts against a mocked App (see scripts/obsidian-mock.mjs), connects a real MCP Client over an in-memory transport pair, and makes real listTools/listResources/listPrompts/callTool/readResource calls with assertions (not just printed output). This validates the MCP-protocol layer; it does not validate real Obsidian API behavior (see below).

StreamableHTTPServerTransport.handleRequest works against a bare node:http server (no Express); auth, method/Host-header rejection, EADDRINUSE handling, and safe double-close all behave correctly

npm run verifyverify-http.ts — a real, unmocked integration test (neither httpTransport.ts nor auth.ts imports obsidian) that starts real servers on a real port and sends real HTTP requests, including deliberately triggering a real port conflict

The restart-race fix actually prevents the server from ending up stopped when it should be running

npm run verifyverify-restart.ts — instantiates the real ObsidianMcpPlugin class (mocked App, real HTTP port) and fires concurrent restartServer()/startServer()/stopServer() calls, asserting the server is reachable/unreachable at each expected point

The note-path validator rejects absolute paths, .. traversal, .obsidian/ paths, and non-.md paths, while leaving normal paths untouched

npm run verifyverify-notepath.ts — a real, unmocked unit test (notePathValidation.ts has no obsidian import)

Not verified — you need to do this yourself, in a real vault:

  • Loading the plugin in Obsidian, enabling it, and confirming the settings tab renders correctly

  • All Obsidian API calls (app.vault.*, app.metadataCache.*, app.fileManager.processFrontMatter, app.workspace.getActiveFile) against real files — the mock in scripts/obsidian-mock.mjs only stubs 2-3 methods well enough to smoke-test tool registration, it is not a faithful vault

  • obsidian_run_command and obsidian_get_periodic_note, which both use undocumented internal Obsidian APIs (app.commands, app.internalPlugins) not present in the public obsidian.d.ts at all — these are common patterns in the plugin ecosystem, but they can break across Obsidian versions without notice

  • The Dataview integration (obsidian_dataview_query), which similarly reads app.plugins.plugins.dataview.api via an internal-API cast

  • Real GitHub API calls (never run in this session — no PAT was used, no requests were made)

  • The embedding-search code path (never called an actual embedding endpoint)

Related MCP server: obsidian-local-mcp

What this adds over the baseline reference

  • Event-driven cache: a TF-IDF search index kept current via vault.on('modify'|'create'|'delete'|'rename') instead of rescanning the vault per search.

  • Batch tools: obsidian_batch_read / obsidian_batch_write for up to 200 notes per call.

  • Local semantic search with no external dependency (TF-IDF cosine similarity), plus an optional OpenAI-compatible embedding re-ranking mode — off by default, see Security below.

  • Graph awareness: backlinks, outgoing links, and a vault-wide tag graph via metadataCache.

  • Dataview integration, gracefully absent (not erroring) when Dataview isn't installed.

  • Full MCP surface: tools, a resources template (obsidian://vault/{path}), and 3 prompts — not tools only.

  • GitHub integration: read/write files, list repo contents, create/list issues, get repo info.

Requirements

  • Obsidian desktop (this plugin is isDesktopOnly: true — it uses Node's node:http/node:crypto, unavailable on mobile)

  • Node.js ≥ 18 only for building the plugin from source; end users just copy the built files into their vault

Installing (from source, on your own machine)

git clone <your-fork-url> obsidian-mcp-server
cd obsidian-mcp-server
npm install
npm run build

This produces main.js at the project root, alongside manifest.json. Then, in your vault:

mkdir -p "<YourVault>/.obsidian/plugins/obsidian-mcp-server"
cp main.js manifest.json versions.json "<YourVault>/.obsidian/plugins/obsidian-mcp-server/"

In Obsidian: Settings → Community plugins → disable Restricted mode (if on) → reload plugins → enable "MCP Server".

Configuring

Open Settings → MCP Server:

Setting

Notes

Port

Default 27123. Restart the server (see below) after changing.

Bind host

Default 127.0.0.1 (localhost-only). 0.0.0.0 exposes it to your network and disables the built-in Host-header check — only do this on a trusted network.

Auth token

Required. The server refuses to start without one. Clients must send Authorization: Bearer <token>.

Enable embedding search

Off by default. Sends candidate note text to the embedding endpoint below on every embedding-mode search. Local semantic (TF-IDF) mode always works offline regardless of this toggle.

Embedding endpoint / API key / model

An OpenAI-compatible /embeddings endpoint.

GitHub Personal Access Token

Required for all github_* tools.

GitHub default owner/repo

Used when a github_* tool call omits owner/repo.

Settings changes are saved immediately but do not hot-reload the running server (so typing a token doesn't restart it on every keystroke). Run the command "Restart MCP server" from the Command Palette, or disable/re-enable the plugin, after changing port/host/token.

Restart behavior and error messages

Start/stop/restart are serialized through an internal lock, so triggering "Restart MCP server" more than once in a row (or while the plugin is still starting up) can't interleave and leave the server stopped when it should be running, or vice versa — each operation waits its turn and runs against the actual current state.

Startup failures are reported clearly instead of silently: the plugin waits for the HTTP server to actually start listening (or fail) before showing any Notice.

  • Port already in use: "Port 27123 is already in use on 127.0.0.1. Another process (or another copy of this plugin) is already listening there — pick a different port in plugin settings, or stop whatever else is using it."

  • Permission denied (binding to a port below 1024 without elevated privileges): a similar explicit message suggesting a port above 1024.

  • Restart failure: shown as "MCP Server: restart failed — <reason>".

close()-ing an already-stopped or never-started server is safe and does not throw.

Security notes (read this before using GitHub or embedding features)

  • The auth token and the GitHub PAT are stored in plain text in this plugin's data.json (.obsidian/plugins/obsidian-mcp-server/data.json). Obsidian has no encrypted secret storage available to community plugins. Anyone with filesystem access to that folder can read both. Use a fine-grained GitHub PAT scoped to only the repos you intend this plugin to touch.

  • The server binds to 127.0.0.1 by default. Only widen this (0.0.0.0) on networks you trust — it also disables the Host-header DNS-rebinding check described below.

  • DNS-rebinding protection is hand-rolled, not from a framework. Because this project deliberately avoids Express (see Architecture below), there's no createMcpExpressApp() to lean on. src/server/httpTransport.ts rejects any request whose Host header isn't 127.0.0.1, localhost, or the configured bind host — unless the bind host is 0.0.0.0, in which case you've opted out.

  • Embedding search (off by default) sends note text to whatever endpoint you configure — capped at the first 8000 characters of each of up to 50 candidate notes per query, with a 15-second timeout (best-effort: it stops waiting on a slow endpoint, it does not cancel the in-flight request) and strict validation of the response shape (right number of embeddings, all numeric, consistent dimensions) before any result is trusted. Keyword and semantic (TF-IDF) search never leave your machine.

  • obsidian_delete_note, obsidian_write_note (full overwrite), obsidian_run_command, github_create_issue, and the other github_* write tools are all marked destructiveHint: true in their MCP annotations — a well-behaved MCP client should surface that to the user before calling them, but don't rely on it as your only safety net.

  • Note tools only accept vault-relative .md paths. Every read/write/delete/rename/patch/batch operation is validated before it touches the filesystem: empty paths, absolute paths, .. traversal segments, . segments, anything under .obsidian/ (Obsidian's own config folder), and anything not ending in .md are all rejected with a specific error (e.g. path traversal ('..') is not allowed) rather than silently normalized or, worse, acted on. This closes off using the note tools to read/write/delete arbitrary files on disk or other plugins' config.

Connecting an MCP client

The server listens at http://<bindHost>:<port>/mcp (POST only; GET/DELETE return 405). Example for Claude Desktop's claude_desktop_config.json (Streamable HTTP over mcp-remote, since Claude Desktop's built-in config format expects a local command):

{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://127.0.0.1:27123/mcp",
        "--header",
        "Authorization: Bearer YOUR_TOKEN_HERE"
      ]
    }
  }
}

For any MCP client that speaks Streamable HTTP natively, just point it at the URL above with that same Authorization header — no proxy needed.

Architecture

src/
├── main.ts               # Plugin lifecycle: settings load/save, vault event wiring, server start/stop
├── settings.ts            # SettingTab UI
├── types.ts                # ObsidianMcpSettings + shared result types
├── server/
│   ├── mcpServer.ts         # Builds one McpServer per HTTP request, registers all tools/resources/prompts
│   ├── httpTransport.ts     # Bare node:http server + StreamableHTTPServerTransport (no Express)
│   ├── auth.ts               # Bearer-token check (constant-time compare)
│   └── deps.ts                 # Shared dependency-injection interface for tools/resources/prompts
├── vault/
│   ├── cache.ts               # Event-maintained TF-IDF SearchIndex
│   ├── noteService.ts          # CRUD + heading/block/frontmatter patch
│   ├── searchService.ts         # keyword / semantic (TF-IDF) / embedding search
│   ├── graphService.ts           # backlinks / outgoing links / tag graph
│   ├── batchService.ts            # batch read/write
│   └── internalPlugins.ts          # Escape hatch for undocumented app.plugins/app.commands/app.internalPlugins
├── github/githubService.ts    # GitHub REST client (via Obsidian's requestUrl, no CORS issues)
├── tools/                     # One file per tool domain, registerTool() calls
├── resources/noteResources.ts # obsidian://vault/{path} resource template
├── prompts/prompts.ts         # daily-note-summary, create-note-from-template, weekly-review
└── shared/toolHelpers.ts      # safeHandler() error wrapper, pagination, char-limit truncation

Why a fresh McpServer per HTTP request?

The transport runs in stateless mode (sessionIdGenerator: undefined) per this project's architecture requirements. That has a real consequence for the resources' list_changed notification: it's wired to vault events (src/resources/noteResources.ts), but because each POST request gets its own McpServer that's torn down right after the response, there's no persistent connection for a server-push notification to reach between requests. The notification code is correct and will fire for a client holding a live connection during that one request, but most MCP clients reconnect per-turn anyway, so treat "list resources on demand" as the reliable source of truth rather than relying on push notifications. This tradeoff — stateless HTTP vs. persistent notification channels — is inherent to the architecture spec this plugin was built against, not an oversight.

Large content: pagination and truncation

Every tool/resource/prompt that can return a note's raw content caps it, by default, at 25,000 characters (DEFAULT_CHAR_LIMIT in src/shared/toolHelpers.ts) so one big note can't blow past a client's context window or balloon latency:

  • obsidian_read_note takes optional offset (0-based character offset) and limit parameters to page through a large note manually. Its response includes both a human-readable text marker ([TRUNCATED] {"truncated":true,"totalCharacters":...,"nextOffset":...}) and a structured structuredContent object ({ path, content, totalCharacters, truncated, nextOffset }) — call again with offset: nextOffset to get the next page. A call without offset/limit still works exactly as before, it now just also gets capped and marked if the note happens to be large.

  • obsidian_get_active_note and obsidian_get_periodic_note are capped the same way, but with no offset/limit parameters — use obsidian_read_note with an offset to read past the cap.

  • The obsidian://vault/{path} resource is capped the same way (resources have no offset/limit parameter in the MCP spec, so a truncated resource read just carries the same text marker).

  • Prompts (daily-note-summary, create-note-from-template, weekly-review) cap each note they embed at 8,000 characters, so a prompt combining several large notes doesn't grow unbounded either.

Patch semantics (obsidian_patch_note)

  • heading: matches a heading's text case-insensitively; the "section" is that heading line through the next heading of equal-or-shallower level (or end of file).

  • block: matches a block reference id (without the leading ^), via metadataCache's block index.

  • frontmatter: matches a YAML frontmatter key, via app.fileManager.processFrontMatter.

Development

npm run typecheck   # tsc --noEmit
npm run build        # production bundle -> main.js
npm run dev            # esbuild watch mode
npm run verify           # runs all four test scripts below in sequence

npm run verify (scripts/run-verify.mjs) runs, in order:

Script

What it tests

Needs the Obsidian mock?

verify-http.ts

httpTransport.ts + auth.ts for real: auth (missing/wrong/correct token), method/Host-header rejection, a real EADDRINUSE port conflict and recovery, safe double-close()

No — real Node http, no obsidian import at all

verify-notepath.ts

notePathValidation.ts for real: valid paths pass through, empty/absolute/traversal/.obsidian/non-.md paths are rejected with specific messages

No — pure function, no obsidian import

verify-tools.ts

Full McpServer over a real in-memory MCP Client: tool/resource/prompt registration, an annotation check, obsidian_read_note pagination (30,000-char note, two pages), a resource URI round-trip with #/spaces/Turkish characters, non-.md/traversal rejection through the actual tools, mixed-success batch reads, and direct embedding-response-shape validation (5 malformed payloads + 1 valid one)

Yes — mocked App (scripts/obsidian-mock.mjs)

verify-restart.ts

The real ObsidianMcpPlugin class against a real HTTP port: server reachable after onload(), still reachable after two concurrent restarts, still reachable after an interleaved stop+start+restart, and unreachable (port freed) after onunload()

Yes — mocked App

What none of this proves: real Obsidian API behavior (app.vault/app.metadataCache/app.fileManager against actual files, real TFile/normalizePath semantics), the settings tab UI rendering, or the undocumented-internal-API tools (obsidian_run_command, obsidian_get_periodic_note, obsidian_dataview_query) against a real Obsidian instance — none of that can run outside the Obsidian desktop app itself. Test in a real vault before relying on this in production.

Tool reference

Tool

Description

obsidian_read_note

Read a note's raw markdown by path, with optional offset/limit paging for large notes

obsidian_write_note

Create or fully overwrite a note

obsidian_patch_note

Surgically edit a heading section, block, or frontmatter key

obsidian_delete_note

Permanently delete a note

obsidian_rename_note

Rename/move a note, updating internal links

obsidian_list_notes

List note paths, optionally filtered by folder prefix

obsidian_search_notes

Keyword, local TF-IDF ("semantic"), or optional embedding search

obsidian_get_backlinks

Notes that link to a given note

obsidian_get_outgoing_links

Notes a given note links to

obsidian_get_tag_graph

Every tag in the vault with the notes using it

obsidian_batch_read

Read up to 200 notes in one call

obsidian_batch_write

Create/overwrite up to 200 notes in one call

obsidian_get_active_note

The note currently open in the editor

obsidian_get_periodic_note

Today's daily note (daily period only; see caveats above)

obsidian_run_command

Execute an Obsidian command by id (internal API, see caveats above)

obsidian_dataview_query

Run a DQL query — only registered when Dataview is installed and enabled

github_get_file

Fetch a file's decoded content + sha

github_create_or_update_file

Commit a new/updated file via the Contents API

github_list_repo_contents

List files/folders at a path (one level)

github_create_issue

Open a new issue

github_list_issues

List issues (PRs excluded)

github_get_repo_info

Basic repo metadata

Plus the obsidian://vault/{path} resource template and the daily-note-summary / create-note-from-template / weekly-review prompts described above.

License

GNU General Public License v3.0 only (GPL-3.0-only) — see the LICENSE file for the full text. In short: you can use, modify, and redistribute this plugin, but any distributed derivative work must also be licensed under GPL-3.0 and its source made available.

Third-party licenses in the bundled main.js: npm run build bundles this project's code together with its dependencies (@modelcontextprotocol/sdk, zod, and their transitive deps like @hono/node-server) into one file. Those dependencies are MIT-licensed, which is compatible with GPL-3.0 distribution, but MIT requires its copyright notice to be preserved in redistributed copies. This repository does not currently ship a generated third-party-notices file for main.js — if you plan to publish this plugin publicly (e.g. to the Obsidian community plugin directory), run a license report (e.g. npx license-checker --production) and add a THIRD-PARTY-NOTICES.md before distributing the built artifact. Flagging this now rather than silently skipping it.

Before publishing: manifest.json's author/authorUrl fields and package.json's author field are still empty placeholders — fill those in with your own name/contact before distributing.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    -
    quality
    C
    maintenance
    Embeds an MCP server directly within Obsidian to provide applications with streamlined access to vault operations including file management, fuzzy search, and structured data creation with custom schemas.
    11
    13
    MIT
  • F
    license
    -
    quality
    D
    maintenance
    A local MCP server that wraps the Obsidian CLI to give AI assistants direct access to read, edit, and manage notes within an Obsidian vault. It enables advanced operations such as frontmatter property management, context-aware searching, and the execution of internal Obsidian commands.
    2
  • A
    license
    -
    quality
    D
    maintenance
    An Obsidian plugin that runs an MCP server, enabling AI agents to read, edit, search notes, and run Dataview queries in your vault.
    3
    BSD Zero Clause
  • A
    license
    -
    quality
    A
    maintenance
    An Obsidian plugin that runs an MCP server, enabling external LLM tools to read, search, create, and modify notes in your vault via HTTP or stdio transport.
    13
    32
    MIT

View all related MCP servers

Related MCP Connectors

  • An MCP server that gives your AI access to the source code and docs of all public github repos

  • Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer

  • Markdown-first MCP server for Notion API with 8 composite tools and 39 actions.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aytacmehmet/obsidian-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server