BrandGhost
Hosting an MCP Server in ASP.NET Core: MapMcp, Stateless Mode, and Production Config

Hosting an MCP Server in ASP.NET Core: MapMcp, Stateless Mode, and Production Config

If you're ready to host an MCP server in ASP.NET Core, the interesting work is not just adding one NuGet package and calling MapMcp(). The real decision is how your ASP.NET Core MCP server behaves as an HTTP service: whether it is stateless, which hosts and browser origins can reach it, how tools are registered, and what production signals prove it is healthy.

This is an implementation guide. I'll assume Streamable HTTP is already the transport choice and focus on hosting it well in ASP.NET Core. Authentication and OAuth deserve their own treatment, so this article only flags where authorization policies plug into the endpoint. Same for Azure deployment. The goal here is to help you evaluate and implement the hosting shape before you wire it into a specific cloud environment.

As of writing, NuGet lists ModelContextProtocol.AspNetCore 1.4.0 as the latest stable package, with a 2.0.0-preview.1 also visible. Check NuGet before pinning. The code below was verified against the live MCP C# SDK README, the MCP C# SDK getting-started guide, the SDK transport docs, API docs for HttpServerTransportOptions, and the ASP.NET Core samples in the official repository.

Why ASP.NET Core Hosting Is Different From a Local Server

A local MCP server usually runs as a child process over stdio. That model is useful for developer tools because the host starts your process, talks over stdin and stdout, and shuts it down when done. Hosting an ASP.NET Core MCP server changes the operating model. Your server becomes an HTTP application with routes, middleware, host filtering, CORS, logs, metrics, health checks, deployment settings, and potentially multiple clients.

That shift is why ModelContextProtocol.AspNetCore exists. The package references the main ModelContextProtocol package and adds HTTP server hosting support for ASP.NET Core. In the current SDK docs, the hosting pattern is intentionally familiar: WebApplication.CreateBuilder(args), builder.Services.AddMcpServer(), .WithHttpTransport(...), and app.MapMcp().

If your MCP tools call downstream APIs, the same discipline from HttpClient in C#: The Complete Guide for .NET Developers still applies. If those calls need named clients, typed clients, or shared resilience configuration, IHttpClientFactory in .NET maps cleanly into the services your tools consume.

The practical tradeoff is this: ASP.NET Core gives you normal production hosting primitives, but it also exposes your MCP contract to normal web risks. DNS rebinding, broad CORS, missing health checks, noisy logs, unbounded tool execution, and accidental statefulness are all hosting concerns. A remote MCP endpoint should be treated like an API surface, not a demo console app.

Minimal ASP.NET Core MCP Server with MapMcp

Start with the package intended for HTTP servers:

dotnet new web -n MyRemoteMcpServer
cd MyRemoteMcpServer
dotnet add package ModelContextProtocol.AspNetCore --version 1.4.0

Then wire the server into Program.cs. This is the smallest useful shape for an ASP.NET Core MCP server using Streamable HTTP.

using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        // Prefer stateless unless tools need server-to-client calls or session state.
        options.Stateless = true;
    })
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp("/mcp");

app.Run("http://localhost:3001");

[McpServerToolType]
public static class TimeTools
{
    [McpServerTool]
    [Description("Returns the current UTC time in ISO 8601 format.")]
    public static string GetUtcTime() => DateTimeOffset.UtcNow.ToString("O");
}

MapMcp() maps the Streamable HTTP endpoint. The SDK API docs for MapMcp show the signature as MapMcp(this IEndpointRouteBuilder endpoints, string pattern = ""), so the route pattern is optional. I prefer mapping a dedicated /mcp path because it is obvious in logs, reverse proxy rules, and client configuration. A client should connect directly to that route, such as https://example.com/mcp.

The key thing to notice is where concerns belong. MCP registration happens in DI. HTTP routing happens on the app. Tool discovery is assembly-based in this example. For a small server, that is enough. For a production ASP.NET Core MCP server, you will almost certainly add CORS, host filtering, endpoint policies, downstream clients, logging, and health endpoints.

Stateless Mode for an MCP Server in ASP.NET Core

For most remote HTTP servers, stateless mode is the recommended default. In WithHttpTransport, set options.Stateless = true explicitly. The live SDK transport docs recommend explicit configuration because older versions and samples have moved through default changes, and explicit code makes your intent clear to future maintainers.

Stateless mode means the server does not track MCP session state across requests. There is no Mcp-Session-Id contract for the client to maintain, and every request is handled independently. That is usually what you want for a hosted MCP endpoint that exposes tools like search, lookup, transformation, validation, or downstream API calls.

The production benefits are concrete:

  • horizontal scaling is easier because you do not need sticky sessions;
  • memory pressure is lower because idle sessions are not retained;
  • deployments are simpler behind load balancers and reverse proxies;
  • clients that do not preserve session headers are less likely to break.

There are real tradeoffs, though. Stateless mode is not a magic setting you can apply to every server without thinking. The SDK docs call out that stateless mode disables session-dependent behavior. That includes server-to-client requests such as sampling, elicitation, and roots for clients that depend on session-based interaction. It also removes unsolicited server-to-client notifications and resource subscription behavior. If your server needs to push messages to a client outside a direct POST response, stateful mode may be the right choice.

For most hosting work, I frame the decision this way: if your tool takes input and returns output, start stateless. If your tool needs a remembered workspace, per-client state, resource subscriptions, or server-initiated calls back to the client, evaluate stateful mode deliberately. That tradeoff is similar to other web architecture decisions. State can be useful, but it brings affinity, cleanup, capacity planning, and failure-mode questions.

Here is the stateful shape when you intentionally need it:

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        // Use stateful mode only when session-dependent MCP features are required.
        options.Stateless = false;
    })
    .WithToolsFromAssembly();

Do not turn on legacy SSE just because you see the option. The live HttpServerTransportOptions API docs mark EnableLegacySse obsolete with diagnostic MCP9004, and the transport docs describe SSE as legacy. If you still have older clients, treat that as a migration concern, not the default hosting path for a new remote MCP server.

Production Host Filtering and DNS Rebinding Protection

An ASP.NET Core MCP server is an HTTP endpoint, so host header handling matters. The MCP C# SDK getting-started and transport docs specifically call out DNS rebinding protection for local HTTP servers. ASP.NET Core Kestrel does not validate Host headers by default. Microsoft Learn recommends Host Filtering Middleware through the AllowedHosts setting when Kestrel is public-facing or directly receives the forwarded host header.

For local development, keep the list narrow:

{
  "AllowedHosts": "localhost;127.0.0.1;[::1]"
}

For production, configure the exact public host names for your deployment rather than *:

{
  "AllowedHosts": "mcp.example.com;api.example.com"
}

This is not unique to MCP, but the risk is important for browser-accessible local servers. DNS rebinding tricks a browser into reaching a local service through an attacker-controlled host name. If your MCP server accepts any Host header, a browser can be used as a path into a local HTTP service that was never intended to trust that origin.

Allowed hosts are one layer. If your ASP.NET Core MCP server sits behind a reverse proxy or load balancer, validate host names at the layer that actually receives and forwards the request. Microsoft Learn also calls out ForwardedHeadersOptions.AllowedHosts for proxy scenarios where the original host header is not preserved.

CORS for Browser-Based MCP Clients

CORS is a browser control. It is not authentication, and it is not a replacement for host validation. Use it only when a browser-based MCP client intentionally calls your server from another origin.

For a stateless ASP.NET Core MCP server, the MCP transport docs say a narrow CORS policy usually needs the non-safelisted request headers used by browser clients: Content-Type, Authorization if the endpoint is protected, and MCP-Protocol-Version. If you enable stateful sessions or resumability, also allow Mcp-Session-Id and Last-Event-ID, and expose Mcp-Session-Id on responses so browser code can read it.

A restrictive policy looks like this:

var allowedOrigins = builder.Configuration
    .GetSection("Mcp:AllowedOrigins")
    .Get<string[]>() ?? [];

builder.Services.AddCors(options =>
{
    options.AddPolicy("McpBrowserClient", policy =>
    {
        policy.WithOrigins(allowedOrigins)
            // Stateless Streamable HTTP handles normal client messages through POST.
            .WithMethods("POST")
            .WithHeaders("Content-Type", "Authorization", "MCP-Protocol-Version");
    });
});

var app = builder.Build();

app.UseCors();
app.MapMcp("/mcp").RequireCors("McpBrowserClient");

Keep the allowlist boring. Use concrete origins like https://app.example.com, not wildcard origins. The official ASP.NET Core CORS docs are direct about this: CORS relaxes browser restrictions. It does not make an API safer. If your MCP server requires user identity or tenant isolation, that belongs in authentication and authorization policy design. That is a separate guide, and it should not be hand-waved into a CORS section.

Per-Session and Per-Request Tool Registration Patterns

The official repository has a per-session tools sample that uses ConfigureSessionOptions with stateful mode and route parameters to vary which tools are visible. That pattern is useful when the tool catalog really is tied to an MCP session. For example, a debugging session might expose workspace-specific tools that should persist for that connected client.

For a stateless hosted MCP endpoint, the same callback is still useful, but the SDK docs say it runs per HTTP request rather than once per session. That can be a better fit for production. You can resolve a tenant, feature flag, or route category for each request and expose only the relevant tools without storing MCP session state.

using ModelContextProtocol.Server;
using System.Collections.Concurrent;
using System.Reflection;

var toolCatalog = new ConcurrentDictionary<string, McpServerTool[]>(StringComparer.OrdinalIgnoreCase)
{
    ["public"] = GetToolsForType<PublicTools>(),
    ["admin"] = GetToolsForType<AdminTools>()
};

builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.Stateless = true;
        options.ConfigureSessionOptions = (httpContext, mcpOptions, cancellationToken) =>
        {
            var category = httpContext.Request.RouteValues["category"]?.ToString() ?? "public";
            var tools = toolCatalog.TryGetValue(category, out var selectedTools)
                ? selectedTools
                : toolCatalog["public"];

            mcpOptions.Capabilities = new();
            mcpOptions.Capabilities.Tools = new();
            mcpOptions.ToolCollection = [];

            foreach (var tool in tools)
            {
                mcpOptions.ToolCollection.Add(tool);
            }

            return Task.CompletedTask;
        };
    });

app.MapMcp("/mcp/{category?}");

static McpServerTool[] GetToolsForType<
    [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(
        System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    return typeof(T)
        .GetMethods(BindingFlags.Public | BindingFlags.Static)
        .Where(method => method.GetCustomAttributes(typeof(McpServerToolAttribute), false).Any())
        .Select(method => McpServerTool.Create(method, target: null, new McpServerToolCreateOptions()))
        .ToArray();
}

The important design rule is to avoid exposing every possible operation to every possible client by default. Tool catalogs are part of your contract. If an operation is destructive, high-cost, tenant-specific, or admin-only, shape the catalog intentionally and pair it with real authorization. If you need human approval patterns around tool execution, tool approval and human-in-the-loop workflows are worth thinking about before you expose actions broadly.

Binding to a Port and Configuration Shape

For local development, app.Run("http://localhost:3001") is fine because it makes the listening address obvious. In production, I prefer configuration over hardcoded addresses. Let hosting infrastructure set ASPNETCORE_URLS, --urls, environment variables, or Kestrel configuration.

A practical local launch profile or environment variable can point clients to the same stable route:

$env:ASPNETCORE_URLS = 'http://localhost:3001'
dotnet run

Your MCP client configuration then targets http://localhost:3001/mcp. Keep the route and binding separate in your mental model. MapMcp("/mcp") chooses where the MCP endpoint lives in the ASP.NET Core routing table. Kestrel binding chooses which address and port the app listens on.

If your server streams responses or holds requests open, review the same HTTP streaming considerations you would for any .NET service. The details in HttpClient Streaming in C# are useful context for client behavior, buffering, and long-running responses.

Health, Readiness, Logging, and Observability

MapMcp() gives you the MCP endpoint. It does not remove the need for ordinary production endpoints. At minimum, add liveness and readiness checks that your platform can call without speaking MCP.

builder.Services.AddHealthChecks();

var app = builder.Build();

app.MapHealthChecks("/healthz");
app.MapGet("/readyz", () => Results.Ok(new
{
    Status = "Ready",
    Service = "mcp-server",
    Utc = DateTimeOffset.UtcNow
}));

app.MapMcp("/mcp");

Keep readiness honest. If tools depend on a database, queue, search index, or downstream HTTP API, decide which dependencies must be checked before the server should receive traffic. Do not turn readiness into a massive integration test, but do not return green from an app that cannot perform its core tool calls.

Logging deserves the same care. Use structured logs, include route and tool metadata where safe, and avoid logging prompts, credentials, or sensitive tool arguments by default. If you are standardizing logging, Logging in .NET is directly relevant. For distributed traces and metrics, the official ASP.NET Core MCP sample includes OpenTelemetry setup, and OpenTelemetry and Observability in Microsoft Agent Framework is useful background for agent-adjacent systems.

Production Checklist for a Remote MCP Host

Before publishing an ASP.NET Core MCP server, I would evaluate these hosting checks:

  • ModelContextProtocol.AspNetCore is pinned to a stable version you verified on NuGet.
  • WithHttpTransport(options => options.Stateless = true) is explicit unless stateful behavior is required.
  • MapMcp("/mcp") uses a route that matches client configuration and proxy rules.
  • AllowedHosts is not * unless a trusted upstream layer validates the host before Kestrel receives it.
  • CORS is disabled unless browser clients need it, and the origin allowlist is narrow.
  • health and readiness endpoints exist outside the MCP protocol route.
  • logs and traces capture operational signals without storing sensitive tool inputs.
  • downstream HttpClient usage is registered through DI and configured for timeouts.
  • dangerous tools have authorization and approval boundaries, not just naming conventions.

That list is not meant to slow you down. It is meant to keep the hosting model boring. Boring is good here. The novelty should be in the tool capabilities you expose, not in whether your remote HTTP endpoint survives normal production traffic.

FAQ

What package do I need for ASP.NET Core MCP hosting?

Use ModelContextProtocol.AspNetCore for an HTTP-hosted ASP.NET Core MCP server. The SDK README and getting-started docs describe it as the package for HTTP-based MCP servers hosted in ASP.NET Core. It references the main ModelContextProtocol package, so you still get hosting, dependency injection, and attribute-based tool discovery.

What does MapMcp do in ASP.NET Core?

MapMcp() maps the MCP Streamable HTTP endpoint into ASP.NET Core routing. The live API docs show an optional route pattern parameter, so app.MapMcp() maps at the root by default and app.MapMcp("/mcp") maps under /mcp. The returned endpoint convention builder can be used with endpoint conventions such as CORS or authorization policies.

Should my ASP.NET Core MCP server use stateless mode?

Most HTTP-hosted MCP servers should start with stateless mode. It avoids session affinity, reduces in-memory session management, and fits request/response tools that call APIs, query data, or compute results. Use stateful mode when you need server-to-client requests, unsolicited notifications, resource subscriptions, or session-scoped state.

What breaks when stateless mode is enabled?

Stateless mode removes session-dependent behavior. The SDK docs call out that server-to-client requests, unsolicited notifications, resource subscriptions, legacy SSE endpoints, and per-client session state are not available in the normal stateless path. If your feature depends on those behaviors, choose stateful mode intentionally and plan for affinity, cleanup, and capacity.

Do I need CORS for ASP.NET Core MCP hosting?

Only enable CORS when a browser-based MCP client calls your server from another origin. Non-browser clients do not need CORS. If you enable it, use explicit origins and only the headers required by your client. CORS is not authentication and does not replace host filtering.

How should I configure AllowedHosts for a remote MCP server?

Use exact host names. For local development, that usually means localhost, 127.0.0.1, and [::1]. For production, use your real public host names. Avoid * unless you have a very specific reason and another layer is validating the host header before Kestrel receives it.

Where should authentication fit in this hosting setup?

Authentication belongs on the ASP.NET Core endpoint pipeline, typically through normal authentication and authorization middleware plus endpoint policies on MapMcp(). This article intentionally does not go deep on OAuth or identity flows because that design has enough nuance to deserve a separate implementation guide.

Final Thoughts on ASP.NET Core MCP Hosting

Hosting an MCP server in ASP.NET Core is straightforward at the API level: add ModelContextProtocol.AspNetCore, configure WithHttpTransport, map the endpoint with MapMcp, and run the app. The decision support comes from everything around that endpoint.

Choose stateless mode unless your capabilities need sessions. Lock down host names. Keep CORS narrow. Register tools intentionally. Add health checks and observability before production traffic depends on the service. That gives you an ASP.NET Core MCP server that behaves like a real HTTP service, not just a sample that happened to start on a port.

Building MCP Servers and Clients in C#: The Complete Guide

Learn how to build an MCP server C# developers can trust, from stdio and HTTP transports to tools, clients, security, Azure hosting, and debugging tips.

Build Your First MCP Server in C#: A Step-by-Step Quickstart

Build an MCP server in C# with a local stdio quickstart covering packages, Host setup, tool attributes, VS Code wiring, stderr logs, and common fixes.

MCP Transports in C#: stdio vs. Streamable HTTP vs. Legacy SSE

Compare MCP transports in C# across stdio, Streamable HTTP, stateless/stateful sessions, and legacy SSE migration so .NET teams choose safe .NET wiring.

An error has occurred. This application may no longer respond until reloaded. Reload