feat(mcp): proxy the remote MCP server over stdio using the CLI login - #1026
Merged
Conversation
Adds `railway mcp proxy`: a stdio bridge to mcp.railway.com that attaches a fresh Bearer token from the stored `railway login` on every request, so MCP-capable editors can use the remote server without the OAuth (DCR + browser consent) flow and without long-lived credentials in editor configs. - Token freshness delegates to ensure_valid_token (lockfile-serialized, so the proxy coexists with the local `railway mcp` server); a proxy started logged-out completes the handshake with a login hint and heals on the next request after `railway login`, no restart needed. - SSE responses are re-framed as newline-delimited JSON-RPC via a single writer task; concurrent tool calls after the handshake; transparent re-initialize + one retry on session loss; bounded 5s shutdown so a stalled upstream can't orphan the process; JSON-RPC batch error paths answer every id. - `railway mcp install` gains a three-way transport: --remote now installs the proxy, --remote --oauth keeps the plain-URL editor OAuth flow, and the config matchers distinguish local/proxy/oauth entries (a bare "args contains mcp" check would misdetect proxy installs as local). - Same choices plumbed through `railway setup agent`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security review of the credential-injecting proxy. The token-handling core was already sound (destination fixed at startup and not derivable from server/harness input; reqwest strips the Bearer on cross-host redirect, verified against the 0.12.28 source; TLS enforced outside Dev; no path stringifies the credential). Closing the residual robustness/defense-in-depth gaps it found: - Bound every upstream response to MAX_RESPONSE_BYTES (32 MiB). reqwest's text()/bytes() have no size cap and the proxy sets no overall timeout, so a compromised edge or dev/http-override MITM streaming a boundary-less SSE body (or one giant event/body) could OOM a long-lived proxy. stream_sse and a new read_body_capped helper now abort past the ceiling. - redirect(Policy::none()): an MCP JSON-RPC POST is never legitimately redirected. The Bearer is stripped cross-host, but refusing redirects outright also closes the same-host 307/308 body-resend and the redirected-response relay edges. - Validate RAILWAY_MCP_URL: reject non-https outside Dev so the override can't send the Bearer over plaintext (the first hop isn't covered by the cross-host strip). Logic extracted to validate_mcp_override + unit test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
Author
Security review (pre-merge)Ran a dedicated threat-model pass on the proxy as a credential-handling component, since it injects a live OAuth Bearer on every request. Verified the redirect/header behavior against the actual reqwest 0.12.28 source, not from memory. Core is sound — no changes needed:
Hardened in
34 tests pass; re-verified live streaming against |
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.
What
Adds
railway mcp proxy: a local stdio bridge tomcp.railway.comthat attaches a fresh Bearer token from the storedrailway loginon every request. MCP-capable editors (Claude Code, Cursor, Codex, OpenCode, Copilot, Factory Droid) can use the remote MCP server without the OAuth flow (dynamic client registration + browser consent) and without long-lived credentials in editor configs.Today an editor pointed at
mcp.railway.comruns the full MCP OAuth dance. For developers who already ranrailway login, that's a redundant second auth ceremony — and pasting a token into an editor config can't substitute for it because remote access tokens expire hourly. The proxy makes the existing CLI login be the auth.How it works
ensure_valid_token(the same lockfile-serialized refresh the rest of the CLI uses), so the proxy coexists safely with the localrailway mcpserver and other CLI processes.railway login" on each call; the next request after login succeeds automatically.Install surface
railway mcp installgains a three-way transport:--remote→ installs the proxy (railway mcp proxy), the new no-prompt default for CLI users.--remote --oauth→ keeps the plain-URL, editor-managed OAuth flow (for environments without the CLI: web clients, hosted agents, teammates who never installedrailway).Config matchers distinguish local / proxy / oauth entries (a bare "args contains
mcp" check would misdetect a proxy install as local). Same three choices are plumbed throughrailway setup agent.Testing
cargo buildclean;cargo test mcp— 33 passing (SSE boundary/chunk-split parsing, stdout framing, batch handling, three-way matchers across JSON/opencode-array/codex-TOML config shapes).mcp.railway.comthrough the built binary: initialize → 27 tools → live tool calls.Scope
MCP files only — deliberately excludes the unrelated
railway codework.🤖 Generated with Claude Code