2026-06-21 Remote Obsidian MCP Connector

What I set out to do

Turn the local Obsidian MCP (the @bitbonsai/mcpvault stdio server that only works on this Mac) into a remote MCP I can add as a custom connector from the Claude iOS/Desktop app. Started as an open exploration of “what’s possible and recommended,” not a fixed plan.

What I actually did

Worked it end to end with Claude over a long session.

Design. Brainstormed and wrote a spec + plan. Key constraint: a custom connector means Anthropic’s cloud connects to the server, not the phone, so it must be publicly reachable, speak Streamable HTTP (not stdio), and do OAuth. Chose Option A: keep the server on the Mac (reads the live iCloud vault), expose it via a Cloudflare Tunnel, and gate it with Cloudflare Access + OAuth. Rejected the cloud-hosted option (would need a vault sync pipeline). Everything managed declaratively in nix home-manager, matching how programs.mcp already works.

Build (mostly via Playwright driving the Cloudflare dashboard).

  • New nix module obsidian-remote-mcp.nix: a stdio→Streamable-HTTP bridge + cloudflared tunnel, both as launchd agents.
  • Created the obsidian-mcp tunnel, stored its token in ~/.config/secrets/.secrets, routed obsidian-mcp.achhina.com → localhost:8788.
  • Auth: a self-hosted Access application on that hostname with Managed OAuth and a single-email policy. Corrected course mid-flight — Cloudflare’s docs say a self-hosted app (not an MCP portal) is the right pattern for one server; the portal would have left the raw tunnel hostname open as a back door.
  • Identity provider: set up Google — created a GCP OAuth client + consent screen and published the app to production.
  • Debugged the connector’s first failure: Claude’s Dynamic Client Registration was rejected because the Managed-OAuth allowed_uris list was empty. Added Claude’s claude.ai/claude.com callbacks → registration returned 201 → connector worked.
  • Added an Obsidian favicon via a Cloudflare Single Redirect (which, usefully, runs before Access).

Security review (asked Claude to harden everything):

  • Caught a real hole: supergateway binds all interfaces, so the unauthenticated bridge was reachable on the LAN (http://<mac-ip>:8788), bypassing Access entirely. Swapped the bridge to mcp-proxy, which binds 127.0.0.1 only.
  • Redacted the tunnel token that had leaked into the session transcript (jq in place).
  • Turned off DCR localhost/loopback (only Claude’s explicit callbacks remain).
  • Deleted the leftover MCP-portal objects from the abandoned approach.
  • Enabled Google login on the existing achhina.com Access apps too.

Made it permanent. Once the pre-existing hm/Zotero copyApps blocker was fixed, deployed so the bridge + tunnel run as managed launchd agents (survive reboot, no more manual nohup). Finally removed the now-redundant local stdio Obsidian server from Claude Desktop (kept it for the CLI/Codex).

What was striking

  • supergateway has no host-binding flag — silent all-interfaces exposure. The kind of thing that looks fine (“serves on localhost”) until you check the listening socket. mcp-proxy defaults to loopback.
  • Self-hosted Access app + Managed OAuth is the clean single-server pattern; the MCP portal is for aggregating many and would’ve left a back door.
  • Cloudflare dash API mutations are CSRF-protected (GETs work, PUT/DELETE 403), so most changes went through the UI via Playwright. Authenticated fetch reads were the workhorse for verification.
  • Cloudflare Single Redirects execute before Access — the favicon redirect worked without any auth bypass.
  • RFC 7396 merge needs an explicit null to delete a key — omitting obsidian from the desktop patch wasn’t enough; had to set obsidian = null.
  • .secrets has zsh-only vars, so wrapper scripts must use set -e, not set -eu.

Top 3 tomorrow

  1. Reconnect the obsidian MCP in this Claude Code session (/mcp) or restart — to restore the in-session vault tools.
  2. Restart Claude Desktop so it drops the local stdio obsidian server.
  3. Phone read + write round-trip sanity check (already effectively confirmed by Inbox/Test Note from Phone.md).
  4. Optionally rotate the Google client secret (briefly visible in a setup screenshot).

Update — later same session

Kept going after the first write:

  • Acted on the security review: the LAN-binding fix (mcp-proxy on loopback), transcript token redaction, DCR tightening, and deletion of the abandoned MCP-portal objects.
  • Made it permanent: once the pre-existing hm/Zotero copyApps blocker was fixed, the bridge + cloudflared now run as declarative launchd agents (RunAtLoad + KeepAlive, survive reboot) — no more manual nohup.
  • keepAwake = true: the caffeinate agent holds a PreventSystemSleep assertion, so the Mac won’t sleep on AC power and the connector stays reachable. (caffeinate -s is AC-only; on battery it still sleeps — -i if I ever want battery too.)
  • Permissions: added Bash(cloudflared *) to the nix-managed Claude allowlist for tunnel management.
  • Removed the redundant local stdio obsidian server from Claude Desktop (kept for the CLI/Codex).
  • Commits on main: 74e3ce7, 2e98e49, bfad4db, 8cd6ede.

Debugging note — the obsidian MCP “failure”. While writing this journal, every mcp__obsidian__* call failed. Chased it down: mcpvault and the vault are completely healthy — a fresh stdio launch listed all 15 tools and returned vault stats (version 0.11.4). The fault was this Claude Code session’s connection to the server: it disconnected and reconnected under a new id mid-session — most likely because repeated hm runs regenerated ~/.config/mcp/mcp.json, prompting Claude Code to tear down and reconnect the server — and the new handle was stale. Fix is a session-level /mcp reconnect or restart. I’d first (wrongly) blamed a mcpvault version regression; that was a misread of its serverInfo string. Lesson: a dead MCP connection looks identical to a broken server — launch the server standalone before blaming it. (This journal was written via direct file write as the fallback.)

Follow-ups