using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.AspNetCore; using ModelContextProtocol.Protocol; using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.Builder; /// /// Provides extension methods for to add MCP endpoints. /// public static class McpEndpointRouteBuilderExtensions { /// /// Sets up endpoints for handling MCP Streamable HTTP transport. /// /// The web application to attach MCP HTTP endpoints. /// The route pattern prefix to map to. /// Returns a builder for configuring additional endpoint conventions like authorization policies. /// The required MCP services have not been registered. Ensure has been called during application startup. /// /// For details about the Streamable HTTP transport, see the 2025-11-25 protocol specification. /// When legacy SSE is enabled via , this method also maps legacy SSE endpoints at the path "/sse" and "/message". For details about the HTTP with SSE transport, see the 2024-11-05 protocol specification. /// public static IEndpointConventionBuilder MapMcp(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern = "") { var streamableHttpHandler = endpoints.ServiceProvider.GetService() ?? throw new InvalidOperationException("You must call WithHttpTransport(). Unable to find required services. Call builder.Services.AddMcpServer().WithHttpTransport() in application startup code."); var options = streamableHttpHandler.HttpServerTransportOptions; #pragma warning disable MCP9004 // EnableLegacySse - reading the obsolete property to check if SSE is enabled if (options.Stateless && options.EnableLegacySse) { throw new InvalidOperationException( "Legacy SSE endpoints cannot be enabled in stateless mode because SSE requires in-memory session state " + "shared between the GET /sse and POST /message requests. Remove the EnableLegacySse setting or disable stateless mode."); } #pragma warning restore MCP9004 var mcpGroup = endpoints.MapGroup(pattern); var streamableHttpGroup = mcpGroup.MapGroup("") .WithDisplayName(b => $"MCP Streamable HTTP | {b.DisplayName}") .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status404NotFound, typeof(JsonRpcError), contentTypes: ["application/json"])); streamableHttpGroup.MapPost("", streamableHttpHandler.HandlePostRequestAsync) .WithMetadata(new AcceptsMetadata(["application/json"])) .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, contentTypes: ["text/event-stream"])) .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); if (!options.Stateless) { // The GET endpoint is not mapped in Stateless mode since there's no way to send unsolicited messages. // Resuming streams via GET is currently not supported in Stateless mode. streamableHttpGroup.MapGet("", streamableHttpHandler.HandleGetRequestAsync) .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, contentTypes: ["text/event-stream"])); // The DELETE endpoint is not mapped in Stateless mode since there is no server-side state for the DELETE to clean up. streamableHttpGroup.MapDelete("", streamableHttpHandler.HandleDeleteRequestAsync); #pragma warning disable MCP9004 // EnableLegacySse - reading the obsolete property to check if SSE is enabled if (options.EnableLegacySse) #pragma warning restore MCP9004 { // Map legacy HTTP with SSE endpoints. These are disabled by default because the SSE transport // has no built-in request backpressure (POST returns 202 immediately). Enable only for trusted clients. var sseHandler = endpoints.ServiceProvider.GetRequiredService(); var sseGroup = mcpGroup.MapGroup("") .WithDisplayName(b => $"MCP HTTP with SSE | {b.DisplayName}"); sseGroup.MapGet("/sse", sseHandler.HandleSseRequestAsync) .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, contentTypes: ["text/event-stream"])); sseGroup.MapPost("/message", sseHandler.HandleMessageRequestAsync) .WithMetadata(new AcceptsMetadata(["application/json"])) .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted)); } } return mcpGroup; } }