Obsidian MCP Server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Obsidian MCP Serversearch my vault for notes about MCP"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
|
|
|
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 |
|
|
|
The restart-race fix actually prevents the server from ending up stopped when it should be running |
|
The note-path validator rejects absolute paths, |
|
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 inscripts/obsidian-mock.mjsonly stubs 2-3 methods well enough to smoke-test tool registration, it is not a faithful vaultobsidian_run_commandandobsidian_get_periodic_note, which both use undocumented internal Obsidian APIs (app.commands,app.internalPlugins) not present in the publicobsidian.d.tsat all — these are common patterns in the plugin ecosystem, but they can break across Obsidian versions without noticeThe Dataview integration (
obsidian_dataview_query), which similarly readsapp.plugins.plugins.dataview.apivia an internal-API castReal 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_writefor 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
resourcestemplate (obsidian://vault/{path}), and 3prompts— 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'snode: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 buildThis 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 |
Bind host | Default |
Auth token | Required. The server refuses to start without one. Clients must send |
Enable embedding search | Off by default. Sends candidate note text to the embedding endpoint below on every |
Embedding endpoint / API key / model | An OpenAI-compatible |
GitHub Personal Access Token | Required for all |
GitHub default owner/repo | Used when a |
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.1by 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.tsrejects any request whoseHostheader isn't127.0.0.1,localhost, or the configured bind host — unless the bind host is0.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 othergithub_*write tools are all markeddestructiveHint: truein 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
.mdpaths. 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.mdare 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 truncationWhy 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_notetakes optionaloffset(0-based character offset) andlimitparameters to page through a large note manually. Its response includes both a human-readable text marker ([TRUNCATED] {"truncated":true,"totalCharacters":...,"nextOffset":...}) and a structuredstructuredContentobject ({ path, content, totalCharacters, truncated, nextOffset }) — call again withoffset: nextOffsetto get the next page. A call withoutoffset/limitstill works exactly as before, it now just also gets capped and marked if the note happens to be large.obsidian_get_active_noteandobsidian_get_periodic_noteare capped the same way, but with no offset/limit parameters — useobsidian_read_notewith anoffsetto 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^), viametadataCache's block index.frontmatter: matches a YAML frontmatter key, viaapp.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 sequencenpm run verify (scripts/run-verify.mjs) runs, in order:
Script | What it tests | Needs the Obsidian mock? |
|
| No — real Node |
|
| No — pure function, no |
| Full | Yes — mocked |
| The real | Yes — mocked |
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 |
| Read a note's raw markdown by path, with optional |
| Create or fully overwrite a note |
| Surgically edit a heading section, block, or frontmatter key |
| Permanently delete a note |
| Rename/move a note, updating internal links |
| List note paths, optionally filtered by folder prefix |
| Keyword, local TF-IDF ("semantic"), or optional embedding search |
| Notes that link to a given note |
| Notes a given note links to |
| Every tag in the vault with the notes using it |
| Read up to 200 notes in one call |
| Create/overwrite up to 200 notes in one call |
| The note currently open in the editor |
| Today's daily note (daily period only; see caveats above) |
| Execute an Obsidian command by id (internal API, see caveats above) |
| Run a DQL query — only registered when Dataview is installed and enabled |
| Fetch a file's decoded content + sha |
| Commit a new/updated file via the Contents API |
| List files/folders at a path (one level) |
| Open a new issue |
| List issues (PRs excluded) |
| 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.
This server cannot be installed
Maintenance
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
- Alicense-qualityCmaintenanceEmbeds 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.1113MIT
- Flicense-qualityDmaintenanceA 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
- Alicense-qualityDmaintenanceAn Obsidian plugin that runs an MCP server, enabling AI agents to read, edit, search notes, and run Dataview queries in your vault.3BSD Zero Clause
- Alicense-qualityAmaintenanceAn 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.1332MIT
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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