* Phase 1 MCP stateless migration baseline * Phase 1 elicitation wrapper hardening and HTTP stateless transport tests * Phase 1 core test harness migration: fix obsolete SDK API errors and add negative HTTP test * Phase 1 mode coverage and session-affinity regression tests - Add ServerModeCoverageTests: stdio stateless tools/list for single, namespace, and all modes without initialize handshake (Workstream I item 5) - Add Mcp-Session-Id absence assertion to HTTP positive test in ConsolidatedModeTests (Workstream I item 7 / Workstream B): server must not set Mcp-Session-Id under the 2026-07-28 stateless protocol * Phase 1 Workstream B item 2: Mcp-Method/body mismatch rejection test Add negative test that sends Mcp-Method: tools/call in headers but tools/list in the JSON body. Under the 2026-07-28 protocol header/body agreement is required for explicit gateway routing; server must respond with a 4xx client error for a mismatch. * Phase 2: harden HTTP header/meta conformance tests * Phase 2: enforce required MCP HTTP routing headers * Phase 2: implement W3C trace context propagation - Add W3C trace context support (traceparent, tracestate, baggage) to _meta handling - Capture trace context fields in both CallToolHandler and ListToolsHandler - Add W3C trace context tags to telemetry constants (w3c.traceparent, w3c.tracestate, w3c.baggage) - Add comprehensive test for W3C trace context propagation in HTTP mode - Verified Azure telemetry enrichment remains compatible with new trace context capture * Phase 3: establish deterministic tool resolution fallback for sampling deprecation Workstream E: Sampling deprecation and deterministic routing - Add telemetry tracking for sampling vs deterministic resolution (resolution.method, sampling.attempted, sampling.succeeded, resolution.confidence) - Implement DeterministicToolResolution utility with token-based matching and Levenshtein distance for command/tool inference - Support deterministic fallback when sampling is unavailable or deprecated - Add 8 comprehensive unit tests validating tool name and command matching - Fix pre-existing RequestContext constructor issue in ToolLoaderTelemetryTests Strategy: Keep sampling as optional enhancement while building deterministic path parity through telemetry measurement. Enables Phase 3 sampling deprecation roadmap. * Phase 2: finalize session-affinity disposition and operator guidance - Record Workstream B item 4 decision: session-affinity package remains optional for explicit legacy/custom state-routing scenarios only - Keep package out of default Azure MCP runtime assumptions - Update operator guidance to header-first stateless routing for MCP 2026-07-28 (Mcp-Method/Mcp-Name) - Clarify when not to enable session affinity in distributed package README * Phase 2: add stateless server/discover discovery coverage - Add consolidated-mode HTTP integration test for server/discover with MCP 2026-07-28 routing headers - Validate discovery succeeds without initialize handshake - Validate stateless response does not emit Mcp-Session-Id - Record Workstream C progress in migration proposal * Phase 2: codify startup/discovery split and list cache semantics * Phase 2: mark cache metadata emission as SDK-gated * MCP 2026-07-28: fix HTTP routing-header conformance and registry OAuth validation Phase 2 (Workstream B / SEP-2243): gate Mcp-Method/Mcp-Name enforcement on the 2026-07-28 protocol version and require Mcp-Name only for named methods (tools/call, resources/read, prompts/get), preserving legacy 2025-11-25 HTTP interop. Correct and extend ConsolidatedMode header tests (named-method rejection, non-named-method acceptance, legacy initialize interop). Workstream G: add outbound OAuth validation to RegistryServerProvider, remove misleading dead issuer-binding branch, and correct the OAuth resource-parameter RFC citation (RFC 8707) in Authentication.md. Workstream I item 6: add ServerToolLoader external-server discovery and proxy-routing fallback tests. Docs: record Phase 1/2 and Workstream G verification findings in the migration proposal. * Remove MCP migration proposal doc from PR (design/proposal doc, not shipped) * Fix test harness for MCP 2026-07-28 beta SDK (Workstream I) The beta SDK (2.0.0-preview.1) added a private protected abstract member (ResolveInputRequestsAsync) to McpClient, making it impossible to proxy with NSubstitute or subclass from a test assembly; every Substitute.For<McpClient>() threw TypeLoadException at mock-creation time. Add a reusable LoopbackMcpClient test helper that builds a real McpClient over an in-process loopback transport (forcing the legacy 2025-11-25 handshake) and answers requests via a responder. Convert all McpClient mock sites (MockMcpClientBuilder, MockMcpDiscoveryStrategyBuilder, ServerToolLoaderTests, SingleProxyToolLoaderTests, BaseDiscoveryStrategyTests) to the loopback client, and observe client disposal via a transport-backed tracker. Also fix three pre-existing migration test gaps surfaced by the full run: - ServerStartCommandTests.InvalidMode: expect ClientTransportClosedException (wrapping IOException), the SDK's new early-transport-failure type. - ClientToolTests ping: assert ping is unavailable on the 2026-07-28 protocol (method name retained for recorded-session matching). - McpRuntimeTests null-parameter test: pass null Params so the runtime null-guard is exercised. Core, Microsoft.Mcp.Core, and Azure.Mcp.Server test suites are green. * Fix whitespace formatting in migrated tool-loader tests Normalize over-indented lines flagged by dotnet format (WHITESPACE) that were introduced during the SDK RequestContext API migration edits. * Make InvalidMode test platform-agnostic ClientTransportClosedException.InnerException is platform-dependent (IOException on Windows, null on Linux CI). Assert only the outer ClientTransportClosedException, which is thrown consistently and already proves the invalid mode fails to start the server. * Address Copilot PR review: test hangs, 404 leak, error body, doc - HTTP test processes: disable stdout/stderr redirection (never read by HTTP tests) to eliminate child-process pipe-buffer hang risk. Stdio tests that read stdout keep stdout redirect but disable stderr. (Copilot #1/#2) - SendWithRetryAsync: dispose the 404 HttpResponseMessage before retrying to prevent socket/handler leaks. (Copilot #5/#6) - WriteMissingRoutingHeaderResponse: emit a proper JSON-RPC 2.0 error envelope and use JsonEncodedText.Encode + .ToString() for correct string escaping. (Copilot #3 / my F1) - RegistryServerProvider class doc: replace the misleading Authorization Hardening bullet list with accurate language — only HTTPS + non-empty scope checks are performed here; issuer/app-type/PRM checks are delegated to Azure Identity. (Copilot #4 / my F3) * Fix Fabric ServerStartupTests: restore stderr redirection broken by redirect fix The previous change (afb871c4) set RedirectStandardError = false on all Fabric test blocks. However all three tests call process.BeginErrorReadLine() and assert on the captured stderr to detect DI errors — BeginErrorReadLine() requires RedirectStandardError = true and throws InvalidOperationException otherwise. Restored to true in all three test blocks. * Fix import ordering in McpServerElicitationExtensionsTests * Fix Authentication.md: clarify issuer validation is a deployment requirement, not server-enforced Three places in Authentication.md overclaimed that the Azure MCP Server enforces issuer validation for outbound registry connections. The code (ValidateOAuthConfiguration) only checks HTTPS and non-empty scopes; issuer/audience validation is performed by the downstream resource via Azure Identity bearer tokens. Updated: (1) the remote auth layer description, (2) the MCP 2026-07-28 note box, (3) the OAuth requirements table — separating enforced checks (HTTPS, non-empty scopes) from deployment requirements (issuer, app-type, scope registration). Addresses RickWinter review comment. * Defer DeterministicToolResolution and dead telemetry constants to Phase 3 PR DeterministicToolResolution.cs (220 lines) and its test file (189 lines) have no production callers — all three loader paths still guard intent resolution with SupportsSampling(), so a client without sampling gets the learn response and never reaches this resolver. The four companion telemetry constants (ResolutionMethod, SamplingAttempted, SamplingSucceeded, ResolutionConfidence) are also never emitted. Per RickWinter's review, defer to the Phase 3 PR that actually wires up deterministic routing so the surface lands with its integration tests. The code is preserved in commitae8ff115and can be cherry-picked into the Phase 3 branch. * Harden W3C trace context capture: validate inputs before recording to telemetry CaptureToolCallMeta accepted traceparent, tracestate, and baggage verbatim from untrusted MCP _meta fields. An attacker controlling tool-call input could stuff arbitrary data into these fields and have it written into distributed traces. - traceparent: validate against the W3C format regex (^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$) before adding the tag; silently drop malformed values. - tracestate: cap at 512 chars per W3C spec guidance; drop if longer. - baggage: removed entirely. Baggage is an unbounded cross-service propagation bag, not a trace identifier. Recording it verbatim lets callers write arbitrary key-value pairs into telemetry. Remove the TagName.Baggage constant as it is now unused. * Fix Insights build against MCP SDK 2.0.0-preview.1: suppress deprecated sampling APIs Azure.Mcp.Tools.Insights was added to main after this branch was cut. When CI merges the PR into main the Insights source is compiled against the upgraded SDK, which made four sampling APIs error-level obsolete (MCP9005): CreateMessageRequestParams, SamplingMessage, McpServer.SampleAsync, ClientCapabilities.Sampling. Additionally, CommandContext.McpServer and CommandContext.ProgressToken were removed from CommandContext in this migration, causing CS1061 errors in InsightsGetCommand. Fixes: - Restore McpServer? and ProgressToken? to CommandContext (neither type is deprecated; only McpServer.SampleAsync is). - Restore McpServer = request.Server / ProgressToken = request.Params.ProgressToken in CommandFactoryToolLoader and NamespaceToolLoader. - Add NoWarn>MCP9005 to the Insights src and test csproj files with a comment noting migration to MRTR is tracked as Phase 3 work. All 54 Insights unit tests pass. * Fix broken link in Authentication.md: correct csharp-sdk issue URL * Address review: per-request client info, routing-header middleware docs, telemetry tests, changelog Three open review comments addressed: Comment 2 (alzimmermsft): In the 2026-07-28 stateless protocol there is no initialize handshake, so request.Server.ClientInfo is null on every request. Clients embed their identity in each request's _meta under 'io.modelcontextprotocol/clientInfo'. Update TelemetryService.StartActivity to read client name/version from _meta when present (per-request wins over session-level clientInfo). - McpHelper: add ClientInfoMetaKey/ClientInfoNameKey/ClientInfoVersionKey constants - ITelemetryService.StartActivity: add RequestParams? parameter - TelemetryService: extract SetClientNameAndVersion helper; reads from _meta when requestParams != null - McpRuntime: pass request.Params to both StartActivity calls Comment 1 (vukelich): Document why ValidateMcpRoutingHeadersMiddleware exists alongside the SDK's own ValidateMcpHeaders. The SDK validates post-body-parse; at the Kestrel 30 MB limit it throws BadHttpRequestException instead of a clean JSON-RPC error. Our middleware rejects before the body is read. Updated block comment in ServiceStartCommand.cs. New tests (15 total): - McpRuntimeTests: 9 tests for CaptureToolCallMeta (valid/invalid traceparent, tracestate length cap, baggage never recorded, null meta) - TelemetryServiceTests: 6 tests for SetClientNameAndVersion (null/null, session-only, _meta-only, _meta wins over session, non-string values ignored, missing key) - Exposes CaptureToolCallMeta as internal TestHook_CaptureToolCallMeta; adds Azure.Mcp.Core.Tests to InternalsVisibleTo Changelog: added servers/Azure.Mcp.Server/changelog-entries/1784709301470.yaml for the MCP 2026-07-28 migration. * Add inline comments explaining elicitation wrapper and stateless ClientInfo behaviour * Fix OTLP trace export: register SDK and ASP.NET Core activity sources Two bugs in the OTLP trace pipeline (pre-existing on main, not introduced by this PR): 1. AddSource('Azure.Mcp.Server') was registered in ConfigureOpenTelemetryTracerProvider but not in the OTLP WithTracing chain. These are separate TracerProviderBuilder instances so the OTLP exporter never received those spans. Fixed by also calling AddSource in the OTLP WithTracing block. 2. ListToolsHandler/ToolExecuted spans are children of the ASP.NET Core HTTP request span. Without AddAspNetCoreInstrumentation(), the root HTTP span is unsampled and the entire trace tree is dropped. Added AddAspNetCoreInstrumentation() to the OTLP WithTracing chain so the full POST / -> ListToolsHandler -> (child ops) trace hierarchy appears in Aspire/OTLP collectors. Also added AddSource for ModelContextProtocol and ModelContextProtocol.Server to export MCP SDK-internal spans in the same trace. Validated locally: old server (no fix) shows zero tool traces in Aspire even after a successful tools/call. New server (with fix) shows full POST / + ToolExecuted trace hierarchy with ClientName, ClientVersion, w3c.traceparent, and w3c.tracestate tags.
12 KiB
Authentication in Azure MCP Server
Azure MCP Server runs in two transport modes — stdio (local) and HTTP (remote) — each with a different authentication model.
| Stdio (local) | HTTP (remote) | |
|---|---|---|
| Who authenticates to Azure? | The server itself, using your developer credentials | Depends on the outgoing auth strategy (see below) |
| Inbound auth required? | No — communication is over local process pipes | Yes — Entra ID bearer tokens protect every request |
| Typical use | IDE extensions, CLI, local agents | Cloud-hosted server shared by multiple clients/agents |
Local Authentication (Stdio Transport)
When the server runs locally (the default), it authenticates directly to Azure services using credentials available on your machine.
How credentials are resolved
The server uses a chain of credential providers. The first one that succeeds is used:
flowchart TD
Start{"Broker mode?"} -- yes --> Broker["OS broker (WAM on Windows)"]
Start -- no --> Env["EnvironmentCredential"]
Env -- fail --> VS["VisualStudioCredential"]
VS -- fail --> VSCode["VisualStudioCodeCredential"]
VSCode -- fail --> CLI["AzureCliCredential"]
CLI -- fail --> PS["AzurePowerShellCredential"]
PS -- fail --> AZD["AzureDeveloperCliCredential"]
AZD -- fail --> Browser["InteractiveBrowserCredential"]
Browser -- fail --> Failed(["Authentication failed"])
Broker --> OK(["Authenticated"])
Env -- ok --> OK
VS -- ok --> OK
VSCode -- ok --> OK
CLI -- ok --> OK
PS -- ok --> OK
AZD -- ok --> OK
Browser -- ok --> OK
Note
You can skip the chain and pin a specific credential by setting the
AZURE_TOKEN_CREDENTIALSenvironment variable to the credential name. For example, to use only Azure CLI:AZURE_TOKEN_CREDENTIALS=AzureCliCredential.
Tip
In VS Code, run Azure: Sign In from the Command Palette (ctrl+shift+p) to authenticate quickly.
Broker mode
Set AZURE_MCP_ONLY_USE_BROKER_CREDENTIAL=true to skip the chain entirely and use the OS authentication broker (Web Account Manager on Windows). On unsupported operating systems it falls back to browser-based login.
RBAC
Regardless of which credential is used, the authenticated identity must have the appropriate Azure RBAC roles on the target resources (for example, Storage Blob Data Reader for storage operations). See What is Azure RBAC? for details. For User-Assigned Managed Identity, also set AZURE_CLIENT_ID to the managed identity's client ID. If unset, System-Assigned Managed Identity is used.
Choosing credentials by environment
| Environment | Recommended setup |
|---|---|
| Local development | Sign in with az login, VS Code Azure extension, or Azure PowerShell. The chain picks it up automatically. |
| CI / CD pipelines | Set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, and AZURE_TENANT_ID environment variables (service principal). |
| Production (hosted) | HTTP transport is recommended for production hosting — see Remote Authentication (HTTP Transport). If STDIO is required, set AZURE_TOKEN_CREDENTIALS=prod to restrict the chain to Environment, Workload Identity, and Managed Identity credentials only — no interactive browser fallback. |
Remote Authentication (HTTP Transport)
When the Azure MCP Server is hosted remotely (for example, on Azure Container Apps) using HTTP transport, authentication operates in two layers:
- Inbound authentication — the client authenticates to the Azure MCP Server (protocol-hardened in MCP 2026-07-28)
- Outbound authentication — the Azure MCP Server authenticates to downstream Azure services and external MCP servers (see External MCP Server OAuth Compliance for registry server requirements under MCP 2026-07-28)
sequenceDiagram
participant Client
participant AzureMCPServer as Azure MCP Server
participant Entra as Entra ID
participant Azure as Azure Services
Client->>Entra: 1. Obtain bearer token
Client->>AzureMCPServer: 2. Call MCP Server with bearer token
AzureMCPServer->>Entra: 3. Validate inbound token
AzureMCPServer->>Entra: 4. Obtain outbound token
AzureMCPServer->>Azure: 5. Call Azure service with outbound token
Note
MCP 2026-07-28 changes: Inbound authentication validates JWT issuer, audience, and scope via Microsoft.Identity.Web. For registry-backed external MCP servers, HTTPS and non-empty scope entries are enforced by the server; issuer/audience validation of the outbound Azure Identity token is performed by the downstream resource. See External MCP Server OAuth Compliance for details.
Tip
Want to get started quickly? The
azdtemplates below automate the entire setup illustrated above.
Inbound Authentication
Every incoming request must carry a valid Entra ID bearer token with the required claims in the Authorization header. The inbound token can be obtained in one of two ways depending on the OAuth flow the client uses:
| OAuth Flow | Inbound Authentication | Required Claim |
|---|---|---|
| Authorization Code | Delegated — a user signs in and obtains a bearer token | Mcp.Tools.ReadWrite scope |
| Client Credentials | Application — an application obtains a bearer token (no user involved) | Mcp.Tools.ReadWrite.All role |
Note
The inbound authentication is determined entirely by the OAuth flow the client uses — the Azure MCP Server validates the incoming token and required claim but does not control how it is obtained.
Outbound Authentication
The --outgoing-auth-strategy flag (UseOnBehalfOf or UseHostingEnvironmentIdentity) is passed when starting the Azure MCP Server and controls how it obtains outbound tokens to access downstream Azure services:
| Outbound Authentication | Behavior |
|---|---|
On-Behalf-Of (UseOnBehalfOf) |
Azure MCP Server exchanges the inbound user bearer token for a new token to access the downstream service |
Hosting Environment Identity (UseHostingEnvironmentIdentity) |
Azure MCP Server uses the hosting environment identity (typically a Managed Identity) to obtain a token to access the downstream service |
Note
If
--outgoing-auth-strategyis not specified, the server defaults to On-Behalf-Of .
Supported Authentication Combinations
Use the following to verify your chosen inbound and outbound combination is supported:
| Inbound Authentication | Outbound Authentication | Supported |
|---|---|---|
| Delegated | On-Behalf-Of | ✅ |
| Delegated | Hosting Environment Identity | ✅ |
| Application | Hosting Environment Identity | ✅ |
| Application | On-Behalf-Of | ❌ Application bearer token carries no user identity, so there is nothing to exchange in an On-Behalf-Of flow |
Choosing an Outbound Authentication Strategy
The right strategy depends on your security, auditing, and deployment requirements:
| On-Behalf-Of | Hosting Environment Identity | |
|---|---|---|
| Per-user RBAC | Yes | No — shared identity |
| Audit trail | Per-user | Server identity only |
| Inbound auth type | Delegated only | Delegated or Application |
| Setup complexity | Higher | Lower |
| Best for | Multi-tenant / enterprise / compliance-sensitive scenarios | Single-team or single-client-application scenarios |
Deploy with azd Templates
Configuring Entra ID app registrations, scopes, roles, and hosting infrastructure manually is complex. The following Azure Developer CLI (azd) templates automate the entire setup including deploying Azure MCP Server to Azure Container Apps:
| Template | Outbound Strategy | Client (Agent) |
|---|---|---|
| azmcp-obo-template | On-Behalf-Of | Foundry Agent, Copilot Studio, C# Client |
| azmcp-foundry-aca-mi | Hosting Environment Identity | Foundry Agent |
Each azd template provisions:
- Entra ID app registration(s) with the correct scopes and roles
- Managed identity with appropriate RBAC assignments
- Azure Container App configured to run the Azure MCP Server with all required environment variables and server flags
External MCP Server OAuth Compliance (MCP 2026-07-28)
When the Azure MCP Server acts as an MCP client to external, OAuth-protected MCP servers (configured via registry), it must comply with MCP 2026-07-28 authorization hardening rules.
OAuth Configuration Requirements
External MCP servers configured in the registry with OAuthScopes must meet these requirements:
| Requirement | Details |
|---|---|
| HTTPS endpoint | All OAuth-protected registry servers must use HTTPS (https:// scheme) — enforced by the server |
| Non-empty scopes | All scopes in OAuthScopes must be non-empty strings — enforced by the server |
| Issuer validation | A deployment requirement: the server's issuer identity should be resolvable and validated by its OAuth provider. Azure Identity bearer tokens carry issuer/audience claims that the downstream resource validates; the Azure MCP registry client does not perform additional client-side issuer binding |
| Application type | A deployment requirement: registry servers should be registered as the correct OAuth application type (confidential vs public client) with their OAuth provider |
| Scope registration | A deployment requirement: all scopes in OAuthScopes should be registered with the OAuth provider |
| Protected resource metadata | Recommended: Publish .well-known/oauth-protected-resource metadata per OAuth Protected Resources spec |
Known Limitation: MCP Protocol Resource Parameter
Issue: MCP 2026-07-28 protocol specifies that token endpoint requests include a resource parameter (per RFC 8707, Resource Indicators for OAuth 2.0), but some OAuth providers (e.g., Entra ID) do not support this parameter.
Impact: Access tokens are currently obtained without the resource parameter. This may result in overly-broad token scope.
Workaround: Configure your OAuth provider to accept token requests from Azure MCP Server even without the resource parameter. This is a known limitation documented in modelcontextprotocol/csharp-sdk issue #939.
Recommended mitigation strategies:
- Use narrow OAuth scopes that limit token usage to specific resources
- Implement rate limiting or request signing to protect token endpoints
- Monitor token usage patterns for unexpected requests
- Follow OAuth provider guidance for handling requests without the resource parameter
Issuer Binding and Validation
When registering an external MCP server with OAuth scopes, ensure that:
- The server's URL matches its registered issuer identity
- The issuer is resolvable and publishes OpenID Connect metadata
- Your OAuth provider can validate the issuer claim in received tokens
Example registry entry for an OAuth-protected MCP server:
{
"servers": {
"external-secure-server": {
"url": "https://my-mcp-server.example.com",
"title": "External MCP Server",
"OAuthScopes": ["mcp:invoke"]
}
}
}
In this example:
- URL and issuer match (
my-mcp-server.example.com) - OAuth scopes are explicit and minimal
- The server is registered in your OAuth provider as a resource/API
Testing OAuth-Protected External Servers
To verify compliance:
- Verify issuer resolution: Ensure the server's
.well-known/openid-configurationis reachable - Test token acquisition: Confirm tokens can be obtained with your configured scopes
- Test token usage: Verify that MCP requests succeed with acquired tokens
- Test scope validation: Confirm that requests with insufficient scopes are rejected
- Test issuer mismatch: Verify rejection when token issuer doesn't match server identity