chore(deps): bump rmcp from 2.2.0 to 3.2.0 and port the OAuth transport - #5924
Conversation
Supersedes the red Dependabot PR #5877. rmcp 3.2 reshaped `transport::auth`; this ports our client and the in-process OAuth mock to the new contract without changing any product guarantee. What changed in rmcp, and what we adapted to: 1. `AuthorizationManager::discover_metadata()` is now `resolve_metadata()`, returning `AuthorizationMetadataResolution { metadata, source }`. The discovery order (RFC 9728 protected resource metadata, then RFC 8414 / OIDC, then legacy synthesized endpoints) and the legacy fallback are unchanged, so the two call sites just take `.metadata`. 2. `OAuthState::start_authorization(scopes, redirect_uri, client_name)` is now a single `AuthorizationRequest` builder. It also introduces a client-identity priority order (pre-registered client ID, then a Client ID Metadata Document, then Dynamic Client Registration); we supply neither of the first two, so we still land on DCR exactly as before. Empty scopes now mean "let the SDK select from the WWW-Authenticate challenge / PRM / AS metadata" rather than "request none" — strictly better, and our explicit scopes still win. 3. Discovery now validates the `issuer` in the fetched authorization server metadata against the discovery URL (RFC 8414 / OIDC) and fails with `AuthError::AuthorizationServerMismatch` instead of silently accepting it. Our in-process mock answered *every* `/.well-known/oauth-authorization-server*` path with one document whose `issuer` is the origin root, including rmcp's first candidate `/.well-known/oauth-authorization-server/mcp`, whose issuer must be `<origin>/mcp`. The mock now answers only the canonical path, which is what a real authorization server at the origin root does. That is a conformance fix to a test double, not a product behavior change: every assertion in the eight tests is untouched. 4. `AuthError` split the refresh outcomes: `TokenRefreshFailed` is now the retryable case and the new `TokenRefreshRejected` carries a definitive `invalid_grant`; separately, `refresh_token()` returns `AuthError::AuthorizationRequired` where 2.2 returned `TokenRefreshFailed("No refresh token available")`. The `invalid_grant` text still reaches us intact, so the dead-grant guarantees hold unchanged, but "OAuth authorization required" — a credential with no usable grant left — was classified as a plain transport failure on every surface. `error_text_looks_auth_required` now matches rmcp's full phrase, so that case flips the server to `auth required` and offers the login tool like every other unrecoverable credential. Not adapted because nothing depended on it: `StoredCredentials` gained an `issuer` field and `initialize_from_store` now discards tokens when the authorization server changes; `CredentialStore` gained an optional `acquire_refresh_guard`; the RFC 8707 `resource` parameter on refreshes now prefers the discovered PRM resource indicator and falls back to the base URL, which is what 2.2 always sent. We keep our own credential store, so rmcp's in-memory default is untouched by any of these. Verified locally: cargo fmt clean cargo check -p codewhale-tui clean RUST_MIN_STACK=16777216 cargo test -p codewhale-tui --lib -- mcp:: 230 passed; 0 failed; 1 ignored (was 222 passed; 8 failed) RUST_MIN_STACK=16777216 cargo test -p codewhale-tui --lib 11717 passed; 0 failed; 13 ignored Two earlier full-suite runs each tripped one different pre-existing parallel-execution flake in a module this change does not touch (`model_inventory::...ollama_default_prefers_live_local_tags...`, then `remote_control::...distinct_recovery_turn_ids`); both pass alone and the run above is clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188XYyJaw9Mh9uSrqQBoqhm Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_952405dd-591c-4795-89a2-f5432fa5ceae) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Claude finished @Hmbown's task in 2m 50s —— View job Claude's review of #5924
Note on verification: this sandbox has no network egress and SummarySmall, well-scoped port (4 files, +45/-12). I read the whole diff against the PR's own description of the rmcp 2.2→3.2 Correctness
Provider/model/route safety — not applicable; this diff is entirely inside the MCP OAuth transport, no model/provider selection logic is touched. Reuse/simplification — no duplicated logic introduced; the diff is a direct, minimal adaptation to the new upstream API shape. Tests — the new mock fix ( Security — no secret handling, logging, or exec-policy changes in this diff. One residual risk worth naming (not a bug in this diff): rmcp 3.2's new strict Nothing else stood out. This looks like a faithful, minimal port that matches its own changelog. |
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core OAuth discovery/login behavior via a dependency major update and needs CI/platform/provider coverage to confirm real-world compatibility beyond the in-process mock.
Pull request overview
This PR upgrades the TUI crate’s rmcp dependency from 2.2.0 → 3.2.0 and updates Codewhale’s MCP OAuth integration to the new rmcp::transport::auth API surface, including metadata resolution and authorization startup, while fixing the in-process OAuth mock to comply with rmcp 3.2’s stricter issuer validation during discovery.
Changes:
- Bump
rmcpto 3.2.0 and update the lockfile accordingly. - Port MCP OAuth discovery and login startup to
resolve_metadata()and theAuthorizationRequestbuilder API. - Update OAuth “auth required” classification to recognize rmcp’s
"OAuth authorization required"wording and adjust the OAuth mock discovery routing to serve only the canonical RFC 8414 well-known path.
File summaries
| File | Description |
|---|---|
| crates/tui/src/mcp/oauth.rs | Ports OAuth discovery/login to rmcp 3.2 auth transport API and expands auth-required error-text classification. |
| crates/tui/src/mcp/tests.rs | Fixes the OAuth mock’s well-known routing to match rmcp 3.2 issuer validation expectations. |
| crates/tui/Cargo.toml | Updates rmcp dependency version to 3.2.0 for the TUI crate. |
| Cargo.lock | Captures the dependency graph updates from the rmcp bump. |
Review details
- Files reviewed: 3/4 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Codewhale review
PR bumps rmcp from 2.2.0 to 3.2.0 and ports the OAuth transport call sites in crates/tui/src/mcp/oauth.rs. The visible changes are mostly mechanical: resolve_metadata() replaces discover_metadata(), AuthorizationRequest now builds the authorization request, and the auth-required classifier recognizes rmcp 3.2's new wording. The OAuth mock was tightened to serve metadata only at the canonical well-known path.
Findings
- [WARNING] Empty scopes now trigger SDK scope auto-selection instead of requesting no scopes (
crates/tui/src/mcp/oauth.rs:1359)
start_authorization now always calls AuthorizationRequest::with_scopes(scopes.iter().copied()). In rmcp 3.2, an empty scope collection means the SDK may select scopes from the WWW-Authenticate challenge, protected resource metadata, or authorization server metadata, whereas rmcp 2.2 treated empty scopes as requesting none. This is a product-visible behavior change for servers that advertise no supported scopes. Confirm that the new behavior is intended. - [WARNING] Issuer-validation discovery paths are only partially covered by the updated mock (
crates/tui/src/mcp/tests.rs:5998)
The mock now returns 404 for every well-known path except the canonical /.well-known/oauth-authorization-server. This proves the root-issue case, but no test covers a path-insertion or path-appended discovery URL whose metadata has a matching issuer, nor the new hard failure when a real provider serves root-issuer metadata at a path-insertion URL. Real OAuth providers may differ from this mock; CI-level or integration coverage is still desirable.
Assessment
The port looks correct for the visible call sites, and the local test result is strong. The two risks above are behavior/integration risks rather than obvious code defects; they should be explicitly accepted or covered before relying on this upgrade in production.
Advisory review by Codewhale (codewhale review --pr 5924 --post, head eb3511d5a1d47ded035ae7b1fea6931c8997fb65). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| .start_authorization(scopes, redirect_uri, Some("Codewhale")) | ||
| .start_authorization( | ||
| AuthorizationRequest::new(redirect_uri) | ||
| .with_scopes(scopes.iter().copied()) |
There was a problem hiding this comment.
[WARNING] Empty scopes now trigger SDK scope auto-selection instead of requesting no scopes
start_authorization now always calls AuthorizationRequest::with_scopes(scopes.iter().copied()). In rmcp 3.2, an empty scope collection means the SDK may select scopes from the WWW-Authenticate challenge, protected resource metadata, or authorization server metadata, whereas rmcp 2.2 treated empty scopes as requesting none. This is a product-visible behavior change for servers that advertise no supported scopes. Confirm that the new behavior is intended.
| // 3.2 validates the discovered `issuer` against the | ||
| // discovery URL and rejects metadata served at the wrong | ||
| // one. | ||
| if method == "GET" && path_only == "/.well-known/oauth-authorization-server" { |
There was a problem hiding this comment.
[WARNING] Issuer-validation discovery paths are only partially covered by the updated mock
The mock now returns 404 for every well-known path except the canonical /.well-known/oauth-authorization-server. This proves the root-issue case, but no test covers a path-insertion or path-appended discovery URL whose metadata has a matching issuer, nor the new hard failure when a real provider serves root-issuer metadata at a path-insertion URL. Real OAuth providers may differ from this mock; CI-level or integration coverage is still desirable.
No-Issue: dependency bump (rmcp 2.2.0 → 3.2.0), supersedes Dependabot PR #5877 which was red on every leg.
Supersedes #5877 (the Dependabot bump, red on every leg).
Bumps
rmcp2.2.0 -> 3.2.0 and portscrates/tui/src/mcp/oauth.rsto thereshaped
rmcp::transport::authcontract.What changed in rmcp 3.2
Compared against
rmcp-2.2.0/src/transport/auth.rsin the local registry:discover_metadata()->resolve_metadata(), now returningAuthorizationMetadataResolution { metadata, source }wheresourcesayswhether the endpoints came from RFC 9728 protected resource metadata, RFC
8414 / OIDC metadata, or the legacy synthesized
/authorize/token/registerfallback. The discovery order and the legacy fallback arebyte-for-byte the same as 2.2, so the semantics our two call sites depend on
did not move.
OAuthState::start_authorizationtakes anAuthorizationRequestbuilder instead of
(scopes, redirect_uri, client_name). The builder alsointroduces a client-identity priority order: a pre-registered client ID,
then a Client ID Metadata Document (SEP-991), then Dynamic Client
Registration. We supply neither of the first two, so we still land on DCR
exactly as before. Empty scopes now mean "let the SDK select from the
WWW-Authenticatechallenge / PRM / AS metadata" rather than "requestnone".
Discovery now validates the
issuer.fetch_authorization_metadataderives the expected issuer from the discovery URL (RFC 8414 path-insertion,
OIDC path-insertion and path-appended forms) and hard-fails with
AuthError::AuthorizationServerMismatch/AuthorizationServerMissingIssuerwhen the document disagrees. 2.2 performed this check only at token
exchange. There is a new
set_allow_missing_issuerescape hatch we do notuse.
AuthErrorsplit the refresh outcomes.TokenRefreshFailedis now theretryable case; the new
TokenRefreshRejectedcarries a definitiveinvalid_grantfrom the authorization server. Separately,refresh_token()returns
AuthError::AuthorizationRequired("OAuth authorization required")where 2.2 returned
TokenRefreshFailed("No refresh token available").Not load-bearing for us, listed for the record:
StoredCredentialsgainedan
issuerfield andinitialize_from_storediscards tokens when theauthorization server changes;
CredentialStoregained an optionalacquire_refresh_guardhook; the RFC 8707resourceparameter on refreshesnow prefers a discovered PRM resource indicator and falls back to the base
URL (what 2.2 always sent);
OAuthHttpClientErrorbecame a type-erasedboxed error; the resource-metadata POST probe was dropped in favour of a GET
probe. We keep our own credential store, so rmcp's in-memory default is
untouched by any of this.
What we adapted
oauth.rs: the three renamed call sites (resolve_metadata().metadataindiscover_streamable_http_oauth_with_headersandstart_authorization, andthe
AuthorizationRequestbuilder).oauth.rs:error_text_looks_auth_requirednow also matches rmcp's"OAuth authorization required". Item 4 above moved a real product case —a stored credential with no usable refresh grant left — from an unmatched
string onto a phrase no surface recognised, which would have shown that
server as a plain transport failure instead of
auth requiredwith theself-serve login tool. The full phrase is matched so the predicate stays
anchored to rmcp's own wording rather than the bare words "authorization
required". Covered by a new case in
auth_required_classifier_treats_rejected_grants_as_auth_required, includinga negative case.
tests.rs, the in-processOAuthMcpMock: it answered every/.well-known/oauth-authorization-server*path with one document whoseissueris the origin root — including rmcp's first discovery candidate/.well-known/oauth-authorization-server/mcp, whose issuer must be<origin>/mcpper RFC 8414. Under item 3 that now hard-fails discovery,which is what turned all eight OAuth tests red. The mock now answers only the
canonical
/.well-known/oauth-authorization-server, which is how a realauthorization server sitting at the origin root behaves.
No test assertion was changed. The four product guarantees those eight
tests encode are intact and still asserted: a dead grant flips the server to
auth requiredand offers the login tool; a rotated on-disk token is adopted;invalidation never deletes a credential rotated after the re-read; the browser
wait releases the pool lock. The only test edit is the mock's HTTP routing —
a conformance fix to a test double, not a behavior change.
Verified locally
Two earlier full-suite runs each tripped one different pre-existing
parallel-execution flake in a module this change does not touch
(
model_inventory::...ollama_default_prefers_live_local_tags..., thenremote_control::...distinct_recovery_turn_ids); both pass in isolation andthe run above is clean.
What CI has to prove
Local runs cover only
-p codewhale-tui --libon macOS/arm64 againstin-process mock servers. CI still owns the workspace-wide build and test
matrix, the other platform legs,
npm run check:web, and anything thatexercises a real OAuth provider — no real authorization server was contacted
in any of the above, so item 3 (issuer validation) and item 2 (SDK scope
auto-selection) are verified against our mock only.
Note
Medium Risk
Touches MCP OAuth discovery, login, and token-refresh error classification; behavior is mostly API porting plus tighter issuer checks, with risk if real providers differ from the updated mock assumptions.
Overview
Upgrades
rmcpfrom 2.2.0 to 3.2.0 and updates the TUI MCP OAuth layer to match the reshaped transport/auth API.OAuth integration now calls
resolve_metadata()(instead ofdiscover_metadata()) for scope discovery and pre-registered client flows, and starts login via theAuthorizationRequestbuilder rather than separate scope/redirect/client-name arguments.error_text_looks_auth_requiredalso treats rmcp 3.2’sOAuth authorization requiredmessage as a login-needed signal so dead or unrefreshable stored credentials surfaceauth requiredand the login tool instead of a generic transport failure.Tests: the in-process OAuth mock only serves the canonical
/.well-known/oauth-authorization-serverpath (404 on path-insertion variants), aligning with rmcp 3.2’s issuer validation during discovery. New classifier tests cover the rmcp wording without matching unrelated “authorization required” errors.Reviewed by Cursor Bugbot for commit eb3511d. Bugbot is set up for automated code reviews on this repo. Configure here.