MCP Router Bridge
unlistedby tboome33
Adds /search/smart and /templates/execute REST routes to the Local REST API plugin, delegating to Smart Connections and Templater. Companion plugin for the multi-vault MCP router. No native binary, no telemetry.
obsidian-mcp-router-bridge
A minimal Obsidian community plugin that adds four REST routes to the Local REST API plugin:
| Route | Auth | Delegates to | Used by |
|---|---|---|---|
POST /search/smart | Bearer | Smart Connections — semantic search via vector embeddings | obsidian-mcp-router search_smart tool |
POST /templates/execute | Bearer | Templater — render a template, optionally write to a new file | obsidian-mcp-router execute_template tool |
GET /open/<path> | None (loopback-only, public route) | Obsidian's workspace.openLinkText — navigate to a vault file | Click-to-open links from Claude Code chat / any client emitting clickable http URLs |
GET /ping | None (loopback-only, public route) | Nothing — returns a bare {"pong":true}; optional ?v=<vault-name> answers 404 unless the name matches this vault | Smart-link resolver pages probing for a local mirror — see Presence heartbeat + /ping |
It also runs a presence heartbeat that advertises this device as an active local mirror — see Presence heartbeat + /ping.
Why this exists
The companion router project obsidian-mcp-router needs two REST routes — /search/smart and /templates/execute — to expose semantic search and Templater execution as MCP tools. Local REST API doesn't ship those routes natively; this plugin adds them on top, in the smallest, most boring way possible.
What this plugin does not ship:
- ❌ Any bundled native executable
- ❌ A built-in MCP server (the router handles that, externally)
- ❌ Any telemetry or remote calls
What it does:
- ✅ Four REST handlers that delegate to plugins / Obsidian APIs you already have (Smart Connections + Templater + Obsidian workspace navigation)
- ✅ A
tp.mcpTools.prompt("key")accessor inside Templater templates — used by the router to inject arguments into rendered templates - ✅ A no-auth loopback-only
GET /open/<path>for clickable http links — see Click-to-open below - ✅ A no-auth loopback-only
GET /ping+ a 5-minute presence heartbeat for smart-link device detection — see Presence heartbeat + /ping below
Install
Manual install (until accepted in the community plugins marketplace)
# 1. Build the plugin
git clone https://github.com/tboome33/obsidian-mcp-router-bridge.git
cd obsidian-mcp-router-bridge
npm install
npm run build
# 2. Copy the built artifacts to your vault's plugins folder
# (replace <VAULT> with your vault's absolute path).
# The folder name MUST match the `id` in manifest.json — `mcp-router-bridge`.
mkdir -p "<VAULT>/.obsidian/plugins/mcp-router-bridge"
cp main.js manifest.json "<VAULT>/.obsidian/plugins/mcp-router-bridge/"
# 3. Restart Obsidian, enable the plugin in:
# Settings → Community plugins → MCP Router Bridge
Migrating from v0.1.0? The plugin ID was renamed from
obsidian-mcp-router-bridgetomcp-router-bridgein v0.1.1 to comply with Obsidian's community-plugin naming policy ("obsidian" is not allowed in plugin IDs since it's redundant). After installing v0.1.1 to the new folder, delete the legacy<VAULT>/.obsidian/plugins/obsidian-mcp-router-bridge/folder. Restart Obsidian. The plugin's settings (none currently) and behavior are unchanged.
Click-to-open
GET /open/<vault-relative-path> opens a file in Obsidian when hit. No Bearer token required — this is a addPublicRoute() registration (Local REST API v3.x+ feature). Designed for surfacing wiki pages from clients that emit clickable http(s) links (Claude Code CLI, browsers, etc.) where obsidian:// URIs aren't dispatched.
How to use
https://127.0.0.1:<port>/open/<URL-encoded-vault-path>
Example for wiki/references/router-agents.md in a vault whose Local REST API runs on port 27132:
https://127.0.0.1:27132/open/wiki%2Freferences%2Frouter-agents.md
A click → browser GETs the URL → bridge calls app.workspace.openLinkText → Obsidian navigates to the file → browser tab shows a tiny "Opened in Obsidian" page that attempts to auto-close (browser-dependent).
Bring Obsidian to the front (foreground on click) — v0.5.0+
By default, clicking an http /open link foregrounds your browser, not Obsidian — the note opens correctly but in the background. This is the Windows foreground-activation lock, not a fixable Electron quirk: a background app cannot steal the foreground from the browser that just received the click (app.focus({steal:true}) is "give focus, never take it" by design; setAlwaysOnTop/moveTop/minimize+restore only reorder z-order — all empirically confirmed to fail against a freshly-clicked Chrome). The bridge always calls flashFrame(true) so the taskbar icon blinks, and on macOS/Linux a best-effort app.focus() does bring the window forward — but on Windows the only reliable, native-code-free way to foreground Obsidian is to let the OS protocol handler do it.
Enable it in two steps (Windows):
-
Plugin setting — Settings → MCP Router Bridge → "Bring Obsidian to the front on /open (obsidian:// redirect)" (default OFF, per-vault — enable it in each vault where you want foreground-on-click). When ON, the
/openresponse page redirects toobsidian://open?vault=<this-vault>(vault-only — it just focuses the already-navigated window, it never re-navigates). The OS — not the background renderer — performs the activation, bypassing the foreground lock. -
Chrome policy — without this, Chrome shows an "Open Obsidian?" dialog on every click (the per-site "always allow" checkbox was removed in Chrome 77). Pre-authorize
obsidian://from loopback origins withAutoLaunchProtocolsFromOrigins, set it once (no admin needed for the per-user hive), then restart Chrome so it loads the policy:# Windows, per-user (HKCU). The :* wildcard covers every vault's bridge port. New-Item -Path 'HKCU:\SOFTWARE\Policies\Google\Chrome' -Force | Out-Null New-ItemProperty -Path 'HKCU:\SOFTWARE\Policies\Google\Chrome' ` -Name 'AutoLaunchProtocolsFromOrigins' -PropertyType String -Force ` -Value '[{"protocol":"obsidian","allowed_origins":["http://127.0.0.1:*"]}]' | Out-Null(Machine-wide: same value under
HKLM\…\Policies\Google\Chrome— needs admin. Edge:…\Policies\Microsoft\Edge. Verify atchrome://policyafter the restart.)⚠️ The policy is origin-scoped, not bridge-specific. It pre-allows
obsidian://launches from anyhttp://127.0.0.1page in that browser profile, not just this bridge. Residual risk is low —obsidian://is navigation-only and any local process can already invoke it via the OS shell — but it's broader than the one route. To scope it tightly, replacehttp://127.0.0.1:*with the exact bridge origins (one per vault port, e.g.http://127.0.0.1:27163).
With both in place, clicking a click-to-open link foregrounds Obsidian silently — zero extra clicks. Leave the setting OFF (the default) if you'd rather not set a browser policy: the note still opens in the background and the taskbar flashes.
Security model
- Loopback-only. Local REST API binds 127.0.0.1 by default; the handler additionally checks
req.ipas defense-in-depth and refuses non-loopback requests. - No auth. The scope is intentionally minimal — navigation only, no content read, no write, no execution. Other processes running locally as the same user could already read the vault directly via the filesystem; this route doesn't expand their attack surface.
- Path traversal refused.
..segments, absolute paths, Windows drive letters all return 403. - File must exist.
getAbstractFileByPathreturns null → 404.
Why no Bearer token: a click navigation cannot attach an Authorization header, and embedding the token into the URL would expose it in browser history and clipboard. Localhost + minimal scope makes the unauth registration the right trade-off here.
HTTPS cert warning
Local REST API ships HTTPS with a self-signed cert by default. First click per port shows a browser warning ("Not Secure / Advanced / Continue"). Subsequent clicks within the same browser session are fine. To eliminate the warning, enable the HTTP server in Settings → Local REST API and use that port instead of the HTTPS one.
Requirements
mcp-router-bridge≥ v0.2.0 installed and enabled in the vault.- Local REST API version that exposes
addPublicRoute()(v3.x recent — if not available, the bridge logs a warning at load and skips this route; the other two routes still work normally).
Verify
After enabling, hit the Local REST API root and confirm the bridge appears in apiExtensions:
# Replace 27124 with the port shown in your Local REST API plugin settings.
# 27124 is the default; obsidian-mcp-router users will typically have a
# different port per vault (set by setup-vault.mjs).
curl -sk -H "Authorization: Bearer <api-key>" "https://127.0.0.1:27124/" | grep -A 4 mcp-router-bridge
Or call a route directly:
curl -sk -X POST \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"query":"trading","filter":{"limit":3}}' \
"https://127.0.0.1:27124/search/smart"
Presence heartbeat + /ping (smart links)
Two small pieces (v0.4.0) that let a smart-link resolver decide, at click time and on the clicking device, whether to open a note in a local Obsidian mirror or fall back to a deep link / online view:
-
Presence heartbeat. The plugin writes
wiki-meta/presence/<deviceId>.jsononce at layout-ready and every 5 minutes:{ "device": "desktop-abc123", "vaultName": "MyVault", "insecurePort": 27163, "lastSeen": "2026-06-10T12:00:00.000Z", "bridgeVersion": "0.4.0" }deviceIdis the sanitized machine hostname (or a persisted random id). The file is replicated to the server-side vault copy by Self-hosted LiveSync; the resolver reads the replicated presences to learn which devices have a live mirror and which port to probe (entries older than ~10 minutes are considered stale).LiveSync requirements (load-bearing): the path is deliberately visible — LiveSync does not replicate hidden (dot-prefixed) paths by default — and your LiveSync config must sync non-markdown files (
.json) for the presence to reach the server.Toggle: Settings → MCP Router Bridge → "Presence heartbeat (smart links)" (default ON). Vaults that don't use smart links can switch it off; the stale presence file then simply expires server-side.
-
GET /ping— the probe target. The resolver page (an https page running on the clicking device) doesfetch('http://127.0.0.1:<port>/ping')for each fresh presence port; the first pong wins and the page redirects to that device's/open/<path>. Registered as a public route (no Bearer token — a cross-origin fetch can't attach one), loopback-only like/open, and answering with CORS/PNA headers (Access-Control-Allow-Origin: *,Access-Control-Allow-Private-Network: true,Access-Control-Allow-Methods: GET, OPTIONS,Cache-Control: no-store).Vault confirmation — optional
?v=<vault-name>. On a multi-vault device, every vault runs its own bridge on its own port, and a candidate port from the presence files can belong to a different vault than the one the link targets — a blind pong would make the resolver open the wrong note. Probing with?v=<vault-name>(URL-encoded) makes the pong vault-aware: the route answers200 {"pong":true}only if the URL-decoded name strictly equals this vault's name (app.vault.getName()), and404otherwise — with the same four headers, so the resolver's CORS fetch can read the status either way. Without?v, behavior is unchanged (bare pong). This is a confirmation semantic, not disclosure: the route never reveals its vault name, it only confirms a name the requester already knows (it came from the presence file / the link itself).Privacy posture: the response body is
{"pong":true}and nothing else — no vault name, no plugin version, no port list. A prober learns only "a bridge listens on this port", which the open TCP port reveals anyway (and with?v=, "this port serves the vault I already named" — the 404 body is empty, nothing is echoed). Presence files contain no keys or secrets (device name, port, vault name, timestamp, version)./pingis registered on both of Local REST API's servers, but the resolver probes the insecure (HTTP) server port advertised by the presence file (an https page can fetchhttp://127.0.0.1— loopback is exempt from mixed-content blocking, except on Safari where the cascade falls back as designed). SoenableInsecureServer: truemust be set in Local REST API for probes to succeed (the heartbeat warns once in the console if it isn't).
Pre-requisites in the target vault
| Plugin | Required for | Why |
|---|---|---|
| Local REST API | all routes | Provides the HTTPS server we register against |
| Smart Connections | /search/smart only | Semantic search backend |
| Templater | /templates/execute only | Template engine |
Each route returns HTTP 503 with a clear error message if its dependency is missing — graceful degradation rather than crashing the bridge.
Migration from jacksteamdev/obsidian-mcp-tools
If you currently have MCP Tools installed in this vault:
- Disable MCP Tools in Settings → Community plugins.
- Install and enable this bridge (see Install above).
- Restart Obsidian (recommended so the route registry is rebuilt cleanly).
- The two route paths are identical —
obsidian-mcp-routerrequires no changes. - (Optional) Uninstall MCP Tools to remove the bundled binary from disk.
The tp.mcpTools.prompt("key") accessor inside Templater templates is preserved verbatim, so any templates authored against the original plugin keep working.
API reference
POST /search/smart
Request body (accepted as either real JSON or a JSON-stringified payload in text/plain — the router sends the latter):
{
"query": "rules for breakeven and trailing stop",
"filter": {
"folders": ["Sessions", "Trades"], // optional, restrict to these prefixes
"excludeFolders": [".trash", "Templates"], // optional, skip these prefixes
"limit": 10 // optional, default whatever Smart Connections returns
}
}
Response (200):
{
"results": [
{
"path": "Sessions/2026-04-29.md#Session 2026-04-29#Trades du jour",
"text": "...", // surrounding chunk content
"score": 0.82, // cosine similarity, 0..1
"breadcrumbs": "Sessions > 2026-04-29 > Session 2026-04-29 > Trades du jour"
}
]
}
Errors:
- 400 —
{"error":"Invalid request body","summary":"..."}for malformed input - 503 —
{"error":"Smart Connections plugin is not available", "hint":"..."}if the dependency isn't loaded
POST /templates/execute
Request body (application/json, real object — NOT stringified):
{
"name": "Templates/Trade.md",
"arguments": { "ticker": "AAPL", "direction": "long" },
"createFile": true, // optional, default false (preview only)
"targetPath": "Trades/2026-05-03 - AAPL Long.md" // required if createFile is true
}
Inside the template, the arguments map is exposed at:
<% tp.mcpTools.prompt("ticker") %>
Note: tp.mcpTools.prompt(...) — accessed directly under tp, NOT under tp.user (which is the convention for Templater user scripts). Easy footgun — copy/paste from a Templater tutorial expecting tp.user.* won't find anything.
Response (200):
{
"message": "Prompt executed and file created successfully",
"content": "# AAPL\n\n..."
}
Errors:
- 400 — invalid body (missing
name, missingtargetPathwhencreateFile: true, etc.) - 404 — template file not found in the vault
- 503 — Templater plugin not available, or template execution threw
Development
npm install
npm run dev # esbuild watch mode, rebuilds on file change
npm run build # one-shot production build (minified, no sourcemap)
npm run deploy # build + copy main.js + manifest.json to your reference vault's
# .obsidian/plugins/mcp-router-bridge/ folder
The build emits main.js at the repo root. Combined with manifest.json, that's all Obsidian needs.
npm run deploy finds your reference vault by reading referenceVault from
~/.claude/obsidian-mcp-router/config.json (the obsidian-mcp-router
config file). Set the OBSIDIAN_TEMPLATE_VAULT environment variable to override.
After deploying, propagate to vaults that already have the plugin installed:
# For each consumer vault — re-clones plugins, preserves data.json:
node "<obsidian-mcp-router>/scripts/setup-vault.mjs" "<vault>" --sync-plugins --force
Then disable+re-enable the plugin in each Obsidian instance, or run "Reload app without saving" from the command palette.
License
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.