MCP OAuth: cannot connect to Microsoft Entra ID-protected servers (RFC 8707 resource must be App ID URI, conflicts with RFC 9728 canonical-resource check)

Resolved 💬 2 comments Opened Jul 9, 2026 by ryanfk Closed Jul 13, 2026

What happened

Claude Code's built-in remote MCP OAuth client cannot connect to an MCP server protected by Microsoft Entra ID (Azure AD), because Entra's resource-indicator requirement is mutually exclusive with the RFC 9728 canonical-resource check that the client enforces.

There appears to be no configuration (CLI flag, .mcp.json oauth field, or env var) to bridge the two, so Entra-protected MCP servers are effectively unusable with Claude Code today.

Version: Claude Code 2.1.174

The conflict

An Entra-protected resource server exposes a scope like api://<app-id-guid>/mcp.invoke. Two hard requirements collide:

  1. Entra (RFC 8707) — the resource parameter sent to the /authorize + /token endpoints must be the resource's App ID URI (api://<app-id-guid>). If it's anything else (e.g. the server's own URL), Entra rejects the request:

> AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.

  1. Claude Code (RFC 9728) — the client validates that the resource field in the server's /.well-known/oauth-protected-resource metadata equals the MCP server's own URL / origin, and it echoes that same value as the RFC 8707 resource param. If the metadata advertises the App ID URI, the client refuses to proceed:

> SDK auth failed: Protected resource api://<app-id-guid> does not match expected https://<server-host>/mcp (or origin)

So whichever value the server advertises, exactly one side fails:

| Metadata resource value | Entra /authorize | Claude Code check |
|---|---|---|
| https://<server-host>/mcp (server URL) | ❌ AADSTS9010010 | ✅ passes |
| api://<app-id-guid> (Entra App ID URI) | ✅ passes | ❌ "does not match expected …" |

The only value that could satisfy both is one where the server's URL is itself a registered Entra App ID URI — which requires hosting the server on a tenant-verified custom domain and registering that HTTPS URL as the app's identifier URI. That's infeasible for common cases (Azure Functions *.azurewebsites.net, *.azure-api.net, and internal-only DNS names are not Entra-verifiable domains).

Reproduce

  1. Register an Entra resource-server app; expose a scope api://<guid>/mcp.invoke.
  2. Host an MCP server (e.g. Azure Function) that serves RFC 9728 metadata and validates the Entra JWT.
  3. Register a public-client app with a loopback redirect; claude mcp add --transport http --client-id <public-client> --callback-port 8000 my-server https://<host>/mcp.
  4. /mcp → authenticate.
  • If metadata resource = server URL → browser sign-in, then AADSTS9010010.
  • If metadata resource = App ID URI → "Protected resource … does not match expected …" before the browser even opens.

Evidence the server is otherwise correct

Acquiring a token for the App ID URI out-of-band (a pre-authorized first-party client) and calling the server directly returns a valid MCP initialize result over HTTP 200 — so the server's Entra metadata, JWT/audience validation, and scope gate are all correct. The gap is solely in the client's inability to send a resource param that differs from the metadata/server URL.

Requested fix (any one would unblock)

  1. A config option (e.g. oauth.resource in .mcp.json or a --resource flag) to set the RFC 8707 resource parameter independently of the server URL — so it can be sent as api://<guid> while the client still connects to the azurewebsites.net host.
  2. An explicit Entra / Azure AD provider mode that knows to use the App ID URI (derivable from scopes_supported) as the resource.
  3. An opt-in flag to relax the RFC 9728 canonical-resource equality check to accept the value advertised in scopes_supported.

Microsoft's own guidance for Entra-protected MCP servers uses the App ID URI as the resource, so interoperating with Entra will likely require one of the above. Thanks!

View original on GitHub ↗

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