diff --git a/src/ModelContextProtocol.AspNetCore/DefaultMcpEndpointRouteBuilderConfigurator.cs b/src/ModelContextProtocol.AspNetCore/DefaultMcpEndpointRouteBuilderConfigurator.cs
new file mode 100644
index 00000000..df5e3a62
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/DefaultMcpEndpointRouteBuilderConfigurator.cs
@@ -0,0 +1,85 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol;
+
+namespace ModelContextProtocol.AspNetCore;
+
+///
+/// Default implementation that wires up the MCP Streamable HTTP transport endpoints
+/// (and legacy SSE endpoints when applicable).
+///
+internal sealed class DefaultMcpEndpointRouteBuilderConfigurator(
+ StreamableHttpHandler streamableHttpHandler
+) : IMcpEndpointRouteBuilderConfigurator
+{
+ public IEndpointConventionBuilder Configure(IEndpointRouteBuilder endpoints, string pattern)
+ {
+ 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 (!streamableHttpHandler.HttpServerTransportOptions.Stateless)
+ {
+ // The GET and DELETE endpoints are not mapped in Stateless mode since there's no way to send unsolicited messages
+ // for the GET to handle, and there is no server-side state for the DELETE to clean up.
+ streamableHttpGroup
+ .MapGet("", streamableHttpHandler.HandleGetRequestAsync)
+ .WithMetadata(
+ new ProducesResponseTypeMetadata(
+ StatusCodes.Status200OK,
+ contentTypes: ["text/event-stream"]
+ )
+ );
+ streamableHttpGroup.MapDelete("", streamableHttpHandler.HandleDeleteRequestAsync);
+
+ // Map legacy HTTP with SSE endpoints only if not in Stateless mode, because we cannot guarantee the /message requests
+ // will be handled by the same process as the /sse request.
+ 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;
+ }
+
+ IEndpointConventionBuilder IMcpEndpointRouteBuilderConfigurator.Configure(IEndpointRouteBuilder endpoints, string pattern)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs b/src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs
index 0cdc4e37..25e4db04 100644
--- a/src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs
+++ b/src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs
@@ -28,6 +28,11 @@ public static IMcpServerBuilder WithHttpTransport(this IMcpServerBuilder builder
builder.Services.AddHostedService();
builder.Services.AddDataProtection();
+ builder.Services.TryAddSingleton<
+ IMcpEndpointRouteBuilderConfigurator,
+ DefaultMcpEndpointRouteBuilderConfigurator
+ >();
+
if (configureOptions is not null)
{
builder.Services.Configure(configureOptions);
diff --git a/src/ModelContextProtocol.AspNetCore/IMcpEndpointRouteBuilderConfigurator.cs b/src/ModelContextProtocol.AspNetCore/IMcpEndpointRouteBuilderConfigurator.cs
new file mode 100644
index 00000000..ddb59deb
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/IMcpEndpointRouteBuilderConfigurator.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Routing;
+
+namespace ModelContextProtocol.AspNetCore;
+
+///
+/// Abstraction for configuring MCP endpoints on an .
+/// Register an implementation in DI to override the default MapMcp behavior and enable hot swapping
+/// of the transport/endpoints wiring without changing application code.
+///
+public interface IMcpEndpointRouteBuilderConfigurator
+{
+ ///
+ /// Configures the MCP endpoints on the provided using the given .
+ /// Implementations should return the representing the mapped group
+ /// to allow callers to apply additional endpoint conventions (e.g., authorization).
+ ///
+ /// The endpoint route builder to attach MCP endpoints to.
+ /// The route pattern prefix to map to.
+ /// An that can be used to configure conventions.
+ IEndpointConventionBuilder Configure(IEndpointRouteBuilder endpoints, string pattern);
+}
diff --git a/src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs b/src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs
index e7c5f97c..32813f38 100644
--- a/src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs
+++ b/src/ModelContextProtocol.AspNetCore/McpEndpointRouteBuilderExtensions.cs
@@ -1,9 +1,6 @@
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Metadata;
-using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.AspNetCore;
-using ModelContextProtocol.Protocol;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.AspNetCore.Builder;
@@ -16,47 +13,20 @@ public static class McpEndpointRouteBuilderExtensions
///
/// Sets up endpoints for handling MCP Streamable HTTP transport.
/// See the 2025-06-18 protocol specification for details about the Streamable HTTP transport.
- /// Also maps legacy SSE endpoints for backward compatibility at the path "/sse" and "/message". the 2024-11-05 protocol specification for details about the HTTP with SSE 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.
- public static IEndpointConventionBuilder MapMcp(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string pattern = "")
+ 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 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 (!streamableHttpHandler.HttpServerTransportOptions.Stateless)
- {
- // The GET and DELETE endpoints are not mapped in Stateless mode since there's no way to send unsolicited messages
- // for the GET to handle, and there is no server-side state for the DELETE to clean up.
- streamableHttpGroup.MapGet("", streamableHttpHandler.HandleGetRequestAsync)
- .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status200OK, contentTypes: ["text/event-stream"]));
- streamableHttpGroup.MapDelete("", streamableHttpHandler.HandleDeleteRequestAsync);
-
- // Map legacy HTTP with SSE endpoints only if not in Stateless mode, because we cannot guarantee the /message requests
- // will be handled by the same process as the /sse request.
- 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;
+ var configurator =
+ endpoints.ServiceProvider.GetService()
+ ?? throw new InvalidOperationException(
+ "You must call WithHttpTransport(). Unable to find required services. Call builder.Services.AddMcpServer().WithHttpTransport() in application startup code."
+ );
+ return configurator.Configure(endpoints, pattern);
}
}