Skip to content

feat(mcp): OAuth authorization for HTTP MCP servers - #199

Merged
stippi merged 8 commits into
mainfrom
feature/mcp-oauth-flow
Sep 15, 2026
Merged

stippi merged 8 commits into
mainfrom
feature/mcp-oauth-flow

Conversation

@stippi

@stippi stippi commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds OAuth authorization support for HTTP (streamable) MCP servers — the "Authenticate" flow that servers behind SSO require. Previously we only supported static Authorization headers, so such servers could not be connected at all.

Implements MCP's authorization spec (OAuth 2.1 authorization-code + PKCE, discovery via RFC 9728 → RFC 8414, dynamic client registration) on top of rmcp's auth machinery, mirroring what Cline's "Authenticate" button does.

What's included

  • rmcp 2.1 → 3.3 and its auth feature enabled. Only break was the test ServerHandler::call_tool return type (CallToolResultCallToolResponse).
  • Reactive connect: try unauthenticated, and on the server's 401 challenge either reuse a stored token (non-interactive, transparent refresh) or surface a typed AuthorizationRequired so callers can offer an Authenticate action.
  • Interactive login (authenticate_http_server) driven by an OAuthAuthorizer seam; tokens persist via a FileCredentialStore (<config_dir>/mcp-oauth/<server>.json, atomic write, 0600).
  • Embedder wiring: ConfigToolRegistry passes a per-server credential store, so an authenticated server is reused silently by agent runs and by tool discovery.
  • CLI: code-assistant mcp-login <server> (loopback callback per RFC 8252 + browser) and mcp-logout <server>.
  • Settings UI: an "Authenticate" button when an HTTP server needs OAuth; runs the login in the background and re-discovers so its tools appear.
  • Cache: the per-run tool-registry fingerprint now includes the persisted OAuth token state, so completing a login rebuilds the registry and the next run sees the freshly authenticated server without restarting.

Testing

  • cargo check --workspace --tests, cargo fmt --check, cargo clippy (touched crates) green.
  • New/updated unit + integration tests: credential store round-trip, 401 → AuthorizationRequired detection, stdio-rejects-auth guard, loopback bind, registry rebuild on token change. mcp_client and code_assistant_core suites pass.

Notes / possible follow-ups

  • Only Dynamic Client Registration is wired up; CIMD / pre-registered clients could be added via AuthorizationRequest options if a server requires them.

Migrate the test ServerHandler::call_tool to the new CallToolResponse
return type (wrap CallToolResult via From). No behavioural change to the
client; this only prepares for enabling rmcp's OAuth auth feature.
Enable rmcp's 'auth' feature and introduce the reusable pieces the MCP
authorization flow needs on top of the transport:

- FileCredentialStore: a rmcp CredentialStore that persists one server's
  OAuth tokens to a JSON file (atomic write, 0600 on unix) so an
  interactive login survives restarts.
- OAuthAuthorizer trait + AuthorizationOutcome: the embedder-provided
  seam that presents the authorization URL to the user and returns the
  redirect callback params.

Unit-tested the store (round-trip, dir creation, idempotent clear,
permission bits).
connect_http is now reactive:
- with stored credentials, connect authorized non-interactively (rmcp
  refreshes the token transparently);
- otherwise try unauthenticated and, on the server's 401 challenge,
  surface a typed AuthorizationRequired (server name + WWW-Authenticate)
  so embedders can offer an Authenticate action rather than a bare retry.

authenticate_http_server drives the interactive login through an
OAuthAuthorizer (discovery -> client selection -> browser consent ->
code exchange) and persists tokens into the given CredentialStore, so
later connects reuse them silently.

Adds an integration test (401 -> AuthorizationRequired) and a guard that
authenticating a stdio server fails without network access.
- ConfigToolRegistry now passes a persistent per-server credential store
  (<config_dir>/mcp-oauth/<server>.json) when connecting HTTP servers, so
  a token obtained interactively is reused silently on later agent runs.
- tools::mcp gains authenticate_mcp_server / forget_mcp_oauth_tokens /
  has_mcp_oauth_tokens and the OAuth store path helpers.
- New CLI: 'code-assistant mcp-login <server>' runs the browser OAuth
  flow via a loopback callback authorizer (RFC 8252) and 'mcp-logout
  <server>' forgets the tokens. Mirrors Cline's Authenticate button.
- discover_tools_with_credentials reuses stored tokens, so a configuration
  UI can list the tools of an authenticated HTTP server; core exposes
  discover_server_tools which attaches the per-server credential store.
- Move the loopback OAuth authorizer into code_assistant_core
  (tools::mcp_auth::LoopbackAuthorizer + login_mcp_server) so both the CLI
  and the settings UI drive the same flow; the mcp-login command is now a
  thin wrapper.
- Re-export AuthorizationRequired through core for the UI to detect.
The settings section now discovers tools with the stored token
(discover_server_tools), and when an HTTP server needs OAuth it shows a
NeedsAuth state with an 'Authenticate' button instead of a generic
failure. Clicking it runs the loopback OAuth login on a background
runtime (browser + redirect), then re-discovers so the server's tools
appear once authenticated. Adds an Authenticating progress state.
The per-run tool registry is cached by a fingerprint of the config files
(tools.json, mcp-servers.json, .mcp.json). Completing an interactive
login writes only a token file under mcp-oauth/, so the fingerprint was
unchanged and the cached pre-auth (tool-less) registry kept being reused
until restart — the agent never saw a freshly authenticated server.

Include a fingerprint of the persisted OAuth token files (names, sizes,
mtimes) so a login invalidates the cache and the next run rebuilds,
reconnecting the now-authorized server so its tools appear. Tested.
Replace the connect/connect_with_credentials and
discover_tools/discover_tools_with_credentials pairs with single
functions that take the credentials argument. The parallel 'call with
None' wrappers hid the decision: they left the non-pooled
register_mcp_tools silently on the credential-less path and turned the
old discover_tools into dead code once discover_server_tools existed.

Now every call site states its intent — the pooled ConnectionProvider
and tool discovery thread the per-server store; register_mcp_tools and
tests pass None explicitly (stdio / static-header / no-auth), documented
as such. No behaviour change; the compiler verified every caller.
@stippi
stippi merged commit b3e22ff into main Sep 15, 2026
5 checks passed
@stippi
stippi deleted the feature/mcp-oauth-flow branch September 15, 2026 14:52
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