Repository navigation
feat(mcp): dynamic client registration and bearer injection for OAuth MCP servers - #1029
Open
mariomosca wants to merge 1 commit into
Open
mariomosca wants to merge 1 commit into
mariomosca wants to merge 1 commit into
Conversation
… MCP servers MCP servers implementing the MCP authorization spec could not be used from AionUi: the OAuth client id was hardcoded, so a spec-compliant authorization server rejected the flow with `invalid_client` and could never allow-list the loopback redirect URI; and the token obtained by a successful login was read by nothing, so the agent CLI still called the server anonymously. Discovery now follows protected resource metadata (RFC 9728) to the authorization server metadata (RFC 8414 / OIDC), keeping the previous probe as the last fallback. When the server advertises a registration endpoint the client registers itself for the loopback redirect it is about to listen on (RFC 7591); the issued client id and secret are persisted next to the token (migration 045) because the refresh grant must present the same client. When protected resource metadata declares a resource, it is sent as the RFC 8707 resource indicator on authorize, token and refresh requests. At session-build time every authenticated http/sse MCP server reaches the backend with an `Authorization: Bearer` header, on the direct-CLI, ACP and aionrs lanes. A header the user configured by hand is never overwritten, a server with no token is left untouched, and tokens are resolved per build rather than persisted into the conversation snapshot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Follow-up PR against this branch: mariomosca#1. It fixes three gaps found while testing this PR end to end against https://mcp.cloudflare.com/mcp:
Verified live against Cloudflare and in AionUi's Tools page. Happy to adjust anything. |
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.
Problem
MCP servers that implement the MCP authorization spec cannot be used from AionUi today. Two independent gaps:
McpOAuthServicealways sends the hardcodedclient_id = "aionui", and discovery only probes.well-known/oauth-authorization-serveron the MCP server URL itself. A spec-compliant authorization server answers/authorizewithinvalid_client, and the loopback redirect (http://127.0.0.1:<random port>/callback) can never be allow-listed up front — registration at login time is exactly what RFC 7591 exists for, and what the MCP spec prescribes.Concretely: an internal MCP server behind an OAuth 2.1 authorization server (Entra ID upstream, protected resource metadata, mandatory registration, per-client redirect allow-list) is unusable from AionUi, with or without a completed login.
What this changes
Discovery (
oauth_discovery.rs) — the chain the MCP spec describes: protected resource metadata (RFC 9728) → authorization server metadata (RFC 8414) / OIDC discovery, trying the path-aware, path-suffixed and origin forms in that order. The previous behaviour (probing the MCP server URL as its own authorization server) is kept as the last fallback, so servers that publish neither keep working unchanged.Dynamic client registration (
oauth_registration.rs) — when the authorization server advertises aregistration_endpoint, the client registers itself for the loopback redirect URI it is about to listen on (token_endpoint_auth_method: "none",grant_types: [authorization_code, refresh_token], native application type). Without a registration endpoint, the static client id is used exactly as before.Client persistence — the issued
client_id(and secret, if any) is stored next to the token (migration045), because the refresh grant has to present the same client that obtained the token. Nothing is persisted for the static fallback.Resource indicator — when protected resource metadata declares a
resource, it is sent as the RFC 8707resourceparameter on the authorize, token and refresh requests, so the authorization server can audience-restrict the token to that MCP server.Bearer injection (
oauth_injection.rs) — at session-build time, every http/sse MCP server the user authenticated against is handed to the backend withAuthorization: Bearer <token>(refreshed if expired). Covered lanes: the direct-CLI session path (claude/codex/agy, viaSessionBuildInputs.mcp_token_provider) and the ACP path (session/newmcpServers). A header the user configured by hand is never overwritten, and a server with no stored token is left untouched — injection is best effort and never fails a session build. Tokens are resolved per session build and never written into the persisted conversation snapshot.The
McpOAuthServiceis now built once inAppServices, so the instance the MCP routes log in with is the one the agent factory injects tokens from.Tests
oauth_dcr_integration.rs), against a fake authorization server in-process: discovery follows PRM → AS; registration sends the loopback redirect and returns the client id; a registration failure surfaces the server's own error and aborts the login before any browser opens; the refresh grant presents the registered client id and the resource indicator, and persists both; a server without a registration endpoint falls back to the static client id.session_agent: the inline--mcp-configthe claude CLI is spawned with actually carriesAuthorization: Bearer …for an authenticated http MCP server, carries nothing when no token provider is wired, and keeps a user-configuredAuthorizationheader untouched.oauth_live_manual.rs,#[ignore]): full flow against a real server, driven byAIONUI_LIVE_MCP_URL.Verified end-to-end against a real MCP server (OAuth 2.1 authorization server proxying Entra ID): discovery → dynamic registration → browser login → token stored with the registered client id → authenticated
initialize+tools/list+tools/callover the streamable HTTP transport.cargo fmt --checkandcargo clippy --workspace --all-targets -- -D warningsare clean;cargo test -p aionui-mcp -p aionui-db -p aionui-ai-agentpasses locally. The full workspace suite is left to CI.🤖 Generated with Claude Code