Skip to content

chore(deps): bump rmcp from 2.2.0 to 3.2.0 and port the OAuth transport - #5924

Merged
Hmbown merged 1 commit into
mainfrom
chore/rmcp-3.2-5877
Sep 6, 2026
Merged

chore(deps): bump rmcp from 2.2.0 to 3.2.0 and port the OAuth transport#5924
Hmbown merged 1 commit into
mainfrom
chore/rmcp-3.2-5877

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 6, 2026

Copy link
Copy Markdown
Owner

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 rmcp 2.2.0 -> 3.2.0 and ports crates/tui/src/mcp/oauth.rs to the
reshaped rmcp::transport::auth contract.

What changed in rmcp 3.2

Compared against rmcp-2.2.0/src/transport/auth.rs in the local registry:

  1. discover_metadata() -> resolve_metadata(), now returning
    AuthorizationMetadataResolution { metadata, source } where source says
    whether the endpoints came from RFC 9728 protected resource metadata, RFC
    8414 / OIDC metadata, or the legacy synthesized /authorize /token
    /register fallback. The discovery order and the legacy fallback are
    byte-for-byte the same as 2.2, so the semantics our two call sites depend on
    did not move.

  2. OAuthState::start_authorization takes an AuthorizationRequest
    builder
    instead of (scopes, redirect_uri, client_name). The builder also
    introduces 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-Authenticate challenge / PRM / AS metadata" rather than "request
    none".

  3. Discovery now validates the issuer. fetch_authorization_metadata
    derives the expected issuer from the discovery URL (RFC 8414 path-insertion,
    OIDC path-insertion and path-appended forms) and hard-fails with
    AuthError::AuthorizationServerMismatch / AuthorizationServerMissingIssuer
    when the document disagrees. 2.2 performed this check only at token
    exchange. There is a new set_allow_missing_issuer escape hatch we do not
    use.

  4. AuthError split the refresh outcomes. TokenRefreshFailed is now the
    retryable case; the new TokenRefreshRejected carries a definitive
    invalid_grant from the authorization server. Separately, refresh_token()
    returns AuthError::AuthorizationRequired ("OAuth authorization required")
    where 2.2 returned TokenRefreshFailed("No refresh token available").

  5. Not load-bearing for us, listed for the record: StoredCredentials gained
    an issuer field and initialize_from_store discards tokens when the
    authorization server changes; CredentialStore gained an optional
    acquire_refresh_guard hook; the RFC 8707 resource parameter on refreshes
    now prefers a discovered PRM resource indicator and falls back to the base
    URL (what 2.2 always sent); OAuthHttpClientError became a type-erased
    boxed 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().metadata in
    discover_streamable_http_oauth_with_headers and start_authorization, and
    the AuthorizationRequest builder).
  • oauth.rs: error_text_looks_auth_required now 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 required with the
    self-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, including
    a negative case.
  • tests.rs, the in-process OAuthMcpMock: it answered every
    /.well-known/oauth-authorization-server* path with one document whose
    issuer is the origin root — including rmcp's first discovery candidate
    /.well-known/oauth-authorization-server/mcp, whose issuer must be
    <origin>/mcp per 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 real
    authorization 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 required and 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

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      (before: 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 in isolation and
the run above is clean.

What CI has to prove

Local runs cover only -p codewhale-tui --lib on macOS/arm64 against
in-process mock servers. CI still owns the workspace-wide build and test
matrix, the other platform legs, npm run check:web, and anything that
exercises 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 rmcp from 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 of discover_metadata()) for scope discovery and pre-registered client flows, and starts login via the AuthorizationRequest builder rather than separate scope/redirect/client-name arguments. error_text_looks_auth_required also treats rmcp 3.2’s OAuth authorization required message as a login-needed signal so dead or unrefreshable stored credentials surface auth required and the login tool instead of a generic transport failure.

Tests: the in-process OAuth mock only serves the canonical /.well-known/oauth-authorization-server path (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.

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>
Copilot AI lite review requested due to automatic review settings September 6, 2026 06:29
@cursor

cursor Bot commented Sep 6, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-06T06:34:24.193139Z eb3511d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 50s —— View job


Claude's review of #5924

  • Gather context (diff, changed files, rmcp changelog claims)
  • Read crates/tui/src/mcp/oauth.rs changes
  • Read crates/tui/src/mcp/tests.rs changes
  • Check Cargo.toml / Cargo.lock bump
  • Verify build/test claims where feasible
  • Post review findings

Note on verification: this sandbox has no network egress and cargo/git fetch require approval I can't grant myself, so I could not independently re-run cargo check/cargo test or fetch the rmcp 3.2 source to check its API against the vendored copy. The review below is a manual read of the diff plus grepping the workspace for other consumers; it does not re-verify the PR's own reported build/test output.

Summary

Small, 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 transport::auth changes and didn't find a mismatch between what's claimed and what's in the code.

Correctness

  • Both discover_metadata()resolve_metadata() call sites (oauth.rs:624, oauth.rs:1368) correctly pull .metadata off the new AuthorizationMetadataResolution, and the NoAuthorizationSupport arm is preserved.
  • The DCR path (oauth.rs:1354-1362) builds AuthorizationRequest::new(redirect_uri).with_scopes(...).with_client_name("Codewhale"). The pre-registered-client path (oauth.rs:1364-1372) still goes through AuthorizationManager/OAuthClientConfig directly and is unaffected by the builder change — matches the PR's claim that DCR is the only path touched.
  • error_text_looks_auth_required (oauth.rs:70-98) lowercases text before matching (line 71), so the new "oauth authorization required" literal correctly matches rmcp's AuthorizationRequired Display text regardless of case. The new negative test ("authorization required for the requested file") confirms the match stays anchored to the full rmcp phrase rather than a bare "authorization required" substring, avoiding a plausible false-positive I'd otherwise have flagged.
  • Grepped the whole crates/ tree: rmcp is only imported in oauth.rs, and no other call site references the old discover_metadata/3-arg start_authorization API — nothing was missed in the port.
  • The workspace doesn't touch rmcp's StoredCredentials/CredentialStore types (grepped, no hits), consistent with the "not adapted, nothing depended on it" claims for items the PR says it deliberately left alone.
  • Cargo.lock diff is minimal and mechanical: version/checksum bump, indexmap pulled in transitively with a serde feature. Nothing unexpected.

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 (tests.rs) is a correctness fix to the test double, not a weakening: it now only answers the canonical /.well-known/oauth-authorization-server path instead of every oauth-authorization-server* prefix, which is what triggered rmcp 3.2's stricter issuer validation to fail before. The unrelated else if ... starts_with("/.well-known/") 404 fallback already existed and correctly handles the other discovery candidates rmcp probes. Two new unit test cases (positive/negative) reasonably cover the new AuthorizationRequired string classification.

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 issuer validation (item 3 in the PR description) is a real behavior tightening upstream — a previously-working OAuth server whose metadata document doesn't set issuer to exactly the discovery URL per RFC 8414/9728 will now hard-fail discovery where 2.2 accepted it. The PR body already flags this transparently ("no real authorization server was contacted... item 3 is verified against our mock only"), so this is a heads-up for post-merge monitoring rather than something to fix here.

Nothing else stood out. This looks like a faithful, minimal port that matches its own changelog.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 rmcp to 3.2.0 and update the lockfile accordingly.
  • Port MCP OAuth discovery and login startup to resolve_metadata() and the AuthorizationRequest builder 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.

@codewhale-agent codewhale-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@Hmbown
Hmbown merged commit 8142920 into main Sep 6, 2026
37 of 40 checks passed
@Hmbown
Hmbown deleted the chore/rmcp-3.2-5877 branch September 6, 2026 09:36
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.

2 participants