MCP OAuth: VS Code does not evict a stale dynamically-registered client / expired token on HTTP 401 (remote server behind APISIX + Keycloak)
@TylerLeonhardt is already working on this.
Since Jun 17, 2026.
Assessment
This issue has not been assessed yet.
Description
Does this issue occur when all extensions are disabled?: Yes
- VS Code Version: Visual Studio Code - Insiders 1.125.0-insider
- OS Version: Windows 11 Enterprise, 10.0.26100 (Build 26100)
MCP OAuth (RFC 9728 / 8414 / 7591 discovery + dynamic client registration + token caching) is handled by VS Code core, not by an extension — so this reproduces with all extensions disabled.
Summary
When VS Code connects to a remote HTTP MCP server protected by OAuth, it caches the dynamically-registered client_id and the access token. Once that cached credential becomes invalid (token expires, or the client/registration changes server-side), VS Code keeps sending the stale token, gets HTTP 401, and does not automatically refresh the token, evict the cached client, or re-run discovery/registration/authorization. The MCP server stays stuck in a failed (401) state until the user manually clears the dynamic auth state (Sign Out / "forget cached tools" / Authentication: Remove Dynamic Authentication Providers).
A plain HTTP client (PowerShell) that mints a fresh bearer token on every run, hitting the same gateway endpoint, never reproduces the problem — which points at VS Code's credential cache/refresh handling rather than the server.
Likely the same root cause as #263990 / #250960 / #270383 — filing with an APISIX + Keycloak (OIDC) reproduction and the side-by-side "fresh-token HTTP client works" evidence.
Environment / setup (anonymized)
- Remote MCP server (
"type": "http", streamable HTTP/SSE) behind an APISIX gateway; Keycloak as the OAuth Authorization Server (OIDC). MCP server = Resource Server. - Discovery / flow endpoints exposed by the gateway:
MCP endpoint: https://gateway.example.com/mcpserver/mcp Protected resource: https://gateway.example.com/.well-known/oauth-protected-resource AS metadata: https://gateway.example.com/.well-known/oauth-authorization-server Register (DCR): https://gateway.example.com/register Authorize: https://gateway.example.com/authorize (302 -> Keycloak) Token: https://gateway.example.com/token (-> Keycloak) Authorization server: https://idp.example.com/realms/acme /registerreturns a fixed, pre-provisioned public client (client_id: vscode-mcp-public-client,token_endpoint_auth_method: none, authorization_code + PKCE, redirect_urishttp://127.0.0.1:*andhttps://vscode.dev/redirect). The client is pre-created in Keycloak.
Steps to Reproduce
- Configure a remote
"type": "http"MCP server that requires OAuth and advertises RFC 9728 / 8414 / 7591 discovery (gateway + IdP as above). - In VS Code Insiders, connect to it and complete the OAuth flow in the browser. Confirm tools are discovered and tool calls succeed. (In our case:
Discovered 33 tools.) - Invalidate the cached access token without changing the configuration — any of: let the access token expire, re-login/rotate the client on the IdP, or restart the server so previously-issued tokens are rejected.
- Trigger another MCP request from VS Code (use a tool, or restart the server entry).
- Observe that VS Code re-sends the stale token, receives 401, and the server stays failed.
Expected behavior
On a 401 from a previously-authorized MCP server, VS Code should self-heal: (1) try the stored refresh_token; (2) if that fails, evict the cached token and the cached dynamic client registration; (3) re-run discovery → DCR → authorization (prompting only if interaction is needed); (4) retry the request — without the user manually clearing caches.
Actual behavior
VS Code keeps sending the stale token, the gateway keeps returning 401, and the server is shown as failed/unavailable. Recovery requires manual action: Sign Out / "forget cached tools", or Authentication: Remove Dynamic Authentication Providers, then restart the server. After that the OAuth flow re-runs and everything works — until the cached credential next goes invalid.
Logs (anonymized, trace level)
Request made while the cached token was still valid — status:200, valid JSON-RPC result (included to show the server is healthy, and to highlight the header note below):
[trace] Fetching https://gateway.example.com/mcpserver/mcp with options:
{"method":"POST",
"headers":{"Content-Type":"application/json",
"Accept":"text/event-stream, application/json",
"Authorization":"***",
"user-agent":"Visual Studio Code - Insiders/1.125.0-insider"},
"body":"{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"logging/setLevel\",\"params\":{\"level\":\"debug\"}}"}
[trace] Fetched https://gateway.example.com/mcpserver/mcp:
{"status":200,
"headers":{ ...,
"content-type":"text/event-stream",
"www-authenticate":"Bearer resource_metadata=\"https://gateway.example.com/.well-known/oauth-protected-resource\"",
"x-accel-buffering":"no" }}
[debug] [server -> editor] {"result":{},"id":4,"jsonrpc":"2.0"}
Representative failing sequence — reconstructed from our environment and masked. The line format is taken verbatim from VS Code's own trace output (only status/method/id differ); the behavior shown matches what #263990 documents (stale token re-sent, 401 logged, no refresh or re-registration). If/when the verbatim capture is available it should replace this block.
[trace] Fetching https://gateway.example.com/mcpserver/mcp with options:
{"method":"POST",
"headers":{"Content-Type":"application/json",
"Accept":"text/event-stream, application/json",
"Authorization":"***",
"user-agent":"Visual Studio Code - Insiders/1.125.0-insider"},
"body":"{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/list\",\"params\":{}}"}
[trace] Fetched https://gateway.example.com/mcpserver/mcp:
{"status":401,
"headers":{ ...,
"www-authenticate":"Bearer resource_metadata=\"https://gateway.example.com/.well-known/oauth-protected-resource\"" }}
# VS Code logs the 401 but does NOT refresh the token or re-run discovery/registration.
# The next request re-sends the SAME cached (stale) token:
[trace] Fetching https://gateway.example.com/mcpserver/mcp with options:
{"method":"POST","headers":{ ...,"Authorization":"***", ... },
"body":"{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/list\",\"params\":{}}"}
[trace] Fetched https://gateway.example.com/mcpserver/mcp: {"status":401, ...}
# The server enters a failed state. No follow-up calls to
# /.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server,
# /register, /authorize or /token are made — i.e. no automatic recovery.
# Only a manual "Authentication: Remove Dynamic Authentication Providers"
# (or Sign Out + clear cached tools), followed by restarting the server, recovers it.
Workaround
Authentication: Remove Dynamic Authentication Providers (or Sign Out + clear cached tools), then restart the MCP server — forces a fresh DCR + OAuth flow and restores the connection.
Additional notes (full disclosure of gateway-side quirks)
Two aspects of our gateway are non-standard; we're fixing them, but want to confirm whether either interacts with VS Code's auth state machine:
WWW-Authenticateis returned on every response, including HTTP 200 (statically injected alongsidex-accel-buffering: no, as in the trace), not only on 401. RFC 7235 says it belongs with a 401. Does VS Code treat this header on a non-401 response as an auth signal?/registerreturns a fixed client (sameclient_idevery time) rather than real per-client registration. Could an idempotent DCR response affect VS Code's ability to evict/re-register when the cached token later becomes invalid?
Even setting these aside, the core ask stands: VS Code should not get permanently stuck on a stale cached token/client and require manual cache clearing.
Possibly related / duplicate
- #263990 — VS Code does not automatically refresh expired OAuth tokens.
- #250960 — VS Code does not evict the client when it can't be authorized; no way to delete a cached client.
- #270383 — MCP OAuth fails to retry after server restart when scopes are unchanged.
- #269379 — No clean way to remove stale MCP OAuth credentials.
- #256198 — Missing documented method for clearing the MCP cache.
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.9k
- PR merge metrics
- PR metrics pending
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/vscode
-
testplan-item
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
new release
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
testplan-item
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
testplan-item
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
All issues in microsoft/vscode
Similar issues
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 76/100
-
code-quality refactoring
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
github/gh-aw-firewall#8816 ·
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
vercel/react-tweet#225 ·