HTTP MCP OAuth: refresh token not used; client drops to bootstrap tools when access token expires

Resolved 💬 4 comments Opened Apr 30, 2026 by Emredost Closed May 5, 2026

Summary

For HTTP MCP servers using OAuth, Claude Code stores both accessToken and refreshToken in the keychain credentials, but does not exercise the refresh-token flow when the access token expires. Instead the MCP server is shown as connected with only its bootstrap auth tools exposed, forcing the user to re-authorize manually every time the access token expires.

For Slack-backed MCP servers (e.g. the official slack-frends plugin) the access token TTL is ~12 hours, so the user is prompted to re-auth roughly twice a day.

Environment

  • Claude Code: 2.1.119
  • Platform: macOS 26.3.1
  • Node: v25.2.1
  • Affected MCP server: slack-frends (HTTP, https://mcp.slack.com/mcp), installed from the slack@claude-plugins-official plugin marketplace
  • Likely affects any HTTP MCP server using OAuth refresh tokens

Repro

  1. Install the official Slack plugin and complete OAuth (token + refresh token are stored).
  2. Wait ~12 hours for the access token to expire (or shorter if you adjust your test).
  3. Start a new Claude Code session.

Expected

The HTTP MCP client detects the expired access token, calls the OAuth refresh endpoint with the stored refresh token, swaps in the new access token, persists the new expiry, and operational tools (slack_search_*, slack_read_*, slack_send_message, etc.) are available without any user interaction.

Actual

The MCP server is listed as connected, but the only tools exposed are the bootstrap auth tools:

  • mcp__slack-frends__authenticate
  • mcp__slack-frends__complete_authentication

All operational tools are missing until the user manually approves a fresh OAuth flow.

Evidence

Keychain item Claude Code-credentials (account $USER), JSON blob, key mcpOAuth["slack-frends|<hash>"]. Structure (values redacted):

{
  "serverName": "slack-frends",
  "serverUrl": "https://mcp.slack.com/mcp",
  "accessToken": "<xoxe.xox...>",
  "refreshToken": "<xoxe-1-M...>",
  "expiresAt": 1777608318334,
  "scope": "channels:history ...",
  "discoveryState": {
    "authorizationServerUrl": "https://mcp.slack.com",
    "resourceMetadataUrl": "https://mcp.slack.com/.well-known/oauth-protected-resource",
    "oauthMetadataFound": true
  }
}

The expiresAt shown above had already passed by ~12 hours when the new session started. The refresh token is present and the metadata endpoint was discovered, so the client has everything it needs to refresh — it just isn't using it.

Workaround

Run the bootstrap auth tools manually each time:

  1. mcp__slack-frends__authenticate → returns an authorization URL
  2. Open URL in browser, approve
  3. Paste the redirect callback URL into mcp__slack-frends__complete_authentication

This works but is needed twice a day for active users, which adds up.

Why it matters

For workspace-integration MCP servers with short-lived access tokens (Slack is the obvious case), the lack of auto-refresh effectively turns a "set once, works" experience into a "re-auth twice a day forever" experience. Refresh tokens already exist in storage; this is a client-side gap, not a protocol limitation.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗