diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index 1090c5377..2c10afffd 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -39,6 +39,7 @@
+
diff --git a/samples/AspNetCoreMcpControllerServer/AspNetCoreMcpControllerServer.csproj b/samples/AspNetCoreMcpControllerServer/AspNetCoreMcpControllerServer.csproj
new file mode 100644
index 000000000..755bb5d81
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/AspNetCoreMcpControllerServer.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/samples/AspNetCoreMcpControllerServer/Controllers/McpController.cs b/samples/AspNetCoreMcpControllerServer/Controllers/McpController.cs
new file mode 100644
index 000000000..0870274ba
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/Controllers/McpController.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using ModelContextProtocol.AspNetCore;
+
+namespace AspNetCoreMcpControllerServer.Controllers;
+
+///
+/// An MVC controller that handles MCP Streamable HTTP transport requests
+/// by delegating to a created by
+/// .
+///
+[ApiController]
+[Route("mcp")]
+public class McpController : ControllerBase
+{
+ private static readonly RequestDelegate _mcpHandler = McpRequestDelegateFactory.Create();
+
+ [HttpPost]
+ [HttpGet]
+ [HttpDelete]
+ public Task Handle() => _mcpHandler(HttpContext);
+}
diff --git a/samples/AspNetCoreMcpControllerServer/Program.cs b/samples/AspNetCoreMcpControllerServer/Program.cs
new file mode 100644
index 000000000..4c3b38e86
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/Program.cs
@@ -0,0 +1,13 @@
+using AspNetCoreMcpControllerServer.Tools;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddControllers();
+builder.Services.AddMcpServer()
+ .WithHttpTransport()
+ .WithTools();
+
+var app = builder.Build();
+
+app.MapControllers();
+
+app.Run();
diff --git a/samples/AspNetCoreMcpControllerServer/Properties/launchSettings.json b/samples/AspNetCoreMcpControllerServer/Properties/launchSettings.json
new file mode 100644
index 000000000..ca25bdc6d
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:3001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/AspNetCoreMcpControllerServer/Tools/EchoTool.cs b/samples/AspNetCoreMcpControllerServer/Tools/EchoTool.cs
new file mode 100644
index 000000000..dd211c0a2
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/Tools/EchoTool.cs
@@ -0,0 +1,14 @@
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace AspNetCoreMcpControllerServer.Tools;
+
+[McpServerToolType]
+public sealed class EchoTool
+{
+ [McpServerTool, Description("Echoes the input back to the client.")]
+ public static string Echo(string message)
+ {
+ return "hello " + message;
+ }
+}
diff --git a/samples/AspNetCoreMcpControllerServer/appsettings.Development.json b/samples/AspNetCoreMcpControllerServer/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/AspNetCoreMcpControllerServer/appsettings.json b/samples/AspNetCoreMcpControllerServer/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/samples/AspNetCoreMcpControllerServer/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/ModelContextProtocol.AspNetCore/McpRequestDelegateFactory.cs b/src/ModelContextProtocol.AspNetCore/McpRequestDelegateFactory.cs
new file mode 100644
index 000000000..8429a9e9b
--- /dev/null
+++ b/src/ModelContextProtocol.AspNetCore/McpRequestDelegateFactory.cs
@@ -0,0 +1,72 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ModelContextProtocol.AspNetCore;
+
+///
+/// Provides a method to create a that handles MCP Streamable HTTP
+/// transport requests for an ASP.NET Core server.
+///
+///
+///
+/// This factory creates a that routes MCP requests
+/// based on the HTTP method (POST, GET, DELETE) of the incoming request.
+/// The required services must be registered by calling WithHttpTransport()
+/// during application startup.
+///
+///
+/// This is useful for integrating MCP into applications that use traditional MVC controllers
+/// or other request-handling patterns instead of minimal APIs:
+///
+///
+/// [ApiController]
+/// [Route("mcp")]
+/// public class McpController : ControllerBase
+/// {
+/// private static readonly RequestDelegate _mcpHandler = McpRequestDelegateFactory.Create();
+///
+/// [HttpPost]
+/// [HttpGet]
+/// [HttpDelete]
+/// public Task Handle() => _mcpHandler(HttpContext);
+/// }
+///
+///
+public static class McpRequestDelegateFactory
+{
+ ///
+ /// Creates a that handles MCP Streamable HTTP transport requests.
+ ///
+ ///
+ /// A that routes incoming requests to the appropriate MCP handler
+ /// based on the HTTP method. POST requests handle JSON-RPC messages, GET requests handle
+ /// SSE streams for server-to-client messages, and DELETE requests terminate sessions.
+ /// Unsupported HTTP methods receive a 405 Method Not Allowed response.
+ ///
+ ///
+ /// The returned delegate resolves the internal MCP handler from
+ /// on each invocation. The required services are registered by
+ /// calling WithHttpTransport() during application startup.
+ ///
+ public static RequestDelegate Create()
+ {
+ return context =>
+ {
+ var handler = context.RequestServices.GetRequiredService();
+
+ return context.Request.Method switch
+ {
+ "POST" => handler.HandlePostRequestAsync(context),
+ "GET" => handler.HandleGetRequestAsync(context),
+ "DELETE" => handler.HandleDeleteRequestAsync(context),
+ _ => HandleMethodNotAllowedAsync(context),
+ };
+ };
+ }
+
+ private static Task HandleMethodNotAllowedAsync(HttpContext context)
+ {
+ context.Response.StatusCode = StatusCodes.Status405MethodNotAllowed;
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/ModelContextProtocol.AspNetCore/README.md b/src/ModelContextProtocol.AspNetCore/README.md
index f27c76cec..7f5492dad 100644
--- a/src/ModelContextProtocol.AspNetCore/README.md
+++ b/src/ModelContextProtocol.AspNetCore/README.md
@@ -50,3 +50,41 @@ public static class EchoTool
public static string Echo(string message) => $"hello {message}";
}
```
+
+## Using with MVC Controllers
+
+If your application uses traditional MVC controllers instead of minimal APIs,
+you can use `McpRequestDelegateFactory.Create()` to create a `RequestDelegate` that handles MCP requests:
+
+```csharp
+// Program.cs
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddControllers();
+builder.Services.AddMcpServer()
+ .WithHttpTransport()
+ .WithToolsFromAssembly();
+var app = builder.Build();
+
+app.MapControllers(); // No MapMcp() needed!
+
+app.Run("http://localhost:3001");
+```
+
+```csharp
+// Controllers/McpController.cs
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using ModelContextProtocol.AspNetCore;
+
+[ApiController]
+[Route("mcp")]
+public class McpController : ControllerBase
+{
+ private static readonly RequestDelegate _mcpHandler = McpRequestDelegateFactory.Create();
+
+ [HttpPost]
+ [HttpGet]
+ [HttpDelete]
+ public Task Handle() => _mcpHandler(HttpContext);
+}
+```
diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/McpControllerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/McpControllerTests.cs
new file mode 100644
index 000000000..ba1f3dedc
--- /dev/null
+++ b/tests/ModelContextProtocol.AspNetCore.Tests/McpControllerTests.cs
@@ -0,0 +1,117 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.AspNetCore.Tests.Utils;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+namespace ModelContextProtocol.AspNetCore.Tests;
+
+public class McpControllerTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
+{
+ private async Task ConnectAsync()
+ {
+ await using var transport = new HttpClientTransport(new HttpClientTransportOptions
+ {
+ Endpoint = new Uri("http://localhost:5000/mcp"),
+ TransportMode = HttpTransportMode.StreamableHttp,
+ }, HttpClient, LoggerFactory);
+
+ return await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
+ }
+
+ [Fact]
+ public async Task CanConnect_WithMcpClient_ViaController()
+ {
+ Builder.Services.AddControllers()
+ .AddApplicationPart(typeof(TestMcpController).Assembly);
+ Builder.Services.AddMcpServer(options =>
+ {
+ options.ServerInfo = new()
+ {
+ Name = "ControllerTestServer",
+ Version = "1.0.0",
+ };
+ }).WithHttpTransport().WithTools();
+
+ await using var app = Builder.Build();
+
+ app.MapControllers();
+
+ await app.StartAsync(TestContext.Current.CancellationToken);
+
+ await using var mcpClient = await ConnectAsync();
+
+ Assert.Equal("ControllerTestServer", mcpClient.ServerInfo.Name);
+ }
+
+ [Fact]
+ public async Task CanCallTool_ViaController()
+ {
+ Builder.Services.AddControllers()
+ .AddApplicationPart(typeof(TestMcpController).Assembly);
+ Builder.Services.AddMcpServer()
+ .WithHttpTransport()
+ .WithTools();
+
+ await using var app = Builder.Build();
+
+ app.MapControllers();
+
+ await app.StartAsync(TestContext.Current.CancellationToken);
+
+ await using var mcpClient = await ConnectAsync();
+
+ var result = await mcpClient.CallToolAsync(
+ "echo",
+ new Dictionary { ["message"] = "Hello from controller!" },
+ cancellationToken: TestContext.Current.CancellationToken);
+
+ var textContent = Assert.Single(result.Content.OfType());
+ Assert.Equal("hello Hello from controller!", textContent.Text);
+ }
+
+ [Fact]
+ public async Task CanListTools_ViaController()
+ {
+ Builder.Services.AddControllers()
+ .AddApplicationPart(typeof(TestMcpController).Assembly);
+ Builder.Services.AddMcpServer()
+ .WithHttpTransport()
+ .WithTools();
+
+ await using var app = Builder.Build();
+
+ app.MapControllers();
+
+ await app.StartAsync(TestContext.Current.CancellationToken);
+
+ await using var mcpClient = await ConnectAsync();
+
+ var tools = await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ Assert.Contains(tools, t => t.Name == "echo");
+ }
+}
+
+[McpServerToolType]
+internal sealed class ControllerTestTools
+{
+ [McpServerTool(Name = "echo"), Description("Echoes the input back to the client.")]
+ public static string Echo(string message) => "hello " + message;
+}
+
+[ApiController]
+[Route("mcp")]
+public class TestMcpController : ControllerBase
+{
+ private static readonly RequestDelegate _mcpHandler = McpRequestDelegateFactory.Create();
+
+ [HttpPost]
+ [HttpGet]
+ [HttpDelete]
+ public Task Handle() => _mcpHandler(HttpContext);
+}