---
title: "Authenticate an MCP Server with Microsoft Entra ID (Azure AD) Managed Identity"
description: "Use an Azure managed identity to reach your MCP server with no stored secret. Verified code for Microsoft Entra ID (Azure AD), plus the /.default scope trap."
canonicalUrl: "https://zuplo.com/learn/mcp/authentication/azure-managed-identity"
pageType: "mcp-auth-guide"
method: "cloud-iam"
platform: "azure"
lastVerified: "2026-07-29"
specVersion: "2026-07-28"
---

# Authenticate an MCP Server with Microsoft Entra ID (Azure AD) Managed Identity

> When your agent runs in Azure, a managed identity gets it a Microsoft Entra ID (still widely called Azure AD) access token for your MCP server, with no secret in configuration and nothing to rotate.

**Microsoft Entra ID · Azure**

## The exchange

The agent asks Azure for an access token whose audience is the MCP server's Application ID URI, then presents it as a bearer token. The MCP server verifies the signature against Entra's JWKS, checks the audience, issuer and tenant claims, and reads the calling workload from the appid claim. Entra ID is never contacted by the server.

**Participants:** Your agent (VM, App Service, Functions), Entra ID (via the local endpoint), MCP server (validates the JWT)

1. **Your agent → Entra ID** — `GET …/identity/oauth2/token` — `resource=api://mcp.example.com`
2. **Entra ID → Your agent** — `access_token` — `aud = api://mcp.example.com` _(response; carries the credential)_
3. **Your agent → MCP server** — `POST /mcp` — `Authorization: Bearer <token>` _(carries the credential)_
4. **MCP server** (internal step) — Verify the signature against Entra's JWKS
5. **MCP server** (internal step) — Check aud, iss and tid, then read the caller — `appid = 00001111-aaaa-2222-bbbb-3333cccc4444`
6. **MCP server → Your agent** — `200 OK` — `tool result` _(response)_

Notes:

- **Steps 1–2** — Whatever you pass as resource is also what appears in the token's aud claim. You rarely write this request yourself: the Azure Identity libraries take the same audience as scope=api://…/.default and use whichever local token endpoint the host provides — on a VM that is IMDS, which requires the Metadata: true header.
- **Steps 4–5** — Validation is offline against cached JWKS; Entra is never called by the server. The tid check is what rejects a token from another tenant, and the aud check is what stops a token minted for a different Azure resource being replayed here.

## How it works

The agent asks Azure for an access token whose audience is the MCP server's Application ID URI, then sends it as `Authorization: Bearer <token>`. The Azure Identity libraries take that audience as a scope — the Application ID URI with `/.default` appended, one resource per request — while the local managed-identity endpoint underneath takes the same value as `resource=api://…`, which Microsoft documents as what lands in the token's `aud` claim. The server verifies the signature against Entra's JWKS and checks `aud`, `iss` and `tid` before reading the calling workload from `appid` or `oid`.

_MCP Specification 2026-07-28._

### At a glance

- **Best for** — Agents and MCP servers that both run in Azure, in one Entra tenant
- **MCP spec** — Out of scope of the spec's OAuth flow, which the spec makes optional
- **Works with** — Your own workloads, plus Microsoft Foundry Agent Service; no chat or desktop client can mint one
- **Effort** — A day, mostly the app registration and role assignments

## Connect your agent

Each sample connects to the server, sends the credential, and exposes the tools to the model.

### Python

#### MCP Python SDK

```python title="agent.py"
import asyncio, httpx2
from azure.identity import DefaultAzureCredential
from mcp import Client
from mcp.client.streamable_http import streamable_http_client

# Pass one scope: the Application ID URI with "/.default" appended.
# The managed identity credential allows only one scope per request.
token = DefaultAzureCredential().get_token("api://mcp.example.com/.default")

async def main():
    # streamable_http_client has no headers= argument. Headers, auth,
    # proxies and timeouts belong on the httpx2 client you hand it.
    async with httpx2.AsyncClient(
        headers={"Authorization": f"Bearer {token.token}"},
    ) as http_client:
        transport = streamable_http_client(
            "https://mcp.example.com/mcp", http_client=http_client)
        async with Client(transport) as client:
            print([t.name for t in (await client.list_tools()).tools])

asyncio.run(main())
```

#### OpenAI Agents SDK

```python title="agent.py"
import asyncio
from azure.identity import DefaultAzureCredential
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp

# Keep the credential, not the string: Entra access tokens last about
# an hour and the credential re-mints from its own cache.
credential = DefaultAzureCredential()

async def main():
    token = credential.get_token("api://mcp.example.com/.default")
    async with MCPServerStreamableHttp(
        name="Platform MCP",
        params={
            "url": "https://mcp.example.com/mcp",
            "headers": {"Authorization": f"Bearer {token.token}"},
        },
    ) as server:
        agent = Agent(name="Assistant", mcp_servers=[server])
        print((await Runner.run(agent, "List your tools.")).final_output)

asyncio.run(main())
```

### TypeScript

#### Claude Agent SDK

```typescript title="agent.ts"
import { DefaultAzureCredential } from "@azure/identity";
import { query } from "@anthropic-ai/claude-agent-sdk";

// getToken takes a scope, not a resource: the Application ID URI with
// "/.default" appended. Keep the credential and ask it for a token per
// run — a bearer string reused across runs dies after about an hour.
const credential = new DefaultAzureCredential();
const { token } = await credential.getToken(
  "api://mcp.example.com/.default",
);

for await (const message of query({
  prompt: "List your tools.",
  options: {
    mcpServers: {
      platform: {
        type: "http",
        url: "https://mcp.example.com/mcp",
        headers: { Authorization: `Bearer ${token}` },
      },
    },
  },
})) {
  console.log(message);
}
```

#### Vercel AI SDK

```typescript title="agent.ts"
import { DefaultAzureCredential } from "@azure/identity";
import { createMCPClient } from "@ai-sdk/mcp";

const { token } = await new DefaultAzureCredential().getToken(
  "api://mcp.example.com/.default",
);

// The MCP client lives in @ai-sdk/mcp, not `ai`. `type: "http"` is
// streamable HTTP.
const mcp = await createMCPClient({
  transport: {
    type: "http",
    url: "https://mcp.example.com/mcp",
    headers: { Authorization: `Bearer ${token}` },
  },
});

try {
  console.log(Object.keys(await mcp.tools()));
} finally {
  await mcp.close();
}
```

### C#

#### MCP C# SDK

```csharp title="Program.cs"
using Azure.Core;
using Azure.Identity;
using ModelContextProtocol.Client;

// DefaultAzureCredential picks up the managed identity when it runs in
// Azure and your own developer sign-in when it runs on your machine —
// the same code path, no secret either way.
var credential = new DefaultAzureCredential();
AccessToken token = await credential.GetTokenAsync(
    new TokenRequestContext(["api://mcp.example.com/.default"]));

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("https://mcp.example.com/mcp"),
    TransportMode = HttpTransportMode.StreamableHttp,
    AdditionalHeaders = new Dictionary<string, string>
    {
        ["Authorization"] = $"Bearer {token.Token}",
    },
});

await using var client = await McpClient.CreateAsync(transport);
foreach (var tool in await client.ListToolsAsync())
    Console.WriteLine(tool.Name);
```

## When to use something else

**Use this when:**

- The agent and the MCP server both run in Azure under one Entra tenant, so the platform issues the identity and your server only has to verify it.
- You want no stored secret: on Azure a managed identity is the client-credentials grant with the credential taken out.
- Tool calls need to be attributed to a specific workload — an `appid` or `oid` claim — rather than to a key that five services share.

**Use something else when:**

- An MCP client you do not control has to connect. The configuration surfaces in Claude, ChatGPT, Cursor and VS Code take static headers or run the spec OAuth flow; none of them can acquire an Azure token.
- You need a client to onboard itself. Microsoft's own guidance is that many providers do not support Dynamic Client Registration, 'including Microsoft Entra ID', so every client has to be pre-registered or preauthorized by hand.
- The call is made on behalf of a named person: a managed identity token is app-only, so there is no user in it to authorize or to audit.

> The specification makes authorization OPTIONAL and says HTTP transports SHOULD, rather than MUST, conform to its OAuth flow, so an Entra ID token presented as a bearer credential sits outside the spec rather than in conflict with it. That paragraph is word-for-word the same on the 2025-11-25 and 2026-07-28 authorization pages, so nothing in this method changed on 28 July 2026.

## What lands on you next

A managed identity answers one question well — is this workload allowed in — and only for workloads inside your own tenant. The questions that arrive next are all about callers Azure cannot vouch for.

- **Legal wants these tools in Claude with our Entra sign-in. Is that a rewrite?** — `mcp-entra-oauth-inbound` — No — the inbound method is a policy, and there is a first-class one for Entra ID. Exactly one MCP OAuth policy per project, and the Entra token never reaches the MCP client.
- **The token is valid but this app role is not. Where does that check go?** — `require-user-claims-inbound` — On the route. Allow or deny on any claim the token carries — `roles`, `appid`, `tid`, a group — and the caller gets a 403 before your server runs a line of code.
- **Can the reporting agent see fewer tools than the admin one?** — `mcp-capability-filter-inbound` — As two routes, yes: the filter is a static per-route allow-list with no per-user axis. A hidden tool is refused with `MethodNotFound` before the call reaches your server.
- **One agent looped overnight and burned the quota on the API behind it.** — `rate-limit-inbound` — `rateLimitBy: "user"` counts every tool call against the authenticated identity rather than a shared egress IP, so one workload cannot spend another's budget.

All of these attach to one MCP route's `policies.inbound` — the same policy engine on the way in and on the way out. See [Zuplo MCP Gateway](/mcp-gateway).

## FAQ

**Managed identity or service principal — which one for MCP?**

The same token either way; managed identity is the version with no credential to store. Use a service principal with a certificate or a federated credential only where a managed identity is unavailable, which in practice means outside Azure.

**Why scope=api://…/.default rather than the resource parameter?**

Two endpoints, two spellings of one value. The v2.0 token endpoint takes the resource identifier — the Application ID URI — in the scope parameter with .default appended, and rejects the older parameter — AADSTS901002 reports that the 'resource' request parameter isn't supported. The local managed-identity endpoint is the exception and still takes resource=. Either way that value is what appears in the token's aud claim, and every scope in one request must belong to a single resource.

**Should the audience be the Application ID URI or the server's URL?**

Whatever you registered as the Application ID URI on the app registration, character for character, and validate the same string in the server. Ask for an audience Entra does not know and the request fails with AADSTS500011: the resource principal named {name} wasn't found in the tenant named {tenant}. A malformed one fails earlier with AADSTS70011, the provided value for the input parameter 'scope' isn't valid.

**Does Entra ID support the MCP authorization flow?**

Only in part, and Microsoft says so plainly: some providers support Dynamic Client Registration, 'but many don't, including Microsoft Entra ID', so each client has to be preconfigured with a client ID and, preferably, preauthorized. Discovery is thinner than it looks too. Entra publishes OpenID Connect Discovery at /{tenant}/v2.0/.well-known/openid-configuration — the third endpoint a conformant client probes, since both higher-priority well-known forms 404 today — and that document omits code_challenge_methods_supported, whose absence the spec says must make a client refuse to proceed.

**Can Claude, ChatGPT or Cursor connect to a server protected this way?**

No. Their configuration surfaces accept static headers or the spec OAuth flow, and none of them can acquire an Azure token. The one hosted exception is Microsoft Foundry Agent Service, which authenticates to an MCP server with either the agent identity or the project's managed identity.

**Can the tool forward the caller's token to Microsoft Graph?**

No. The spec is explicit — the MCP server MUST NOT pass through the token it received from the MCP client — and Microsoft says the same about its own platform: pass-through scenarios 'create security vulnerabilities', and you should obtain a new token through the on-behalf-of flow. Foundry enforces it for managed OAuth: send a Microsoft-audience token to a custom MCP endpoint and Agent Service returns 'Cannot pass Microsoft token to untrusted MCP endpoint.'

**Is the Azure Functions MCP extension using Entra ID?**

No — it gates the endpoint with a shared secret. Hosted in Azure, the extension requires the mcp_extension system key on /runtime/webhooks/mcp: without it in the x-functions-key header or the code query string the client gets a 401. You can drop that requirement by setting system.webhookAuthorizationLevel to Anonymous in host.json, and either way you can layer App Service's built-in MCP server authorization on top as an identity-based access control layer.

**Did the 2026-07-28 spec release change anything for this method?**

No. Authorization is still OPTIONAL and HTTP transports still only SHOULD conform — that paragraph is unchanged between the two revisions.

## Keep reading

- **Google Cloud IAM** — [Authenticate an MCP Server with Google Cloud IAM](/learn/mcp/authentication/google-iam) — Agents that hold Google credentials, calling a server that can verify a Google token
- **Client credentials (M2M)** — [Machine-to-Machine (M2M) MCP Authentication with the Client Credentials Grant](/learn/mcp/authentication/client-credentials) — Cron jobs, CI steps and background workers, where no user is present to consent
- **AWS IAM (SigV4)** — [Authenticate an MCP Server with AWS IAM (SigV4)](/learn/mcp/authentication/aws-iam-sigv4) — Agents you write that run in AWS, calling a server behind Lambda, API Gateway, or AgentCore
- **MCP Authentication** — [every authentication method](/learn/mcp/authentication)
