#7821·QwenPaw

[Bug]: MCP driver discards refreshed OAuth access_token — live client keeps connect-time Authorization

Author: kabishou11Created Sep 17, 2026Updated Sep 17, 2026

QwenPaw Version

Current main (post Driver / MCP streamable-HTTP stack).

Description

MCPDriverHandler._guarded_execute resolves credentials on every tool invoke (self._credential_provider.resolve()), which is where OAuth2AuthCodeProvider renews access_token inside the refresh margin.

But _execute then did del credential and called self._client.call_tool(...) on the client that was connected in _setup() with the setup-time Authorization header.

So even when the credential store already holds a freshly renewed Bearer token, subsequent MCP calls still send the expired connect-time header until a full reconnect. On remote OAuth MCP (streamable_http / SSE) this surfaces as recurring HTTP 401 and sticky "authorize via the UI" degradation — distinct from refresh-token rotation persistence (#7053 / #7066), which only fixes what is written back to the store.

Related: #7053 (rotated refresh_token not persisted), #4643 (client_secret on initial exchange). This bug is the missing apply-to-live-client step after a successful resolve/refresh.

Security considerations: n/a — applies already-authorized tokens to the connection they were meant for.

Component(s) Affected

  • Core / Backend (app, agents, config, providers, utils, local_models)
  • Console (frontend web UI)
  • Channels
  • Skills
  • CLI
  • Documentation (website)
  • Tests
  • CI/CD
  • Scripts / Deploy

Environment

  • QwenPaw version: current main
  • OS: Linux
  • Install method: from source
  • Python version: 3.11

Steps to Reproduce

  1. Connect a remote MCP driver with oauth2_auth_code over streamable_http.
  2. Let OAuth2AuthCodeProvider.resolve() renew access_token (enter the 5-minute refresh margin, or force-expire expires_at in the store).
  3. Invoke any tool on that driver without tearing the driver down.
  4. Observe the outbound HTTP Authorization header still carries the pre-refresh token (or the call 401s while credentials.yaml already has the new access_token).

Actual vs Expected

  • Actual: _execute discards the resolved credential; the live HttpStatelessClient / HttpStatefulClient keeps connect-time Authorization; tool calls 401 after token renewal until reconnect.
  • Expected: After resolve/refresh, the live client's Authorization is updated (stateless: mutate httpx default headers; stateful: update headers and reload only when the value changed) before call_tool.

Logs / Screenshots

# Pattern: store has fresh token, live client still sends stale Bearer
credentials.yaml  access_token: <fresh>
httpx request     Authorization: Bearer <stale-from-connect>
→ HTTP 401 → MCP client marks oauth_required / user must re-auth

Additional Notes

Minimal fix: apply Bearer {access_token} from the resolved credential onto the connected client inside _execute before call_tool. Do not reload stateful transports when the Authorization value is unchanged.