10 Azure Entra ID Incompatibilities with MCP Server Authentication (And How to Fix Them)

Azure Entra ID Does Not Work with MCP Out of the Box

If you are building an MCP server and your organization uses Azure Entra ID (formerly Azure AD) for identity, you are going to have a rough time.

The MCP auth spec assumes your identity provider supports RFC 8414 (OAuth Authorization Server Metadata), RFC 7591 (Dynamic Client Registration), and RFC 8707 (Resource Indicators). Azure does not support any of these properly.

I hit 10 distinct incompatibilities while connecting Claude Code and MCP Inspector to an Azure-protected MCP server. Every single one required a workaround. This post covers each one in the order I encountered them, with the exact error codes and fixes.

If you are searching for AADSTS9010010, AADSTS90009, AADSTS50011, or "Incompatible auth server" in the context of MCP, you are in the right place.

The Solution: An OAuth Proxy

Before diving into the individual issues, here is the high-level pattern. Your MCP server acts as an OAuth proxy between MCP clients and Azure. The proxy intercepts the standard MCP auth flow, translates it into something Azure can handle, and returns responses that MCP clients expect.

The proxy serves four endpoints:

EndpointPurpose
GET /.well-known/oauth-authorization-serverServes Azure's OIDC metadata with overrides for MCP compatibility
GET /oauth/authorizeStrips unsupported parameters and redirects to Azure
POST /oauth/tokenStrips unsupported parameters and forwards to Azure
POST /oauth/registerMock Dynamic Client Registration, returns a pre-configured client ID

Your /.well-known/oauth-protected-resource points MCP clients at your own server (not Azure directly), so they discover the proxy endpoints.

Here is the full flow:

MCP Client connects to /mcp
  -> Server returns 401 + WWW-Authenticate header
  -> Client fetches /.well-known/oauth-protected-resource
  -> Sees authorization_servers = [your server URL]
  -> Client fetches /.well-known/oauth-authorization-server (from your proxy)
  -> Gets Azure OIDC metadata + your overrides
  -> Client POSTs /oauth/register (mock DCR)
  -> Gets pre-configured MCP client ID
  -> Client opens browser -> GET /oauth/authorize
  -> Proxy strips resource param, redirects to Azure
  -> User logs in at Azure
  -> Azure redirects back to client's localhost callback
  -> Client POSTs /oauth/token
  -> Proxy strips resource param, forwards to Azure
  -> Gets JWT access token
  -> Client sends JWT as Bearer token to /mcp
  -> Server validates JWT and processes the request

Now let's walk through each incompatibility.

1. No RFC 8414 Discovery (404 on oauth-authorization-server)

Error: HTTP 404: Invalid OAuth error response

MCP clients discover the authorization server by fetching /.well-known/oauth-authorization-server per RFC 8414. Azure does not serve this endpoint. Azure uses /.well-known/openid-configuration (OIDC Discovery) instead.

Fix: Your server serves /.well-known/oauth-authorization-server itself by fetching Azure's OIDC config and adding the fields MCP clients need:

@Get('.well-known/oauth-authorization-server')
async getAuthorizationServerMetadata() {
  const oidcUrl = `https://login.microsoftonline.com/${this.tenantId}/v2.0/.well-known/openid-configuration`;
  const res = await fetch(oidcUrl);
  const metadata = await res.json();

  return {
    ...metadata,
    authorization_endpoint: `${this.baseUrl}/oauth/authorize`,
    token_endpoint: `${this.baseUrl}/oauth/token`,
    registration_endpoint: `${this.baseUrl}/oauth/register`,
    code_challenge_methods_supported: ['S256'],
  };
}

This proxies Azure's real metadata while overriding the endpoints MCP clients will call to route through your proxy.

2. No Dynamic Client Registration (Incompatible Auth Server)

Error: Incompatible auth server: does not support dynamic client registration

MCP clients try RFC 7591 Dynamic Client Registration to register themselves and get a client ID. Azure does not support DCR.

Fix: A mock DCR endpoint that accepts any registration request and returns the pre-configured client ID for your MCP client app registration:

@Post('oauth/register')
register() {
  return {
    client_id: this.mcpClientId,
    client_name: 'MCP Client',
    redirect_uris: ['http://localhost'],
  };
}

This is the workaround I like the most. It is simple, it works, and it lets MCP clients follow their normal auth flow without knowing Azure is behind the scenes.

3. Resource Parameter Rejected (AADSTS9010010)

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

The MCP spec mandates RFC 8707 Resource Indicators. Azure v2 endpoints do not implement RFC 8707. When MCP clients include the resource parameter, Azure rejects the request.

Fix: Your OAuth proxy strips the resource parameter from both /authorize and /token requests before forwarding to Azure:

@Get('oauth/authorize')
authorize(@Query() query: Record<string, string>, @Res() res) {
  const params = new URLSearchParams(query);
  params.delete('resource');
  res.redirect(`${this.azureBase}/authorize?${params.toString()}`);
}

@Post('oauth/token')
async token(@Body() body: Record<string, string>) {
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(body)) {
    if (key !== 'resource') {
      params.append(key, String(value));
    }
  }

  const azureRes = await fetch(`${this.azureBase}/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: params.toString(),
  });

  return azureRes.json();
}

The scope parameter (api://{clientId}/.default) already encodes the resource, so stripping resource is safe.

4. Requesting a Token for Itself (AADSTS90009)

Error: AADSTS90009: Application is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App Identifier.

This one is a real head-scratcher the first time you see it. If you use the same app registration as both the OAuth client and the API, Azure blocks the token request because it thinks the app is requesting a token for itself.

Fix: You need two app registrations in Azure Entra ID.

API app registration:

  • Defines app roles (e.g., Admin, User)
  • Exposes an API with a scope (e.g., access)
  • Has a client secret for server-side use
  • Web redirect URI: {BASE_URL}/auth/callback

Client app registration (for MCP clients):

  • Public client (no secret). MCP clients use auth code + PKCE
  • "Allow public client flows" enabled under Authentication
  • Mobile/Desktop redirect URI: http://localhost/callback
  • Granted delegated permission to the API app's access scope
  • Admin consent granted

Your mock DCR endpoint returns the client app's ID. Your JWT validation uses the API app's ID as the audience.

This is probably the most confusing step if you have not done it before. Azure's portal does not make it obvious that you need two registrations.

5. API Permissions Not Exposed by Default

This is not an error code. It is a portal problem.

When you create the client app registration and try to add API permissions for your API app, the permission picker is empty. Your API app does not show up.

Fix: In the API app registration, go to "Expose an API" and add a scope. Until you explicitly expose a scope, Azure will not let other apps request permissions for it. After adding the scope, the client app can find and request it.

6. Redirect URI Path Mismatch (AADSTS50011)

Error: AADSTS50011: The redirect URI specified in the request does not match the redirect URIs configured for the application.

MCP clients like Claude Code use http://localhost/callback or http://127.0.0.1/callback as their redirect URI. Azure requires exact redirect URI matching, including the path.

Fix: In the client app registration, add the exact redirect URI your MCP client uses. For Claude Code, this is typically http://localhost/callback under "Mobile and desktop applications" (not "Web"). You may need to add both localhost and 127.0.0.1 variants.

The category matters. Using "Web" instead of "Mobile and desktop applications" will cause different behavior with PKCE and public client flows.

7. PKCE code_challenge_methods_supported Not Advertised

Azure supports PKCE with S256, but it does not include code_challenge_methods_supported in its metadata. MCP clients see the omission and may assume PKCE is not supported or behave unpredictably.

Fix: Your OAuth proxy injects code_challenge_methods_supported: ['S256'] into the authorization server metadata response (shown in the fix for incompatibility #1).

8. JWT Issuer v1 vs v2 Mismatch

Azure issues tokens with the v1 issuer (https://sts.windows.net/{tenant}/) even when you are using v2 endpoints, unless accessTokenAcceptedVersion is explicitly set to 2 in the app manifest.

If your JWT validation only checks for the v2 issuer, tokens will fail validation silently.

Fix: Accept both issuers in your JWT validation:

issuer: [
  `https://login.microsoftonline.com/${tenantId}/v2.0`,
  `https://sts.windows.net/${tenantId}/`,
];

You can also set accessTokenAcceptedVersion to 2 in the app manifest, but accepting both issuers is more defensive. You do not want authentication to break because someone changed a manifest setting.

9. JWT Audience Format Inconsistency

Tokens may use either api://{clientId} or the bare GUID as the aud claim, depending on how the scope was requested.

Fix: Accept both audience formats:

audience: [clientId, `api://${clientId}`];

This is a small fix but easy to miss. If you only validate one format, some valid tokens will be rejected.

10. NestJS Guard DI Context Issue

This one is not Azure's fault, but it is triggered by the Azure auth setup.

If you are using NestJS with a library like @rekog/mcp-nest, guards passed to the library's guards option are resolved in the library's internal module context. That context does not import your ConfigModule, so guards that inject ConfigService fail at bootstrap.

Fix: Use process.env directly in your auth guard instead of ConfigService:

@Injectable()
export class AuthGuard implements CanActivate {
  private readonly azureEnabled =
    !!process.env.AZURE_TENANT_ID && !!process.env.AZURE_CLIENT_ID;
  private readonly baseUrl =
    process.env.BASE_URL || 'http://localhost:3002';

  canActivate(ctx: ExecutionContext): boolean {
    if (!this.azureEnabled) return true;
    // ... validation logic
  }
}

The values are static env vars read once at startup. There is no reason to route them through DI when the DI context is not yours to control.

The End Result Is Clean

After all that plumbing, the actual authorization code is small. Here is the admin guard that protects sensitive MCP tools:

@Injectable()
export class AdminGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest<{ actor?: Actor }>();
    return req.actor?.role === 'admin';
  }
}

Ten lines. All the Azure complexity is absorbed by the middleware and proxy layers, so your business logic never has to think about it.

Environment Variables

For reference, here are the environment variables the proxy and auth layers need:

AZURE_TENANT_ID=<your-tenant-id>
AZURE_CLIENT_ID=<api-app-client-id>
AZURE_CLIENT_SECRET=<api-app-client-secret>
AZURE_MCP_CLIENT_ID=<client-app-client-id>
BASE_URL=<your-server-public-url>

When AZURE_TENANT_ID is unset, the system falls back to a dev mode that skips auth entirely. That makes local development easy while keeping production locked down.

Why This Matters

Azure Entra ID is the most common enterprise identity provider. MCP is the emerging standard for connecting AI tools to APIs. The fact that these two do not work together without a proxy layer is a real problem for enterprise adoption.

Microsoft is aware of these issues. There is a blog post from Microsoft about building MCP servers with Entra ID, and there are open issues in the MCP SDK tracking Azure compatibility (#862). Claude Code also has an open issue about DCR (#52638).

The MCP spec itself has a proposal to make the resource parameter optional, which would help with incompatibility #3.

Until these get resolved, expect to build and maintain an OAuth proxy if you are connecting MCP clients to Azure-protected APIs. The proxy is not complicated (the entire implementation is under 70 lines), but you need to know it is necessary before you spend hours debugging cryptic Azure error codes.

Quick Reference: Error Codes

If you landed here from a search, here is a quick lookup:

ErrorIssueFix Section
HTTP 404 on /.well-known/oauth-authorization-serverNo RFC 8414#1
Incompatible auth serverNo DCR#2
AADSTS9010010Resource parameter rejected#3
AADSTS90009Token for itself#4
Empty permission picker in portalAPI not exposed#5
AADSTS50011Redirect URI mismatch#6
PKCE not advertisedMissing metadata field#7
JWT validation fails silentlyIssuer v1/v2 mismatch#8
JWT audience rejectedapi:// prefix inconsistency#9
NestJS bootstrap errorDI context mismatch#10

Good luck out there. Azure auth is a solvable problem, it just has more steps than you expect.