Skip to content

Proxy MCP OAuth callbacks through command center#70

Open
mlund01 wants to merge 1 commit into
mainfrom
oauth-proxy-command-center
Open

Proxy MCP OAuth callbacks through command center#70
mlund01 wants to merge 1 commit into
mainfrom
oauth-proxy-command-center

Conversation

@mlund01
Copy link
Copy Markdown
Owner

@mlund01 mlund01 commented Apr 18, 2026

Summary

  • New WsbridgeCallbackSource routes MCP OAuth callbacks through command center instead of a local loopback port, so remote squadrons and HTTPS-only IdPs can complete OAuth.
  • command_center.urlcommand_center.host: a single base URL drives both the WS connection (wss://host/ws) and the OAuth redirect URI (https://host/oauth/callback). Legacy url field rejected with a migration error.
  • Routing uses the OAuth state value, which lets the redirect URI stay fixed per command center deployment — also fixes the current "re-register DCR on every ephemeral port" workaround.
  • Supports both CLI and UI initiation: squadron mcp login still uses loopback (CLI has no live bridge); the command center UI gets a "Connect" button per MCP server that calls POST /api/instances/{id}/mcp/{name}/oauth/start.

Paired with:

TEMP notice

go.mod has a local replace for squadron-wire until the sibling PR merges and a tag is cut. Will be removed + bumped before merge.

Test plan

  • go test ./config/... ./wsbridge/... ./mcp/oauth/... green
  • ./squadron engage --foreground comes up clean, GET /oauth/callback?state=X responds with the expected error page
  • End-to-end with a real MCP server (e.g. Linear): click "Connect" in UI → IdP → callback returns to command center → squadron stores token in vault
  • Verify CLI squadron mcp login still works for local standalone use

Adds a second CallbackSource implementation (WsbridgeCallbackSource) that
routes the IdP redirect through the command center a squadron is
connected to, rather than binding a local loopback port. Motivation:

- Squadrons running on remote hosts can't receive a browser redirect on
  127.0.0.1.
- Some IdPs (Slack, enterprise SSO) reject plain-HTTP loopback redirects
  or require a pre-registered HTTPS callback URL. Command center already
  terminates TLS, so it's the natural place to host a stable callback.

Routing is by OAuth `state` value (always cryptographically random and
per-flow), which means the redirect URI is fixed per command center
deployment and doesn't rotate on every login — fixing the existing
"re-register DCR on every port change" workaround in flow.go.

Config changes:

- command_center.url → command_center.host. Host is just the scheme +
  domain (plus an optional path prefix if command center is mapped behind
  one). Squadron derives WebSocketURL (http(s) → ws(s), /ws appended) and
  OAuthRedirectURI (scheme preserved, /oauth/callback appended) from it.
- Legacy `url` field is rejected with a clear migration error.

New surfaces:

- mcp/oauth/callback_wsbridge.go — WsbridgeCallbackSource.
- mcp/oauth/flow.go — CallbackSource gained SetState(state); generated
  earlier in RunLoginFlow so the bridge source can register its listener
  before the IdP handshake.
- wsbridge/client.go — public SendRequest, OAuth listener plumbing.
- wsbridge/handlers.go — handlers for OAuthCallbackDelivery and
  StartMCPLogin. StartMCPLoginHook is a package-level indirection so
  cmd/engage can wire the oauth package without an import cycle.
- cmd/oauth_hook.go — installs the StartMCPLoginHook, bridging
  commander-initiated logins to RunLoginFlow.

The standalone `squadron mcp login` CLI still uses the loopback source
because the CLI process has no live WS bridge. UI-initiated logins from
command center use the proxy path.

TEMP: go.mod has a local replace pointing at ../squadron-wire for the
new protocol message types. Revert + bump once squadron-wire tags a new
version (see sibling PR).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant