Files
mcp-microsoft/core/Microsoft.ModelContextProtocol.HttpServer.Distributed
Vinay Gera b115a0c1e4 Migrate to mcp stateless protocol SDK Phase 1 & 2. (#3016)
* 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 commit ae8ff115 and 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.
2026-07-23 00:19:54 -07:00
..
…
…

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-Id ownership 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

  1. Register an L2 cache (Redis + Azure AD auth is the most battle-tested option).
  2. Set LocalServerAddress to the routable address other replicas use (scheme, host, port).
  3. Tune ForwarderRequestConfig and HttpClientConfig for your downstream SLAs.
  4. Use DefaultAzureCredential locally and deployment-specific credentials in production.
  5. 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) or sessionId (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: set DefaultEntryOptions.Expiration (L2) and LocalCacheExpiration (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.Distributed logs at Information by default and Debug for routing traces.
  • Watch for ResolvingSessionOwner, SessionEstablished, and ForwardingRequest events 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 LocalServerAddress to 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.

Additional Resources