* 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.
Microsoft Model Context Protocol HttpServer - Distributed Session Affinity
Session-aware routing for Model Context Protocol (MCP) servers that need to run across multiple instances. This package builds on ASP.NET Core HybridCache and YARP so every MCP request reaches the server that owns the session state.
Important
This package is optional and is not required for MCP 2026-07-28 stateless protocol compliance. Use it only when you intentionally need custom stateful routing behavior across replicas.
Tip
For MCP 2026-07-28 deployments, prefer header-based stateless routing (
Mcp-Method,Mcp-Name) at your gateway/load balancer. Only enable session affinity when your application intentionally depends on server-local state.
When Not To Use It
- Your MCP server is stateless and follows MCP 2026-07-28 request semantics.
- You route requests through API gateways or reverse proxies using MCP routing headers.
- You do not rely on
Mcp-Session-Idownership for continuity.
Why Use It
- Keep in-memory session data (prompt history, tool context) with its owning instance
- Scale stateful MCP servers horizontally without changing tool handlers
- Forward requests automatically when the owning instance lives elsewhere
- Plug in any
IDistributedCache(Redis, SQL Server, NCache, etc.) for distributed storage
Install
dotnet add package Microsoft.ModelContextProtocol.HttpServer.Distributed --prerelease
Add the distributed cache provider that matches your environment (for example Microsoft.Extensions.Caching.StackExchangeRedis).
Quick Start (Stateful Opt-In)
using Microsoft.ModelContextProtocol.HttpServer;
using Microsoft.ModelContextProtocol.HttpServer.Distributed;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMicrosoftMcpServer(builder.Configuration)
.WithToolsFromAssembly();
builder.Services.AddMcpHttpSessionAffinity(); // Tracks ownership + routing
var app = builder.Build();
app.UseMicrosoftMcpServer();
app.MapMicrosoftMcpServer()
.WithSessionAffinity(); // Opt-in only when stateful affinity is required
app.Run();
No distributed cache is required until you add additional instances.
Production Checklist
- Register an L2 cache (Redis + Azure AD auth is the most battle-tested option).
- Set
LocalServerAddressto the routable address other replicas use (scheme, host, port). - Tune
ForwarderRequestConfigandHttpClientConfigfor your downstream SLAs. - Use
DefaultAzureCredentiallocally and deployment-specific credentials in production. - Monitor HybridCache hit rate and distributed cache availability for early warning.
Minimal Redis Configuration
using Azure.Identity;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using StackExchange.Redis;
var redisCredential = builder.Environment.IsDevelopment()
? new DefaultAzureCredential()
: new ManagedIdentityCredential();
var endpoint = builder.Configuration["Redis:Endpoint"]
?? throw new InvalidOperationException("Redis:Endpoint is required.");
var redisConfig = await ConfigurationOptions
.Parse(endpoint)
.ConfigureForAzureWithTokenCredentialAsync(redisCredential);
redisConfig.Ssl = true; // Always require TLS in production
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ConfigurationOptions = redisConfig;
options.InstanceName = "MCP:";
});
builder.Services.AddMcpHttpSessionAffinity(options =>
{
options.LocalServerAddress = builder.Configuration["Server:InternalAddress"]
?? throw new InvalidOperationException("Server:InternalAddress is required.");
});
appsettings.json
{
"Redis": {
"Endpoint": "your-mcp-session-affinity.region.redis.azure.net:6380"
},
"Server": {
"InternalAddress": "http://pod-1.mcp.default.svc.cluster.local:8080"
}
}
Core Concepts
- Session ownership: the first request with
Mcp-Session-Id(header) orsessionId(query) claims the session and stores ownership in HybridCache. - HybridCache tiers: L1 memory cache plus optional L2 distributed cache; tune expiration to control how long ownership survives inactivity.
- Forwarding: if the current node is not the owner, YARP forwards the request to the owning instance over HTTP(S).
- Stale detection: when an owning instance restarts, the affinity entry is discarded so clients can establish a fresh session and rebuild state.
Configuration Reference
SessionAffinityOptions.LocalServerAddress: required in multi-instance environments; must be a routable absolute URI.ForwarderRequestConfig: controls forwarding timeout, buffering, and HTTP version.HttpClientConfig: tune connection pooling for heavy cross-node routing.HybridCacheOptions: setDefaultEntryOptions.Expiration(L2) andLocalCacheExpiration(L1) to balance freshness versus resilience.SessionAffinityOptions.SessionEntryExpiration: overrides the default HybridCache expiration for ownership entries if you need tighter or looser affinity windows.
Critical Options at a Glance
| Scenario | What to Set | Why |
|---|---|---|
| Kubernetes / Service Mesh | SessionAffinityOptions.LocalServerAddress to the pod/service address |
Other pods must resolve this node for forwarding |
| High fan-out routing | HttpClientConfig.MaxConnectionsPerServer |
Avoid socket starvation when many sessions forward to a single owner |
| Short-lived sessions | HybridCacheOptions.DefaultEntryOptions.Expiration |
Let ownership expire quickly and reduce stale entries |
| Strict SLAs | ForwarderRequestConfig.ActivityTimeout and AllowedForwardedHeaders |
Control hop latency and prevent oversized buffered responses |
Dynamic Address Resolution
Use IPostConfigureOptions<SessionAffinityOptions> or a custom IListeningEndpointResolver when the routable address is only known at runtime (for example, Azure Container Apps or custom service discovery).
For environments that surface routing details through configuration providers (Kubernetes downward API, Azure App Configuration, Key Vault secrets, etc.), plug in an IListeningEndpointResolver that consumes IConfiguration:
using Microsoft.Extensions.Configuration;
public sealed class ConfigurationEndpointResolver : IListeningEndpointResolver
{
private readonly IConfiguration _configuration;
public ConfigurationEndpointResolver(IConfiguration configuration)
{
_configuration = configuration;
}
public string ResolveListeningEndpoint(IServer server, SessionAffinityOptions options)
{
var explicitAddress = _configuration["SessionAffinity:LocalServerAddress"];
if (!string.IsNullOrEmpty(explicitAddress))
{
return explicitAddress;
}
var host = _configuration["Pod:Ip"] ?? _configuration["POD_IP"] ?? "127.0.0.1";
var port = _configuration["Pod:Port"] ?? _configuration["PORT"] ?? "8080";
return $"http://{host}:{port}";
}
}
builder.Services.AddSingleton<IListeningEndpointResolver, ConfigurationEndpointResolver>();
builder.Services.AddMcpHttpSessionAffinity();
Example HybridCache tuning
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(15),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
Observability
- Enable
Microsoft.ModelContextProtocol.HttpServer.Distributedlogs atInformationby default andDebugfor routing traces. - Watch for
ResolvingSessionOwner,SessionEstablished, andForwardingRequestevents to understand ownership decisions. - Export HybridCache hit/miss metrics to confirm cache sizing and detect unusual churn.
Troubleshooting
- Requests never forward: confirm the session identifier is present and that ownership entries exist in the distributed cache.
- Forwarding loops or failures: ensure every replica sets
LocalServerAddressto a routable address and firewall rules allow peer-to-peer traffic. - Redis authentication errors: grant the managed identity or service principal the Azure Cache for Redis Data Contributor role.
- Slow forwarded requests: increase
MaxConnectionsPerServer, verify network latency, and monitor cache miss rates for hot sessions.