MCP OAuth: VS Code does not evict a stale dynamically-registered client / expired token on HTTP 401 (remote server behind APISIX + Keycloak)

Open
#321,834 0 comments 1 reaction 1 assignee View on GitHub

@TylerLeonhardt is already working on this.

Since Jun 17, 2026.

Assessment

This issue has not been assessed yet.

Description

authentication bug mcp

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
    
  • /register returns a fixed, pre-provisioned public client (client_id: vscode-mcp-public-client, token_endpoint_auth_method: none, authorization_code + PKCE, redirect_uris http://127.0.0.1:* and https://vscode.dev/redirect). The client is pre-created in Keycloak.

Steps to Reproduce

  1. Configure a remote "type": "http" MCP server that requires OAuth and advertises RFC 9728 / 8414 / 7591 discovery (gateway + IdP as above).
  2. 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.)
  3. 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.
  4. Trigger another MCP request from VS Code (use a tool, or restart the server entry).
  5. 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 sequencereconstructed 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:

  1. WWW-Authenticate is returned on every response, including HTTP 200 (statically injected alongside x-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?
  2. /register returns a fixed client (same client_id every 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from microsoft/vscode

All issues in microsoft/vscode

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.