2026-07-25 Remote Obsidian MCP Access Session Rollover

The one-month timer set in 2026-06-22 Remote MCP One-Month Access Sessions came due, and it presented as an outage.

What I set out to do

The remote Obsidian MCP server on athena had stopped responding. Working from atlas, figure out what broke on the host.

What I actually did

Nothing was broken on the host. Walked the request path outward and every hop was healthy:

  • athena up 21 days, reachable over the tailnet, load ~1.3.
  • The mcp-proxy bridge listening on 127.0.0.1:8788, with mcpvault alive underneath it against the iCloud vault. A local initialize returned HTTP 200 in 9ms.
  • cloudflared holding three registered tunnel connections. Last reconnect 2026-07-23 04:25 UTC, clean since, with only daily “version outdated” warnings after (pinned at 2026.6.1 against 2026.7.3 upstream).

The real signal was the shape of the 401 from obsidian-mcp.achhina.com: a well-formed www-authenticate: Bearer realm="OAuth", error="invalid_token" challenge. That is Cloudflare Access reporting a missing client token, not a dead origin. A broken Cloudflare Tunnel would have returned 530 instead.

The bridge log dated the break precisely, since it records every session: the last genuine ListToolsRequest was 2026-07-24 01:56. Client-side, the connector was exposing only authenticate and complete_authentication instead of the vault tools, which is the direct tell that the token is gone.

Re-auth from the phone errored on the first attempt and succeeded instantly on the second. Two fresh sessions then landed on the bridge at 15:57:36 and 15:57:37, each followed by a ListToolsRequest.

What was striking

  • A healthy 401 looks like an outage. The status code alone says nothing; the www-authenticate challenge is what separates “your token expired” from “your origin is down.” I spent the first several checks host-hunting on a box that was fine the whole time.
  • The first re-auth failing may be self-healing. At the session boundary the first attempt has to run the full Claude to Access to Google to Access to callback chain, which is fragile in a mobile in-app browser. It errored, but still set the Cloudflare session cookie, so the retry skipped the IdP entirely. Retry once before debugging anything.
  • launchctl last-exit is not a fault. The tunnel agent showed a nonzero previous exit, which read as a crash. It is just the prior exit code, and cloudflared reconnects routinely.
  • Discount your own probes when reading logs. A bare initialize opens a transport but logs no ListToolsRequest, so my diagnostic traffic was visibly distinct from a real client’s.
  • The monthly rollover is working exactly as designed. It just has no signal attached, so it arrives looking like a failure every time.

Follow-up the same day

Bumped cloudflared 2026.6.1 to 2026.7.3 on athena via Homebrew, then kickstarted the launch agent (brew cleanup deletes the old Cellar path while the running process still has it mapped, so the restart is mandatory, not cosmetic). All four connections re-registered.

That surfaced real drift: athena’s MCP setup is entirely outside Nix. Its plists are plain files rather than store symlinks, its wrappers are hand-written ports in ~/.local/bin calling Homebrew binaries, and home.nix:63-67 still claims the retired NixOS box owns the tunnel. Hence brew upgrade instead of a flake bump.

Explored the service-token approach and reversed my own conclusion mid-investigation. First read said it was blocked: Cloudflare service tokens need CF-Access-Client-Id/Secret, and Claude’s connector request headers are restricted to an allowlist (authorization, x-api-key, x-auth-token). But Cloudflare supports reading a service token from a single header via the app setting read_service_tokens_from_header: "Authorization", with the value as JSON. That path exists precisely for SaaS clients that can only send one custom header, and authorization is on the allowlist. So it is viable after all.

The real gate turns out to be on the Claude side, not Cloudflare’s: request-header auth is beta (“contact Anthropic for early access”), and Authorization cannot be a request header on an OAuth connection, so the connector would have to be re-added as header-auth-only.

What was striking (follow-up)

  • The blocker was one layer off from where I first put it. I had a coherent, well-sourced argument that the header allowlist killed this. It was wrong because I had not checked whether Cloudflare offered an alternative header shape. Confirming the constraint on one side says nothing about the other side’s flexibility.
  • Only claude.ai is Access-gated at all. Claude Code (mcp.nix:38-40) and Open WebUI both hit 127.0.0.1:8788 directly, so a service token would secure the clients that already bypass Access and, without the beta, do nothing for the one that re-auths.
  • Service token durations go up to Non-expiring, which trades a monthly tap for a permanent shared secret living in a cloud connector config. Not obviously an upgrade.

Decision: stay on 1-month interactive Access

Verified all three timers in the dashboard rather than trusting the month-old note. No drift: global = 1 month, app = 1 month, policy “Allow owner” = “Same as application session duration” (defers rather than overriding). Effective cadence is the shortest of the three, so one month.

Then checked whether the app duration could simply be raised to a year. It cannot. The dropdown offers exactly nine values and stops at 1 month: No duration, expires immediately / 15 min / 30 min / 6 h / 12 h / 24 h / 1 week / 2 weeks / 1 month. No year, no “never”. Raising it alone would be capped by the global timer anyway.

That exposed the shape of the real choice. Interactive Access maxes out at 1 month; service tokens start at 1 year. There is nothing in between, so this was never a dial to turn, only a jump to a static credential.

Declined the service token. Only claude.ai is Access-gated at all, so a token would harden the clients that already bypass Access via loopback while trading a near-silent monthly tap for a long-lived shared secret in a cloud connector config, plus an API-only app setting invisible in the dashboard. The monthly cadence was never the defect. Its silence was.

Top tomorrow

  1. Absent-data alert on the bridge so the next rollover (~2026-08-24, rolling from today’s re-auth) announces itself instead of looking like an outage.
  2. Optionally reconcile athena’s hand-rolled setup back into obsidian-remote-mcp.nix.
  3. Revisit service tokens only if a genuinely headless client starts hitting the public hostname.