MCP OAuth: valid stored token for mcp.facebook.com/ads reported as "OAuth session expired and could not be refreshed" (issuer mismatch, no refresh token)

Status Open
Reported on v2.1.228
Maintainer reply None cached
Activity 1 comment · opened Aug 25, 2026

Follow-up to #86430, which was closed for lack of a reproduction. Related to #86023, still open, reported independently by other users.

The previous issue was closed because the maintainer could not run the scenario end to end ("it needs a Meta app ID and a Facebook login"). This report is written so that the core defect can be reproduced with no Meta app and no Facebook login: two curl calls are enough. The authenticated half is at the end, and we can run it on request.

Environment

  • Claude Code 2.1.243 (also seen on 2.1.228 and 2.1.233)
  • macOS 15 (Darwin 25.3.0), Apple Silicon
  • Server: https://mcp.facebook.com/ads, registered per Meta's docs with --transport http --client-id <YOUR_APP_ID>

Symptom

After a successful OAuth authorization, subsequent sessions fail. The visible error is a timeout:

connection timed out after 30000ms

With --debug, the real cause appears:

Failed to authenticate: OAuth session expired and could not be refreshed

The stored token is neither expired nor invalid. Using the same stored token directly over HTTPS, initialize, notifications/initialized and tools/list each complete in under a second and return all 66 tools. What fails is the client's OAuth layer, not the credential.

Reproduction without any credential

The discovery documents contradict each other. Anyone can see this:

curl -s https://mcp.facebook.com/.well-known/oauth-protected-resource/ads
{
  "resource": "https://mcp.facebook.com/ads",
  "authorization_servers": ["https://mcp.facebook.com/ads"]
}

The protected-resource document names https://mcp.facebook.com/ads as the authorization server. Now fetch that authorization server's metadata:

curl -s https://mcp.facebook.com/.well-known/oauth-authorization-server/ads
{
  "issuer": "https://www.facebook.com",
  "authorization_endpoint": "https://www.facebook.com/v26.0/dialog/oauth",
  "token_endpoint": "https://graph.facebook.com/v26.0/oauth/access_token",
  "grant_types_supported": ["authorization_code", "refresh_token"]
}

Two problems, both visible above:

  1. Issuer mismatch. The document is served from https://mcp.facebook.com but declares issuer: https://www.facebook.com. RFC 8414 section 3.3 requires the issuer value to match the location the metadata was retrieved from, and that check is what SEP-2352 added to the SDK. Meta's metadata cannot pass it.
  1. refresh_token is advertised but never issued. Meta's OAuth returns a long-lived access token with no refresh token. So once the client decides the session must be refreshed, there is nothing to refresh with, and the only possible outcome is the message above.

Our reading is that the client discards a perfectly valid stored credential and falls into a refresh path that cannot succeed. Whether the trigger is the failed issuer validation or a separate session-expiry heuristic is exactly the part we cannot see from outside.

What we would expect

A stored, unexpired access token should be used as is. If issuer validation must fail for a non-conformant provider, the error should say so instead of reporting an expired session and a 30 second timeout, which sends everyone looking at the network and at their credentials.

Current workaround

A local stdio bridge registered in place of the HTTP server. Claude Code speaks stdio to it, it speaks HTTPS to Meta with the token already stored in the Keychain, skipping the client's OAuth layer entirely. It has been running since 13/08 with no failures, which is further evidence that the credential and the endpoint are both fine.

Offer

We have the Meta app, the Business Manager and the ad account, which is what was missing last time. Tell us what you want captured and we will run the authenticated path with --debug and post the output. We monitor this issue and will reply within a day.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗