[Bug]: MCP driver discards refreshed OAuth access_token — live client keeps connect-time Authorization
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
- Connect a remote MCP driver with
oauth2_auth_codeoverstreamable_http. - Let
OAuth2AuthCodeProvider.resolve()renewaccess_token(enter the 5-minute refresh margin, or force-expireexpires_atin the store). - Invoke any tool on that driver without tearing the driver down.
- Observe the outbound HTTP
Authorizationheader still carries the pre-refresh token (or the call 401s whilecredentials.yamlalready has the new access_token).
Actual vs Expected
- Actual:
_executediscards the resolved credential; the liveHttpStatelessClient/HttpStatefulClientkeeps 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-authAdditional 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.
Source: agentscope-ai/QwenPaw