- Fenced final OAuth refresh update and terminal-disable CAS statements by row id, serialized credential data, active lease owner, and unexpired lease time.
- Passed an AbortSignal through MCP OAuth token refresh and bounded owned refresh operations below the lease TTL while awaiting the aborted fetch to settle.
- Added regressions for stolen-lease update/disable attempts and timed-out MCP token fetch abort behavior.
Fixes#5081
- Renewed durable OAuth refresh leases while token refreshes are in flight so slow endpoints cannot let a peer steal the row and replay a rotating refresh token.
- Let CAS update and disable storage errors propagate instead of collapsing them into peer-win misses.
- Added regressions for lease renewal and CAS storage failure propagation.
Fixes#5081
- Added durable SQLite refresh ownership for stored OAuth rows, with canonical re-read before refresh and compare-and-set persistence.
- Routed MCP proactive and forced OAuth refresh through the shared owner so waiters reuse the winner's rotated credential.
- Added MCP regression tests for shared SQLite refresh ownership and stale invalid_grant losers.
Fixes#5081
Review on PR #3503 caught that the broad same-origin filter broke valid
protected-resource discovery shapes such as
`https://gateway.example.com/my-service/mcp`: same host as the authorization
server, but a distinct MCP service identified by path. Those advertised
resources must be preserved for audience selection.
- Replaced the unconditional same-origin filter with a provenance-aware policy: exact auth-server-origin resources are always stripped, path-scoped same-origin resources are preserved by default, and only OMP-synthesized fallback resources opt into same-origin path stripping.
- Added `stripSameOriginResource` to `MCPOAuthConfig` and `RefreshMCPOAuthTokenOptions`; quick-add/reauth set it only when the resource came from `config.url` / `runtimeBaseConfig.url` fallback rather than `oauth.resource` or an existing auth resource.
- Refresh uses the same flag when `MCPManager.prepareConfig` falls back to `config.url`, and no longer persists fallback resources into the credential as if they were provider-advertised material.
- Updated RFC 8707 tests to cover both sides: gateway path resource preserved, Plane-style fallback `/http/mcp` stripped, refresh path mirrors the same distinction.
Fixes#3502
Review on PR #3503 flagged that the prior fix anchored the initial-grant filter
on `authorizationUrl` but the refresh filter on `tokenUrl`. RFC 8414 lets the
authorize and token endpoints sit on different origins, so when they do, a
`config.url` fallback equal to the auth-server origin survives the refresh
filter — the credential works until expiry, then refresh resurrects the same
self-referential `resource` the authorize/token exchange intentionally
omitted.
- `MCPStoredOAuthCredential.authorizationUrl?: string` — new field, the issuer the grant was minted against.
- `MCPOAuthFlow.authorizationUrl` getter exposes the value so the persistence site can write it (symmetric with `flow.resource`).
- `refreshMCPOAuthToken` accepts `{ authorizationUrl }` via the trailing options object; filters self-referential indicators against the supplied URL, falling back to `tokenUrl`'s origin for legacy credentials. New `RefreshMCPOAuthTokenOptions` interface keeps the positional resource form working.
- `mcp-command-controller.ts` persists `flow.authorizationUrl` on credential write; `manager.ts` extracts it from the embedded credential material (legacy `MCPAuthConfig` rows lack it and continue through the `tokenUrl` fallback) and threads it to `refreshMCPOAuthToken`.
- Tests: 3 new cross-origin refresh cases — stripped when resource equals auth-server origin with cross-origin token endpoint; preserved when resource points at a third origin; legacy `tokenUrl`-anchored fallback still works without `authorizationUrl`. Plus a `flow.authorizationUrl` getter test. Updated `mcp-manager-oauth-refresh.test.ts` to account for the new opts arg.
Fixes#3502
When an HTTP MCP server returns invalid_grant (or invalid_token / revoked /
plain 401 from the token endpoint) during OAuth refresh, MCPManager
previously logged "MCP OAuth refresh failed, using existing token" and
re-attached the stale access token as Authorization: Bearer on every
subsequent request. The next tool-load 401'd with invalid_token, future
sessions repeated the loop, and the only recovery was to hand-clear the
credential row in agent.db. Reported with Logfire as the trigger; any
remote HTTP MCP that rotates / revokes refresh tokens is affected.
#resolveAuthConfig now reuses pi-ai's isDefinitiveOAuthFailure classifier
(same one auth-broker and AuthStorage use for first-party providers): on
a definitive failure it calls AuthStorage.remove(credentialId), drops the
Bearer entirely, and the next request surfaces a clean auth error so the
user can /mcp reauth <server> (or /mcp unauth) to recover. Transient
failures (network/fetch failed/ECONNREFUSED) still fall back to the
existing token to ride out blips.
Verified with new mcp-manager-oauth-refresh.test.ts (invalid_grant, 401,
transient fallback, happy-path rotation). The full mcp-* test set
(45 tests across 5 files) still passes.
Fixes#1908