Add remote HTTP serve mode: bearer-token auth, headless login, and Claude connector support#79
Open
arditti wants to merge 21 commits into
Open
Add remote HTTP serve mode: bearer-token auth, headless login, and Claude connector support#79arditti wants to merge 21 commits into
arditti wants to merge 21 commits into
Conversation
…ust test teardown
`monarch-mcp-server login-cookies` reads a browser Cookie header from stdin, verifies it against the live Monarch API, and saves the session — login_setup.py option 1 without the prompts, for remote hosts where the session must be seeded via docker exec from a CI runner. Stdin keeps the cookie out of argv and shell history; nothing derived from it is printed. Also make ServeConfig tolerate a set-but-absent MONARCH_MCP_AUTH_TOKEN_FILE (fall back to the env token, or fail with the path in the error) so the container can point at a token file that an init step creates after first boot instead of crashing on the read. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSu34KX26gPeYmdZz6Dfs
`monarch-mcp-server login-password` reads newline-separated credentials from stdin (email, password, optional one-time code), logs in via the current-auth flow, verifies against the live API, and saves the session — login_setup.py option 2 without the prompts, for the same headless docker-exec deployments login-cookies serves. Monarch's new-device email OTP is handled as a two-run flow: the first attempt triggers the email and exits with instructions, the re-run passes the code as line 3 (a TOTP MFA code uses the same slot). CAPTCHA blocks point the operator at the cookie flow. The verify-and-save tail is factored out of login_cookies for reuse, and the conftest monarchmoney stub gains a real CaptchaRequiredException class (an except clause on a MagicMock is a TypeError). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSu34KX26gPeYmdZz6Dfs
The two-run OTP flow was broken: even when handed a code, the login did
a no-code attempt first to detect the challenge — but that attempt makes
Monarch send a fresh email OTP, invalidating the code the operator just
supplied ("Invalid code" every time). Now a supplied code rides the
initial login call (as email_otp, falling back to the TOTP slot if
Monarch signals MFA), so no fresh email is triggered and the operator's
code is the one actually used.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sSu34KX26gPeYmdZz6Dfs
Renders the Claude Code command, Claude Desktop config JSON, MCP URL, and bearer token as ready-to-copy blocks, with copy buttons. No auth of its own — this path is meant to sit behind Cloudflare Access at the edge (home-infra wires that up), same trust model as the existing /healthz custom route. Reads the token live off disk each request (via the new ServeConfig.auth_token_file field) rather than the value cached at process start, so a same-container rotation shows up without a restart. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSu34KX26gPeYmdZz6Dfs
Confirmed no route conflict: FastMCP's streamable-HTTP transport only claims settings.streamable_http_path (/mcp) plus .well-known/* OAuth metadata routes, never bare "/" — verified with a live local server (GET / -> 200 with the setup page, POST /mcp still 401 without a token / 200 with one). Added integration tests locking this in. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSu34KX26gPeYmdZz6Dfs
Claude's native "Add custom connector" UI (web, desktop, mobile) only takes a URL — no custom-header field like Claude Code's CLI has, and leaving its OAuth fields blank triggers a full OAuth discovery/dynamic- registration flow that a static-bearer-token resource server can't answer. mindbody-mcp already solved this the same way in its own hand-rolled HTTP layer (?token= query param, Authorization header still authoritative when present); the MCP SDK's built-in BearerAuthBackend only reads the header, so this adds a small ASGI middleware (QueryTokenAuthMiddleware) that copies ?token= into the Authorization header before FastMCP's own auth check runs — the mcp package itself is untouched, still a stock general-purpose dependency. Wiring this in required switching serve mode from app.mcp.run(transport= "streamable-http") to running the (now-wrapped) ASGI app via uvicorn directly, since FastMCP's run() has no hook to inject middleware. Kept the existing _app_loader-style test seam by adding a parallel _serve_http injection point. The /setup page's first block is now a ready-to-paste connector URL (https://.../mcp?token=<token>) with a one-line note that OAuth Client ID/Secret should stay blank. Verified with a live local server: query token succeeds (200), wrong query token 401s, and a present-but-wrong Authorization header is NOT overridden by a valid query token (still 401) — also covered by new unit tests (middleware in isolation) and integration tests (real subprocess server, real HTTP). 281 tests passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sSu34KX26gPeYmdZz6Dfs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
servesubcommand/run mode exposing the server over MCP streamableHTTP (in addition to the existing stdio mode — unaffected), authenticated
with a static bearer token, so it can be deployed on a VPS/Raspberry
Pi/Docker host and reached remotely behind a reverse proxy or tunnel.
Dockerfile,docker-compose.yml,.dockerignore)and a Cloudflare Tunnel example for running the server remotely.
mcpupgraded to>=1.28,<2so streamable HTTP sessions are bound totheir credentials (fixes a session-replay gap present in 1.13.1).
global failure throttle to resist timing/brute-force attacks; the
interactive login tools are only registered in stdio mode — credentials
never flow through an LLM in serve mode.
login-cookies/login-passwordnon-interactive subcommands: seed aMonarch session from a browser Cookie header or email+password (with
email-OTP/TOTP support) piped over stdin, for headless hosts where
login_setup.py's interactive prompts aren't available (e.g.docker execfrom a CI runner). A supplied one-time code now rides the firstlogin request — a bug where a no-code probe attempt would trigger a
fresh OTP email and invalidate the code just supplied is fixed.
/(root): an optional self-service copy-paste client-config page —Claude Code command, Claude Desktop config JSON, a ready-to-paste
connector URL, and the raw bearer token. It has no auth of its own, so
deployers should put it behind their own access control (reverse-proxy
auth, an identity-aware proxy, etc.) if serving to more than themselves.
?token=query-param bearer-token fallback: Claude's native "Add customconnector" UI (web/desktop/mobile) only accepts a URL, not a custom
header, and leaving its OAuth fields blank triggers a full OAuth
discovery/dynamic-registration flow this static-token resource server
doesn't implement. Implemented as a small ASGI middleware wrapping
FastMCP's app (checked only when no Authorization header is present, so
an explicit header always wins) — the
mcpSDK itself is untouched.Design docs included
docs/superpowers/specs/2026-07-08-remote-http-phase1a-design.md—design spec (reviewed for security, SDK feasibility, and architecture)
docs/superpowers/plans/2026-07-08-remote-http-phase1a.md—implementation plan
Test plan
verifier, HTTP auth-boundary (real subprocess server, real HTTP),
secure session, login-cookies/login-password (including the OTP-timing
fix), the setup page, and the query-token middleware (unit + live-server
integration).
scripts/e2e_http.py— end-to-end check against a running serverusing the real MCP client.
reverse proxy/tunnel, live Monarch login, and the native-connector
?token=flow all confirmed working end to end.