Skip to content

Commit 6bedd92

Browse files
jtarquinoCopilot
andcommitted
feat: expose StreamableHttpHandler for MVC controller support
Make StreamableHttpHandler public so it can be injected into traditional ASP.NET Core MVC controllers, enabling MCP server scenarios without minimal APIs. Changes: - StreamableHttpHandler: internal -> public, constructor takes IServiceProvider - Updated WithHttpTransport() XML docs to mention controller injection - Added 'Using with MVC Controllers' section to AspNetCore README - New sample: AspNetCoreMcpControllerServer - New integration tests: McpControllerTests (connect, call tool, list tools) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f3da472 commit 6bedd92

13 files changed

Lines changed: 330 additions & 1 deletion

File tree

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<Project Path="docs/concepts/progress/samples/server/Progress.csproj" />
4040
</Folder>
4141
<Folder Name="/samples/">
42+
<Project Path="samples/AspNetCoreMcpControllerServer/AspNetCoreMcpControllerServer.csproj" />
4243
<Project Path="samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj" />
4344
<Project Path="samples/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj" />
4445
<Project Path="samples/ChatWithTools/ChatWithTools.csproj" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
11+
</ItemGroup>
12+
13+
</Project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using ModelContextProtocol.AspNetCore;
3+
4+
namespace AspNetCoreMcpControllerServer.Controllers;
5+
6+
/// <summary>
7+
/// An MVC controller that handles MCP Streamable HTTP transport requests
8+
/// by delegating to the <see cref="IStreamableHttpHandler"/> registered by
9+
/// <c>WithHttpTransport()</c>.
10+
/// </summary>
11+
[ApiController]
12+
[Route("mcp")]
13+
public class McpController : ControllerBase
14+
{
15+
[HttpPost]
16+
public Task Post([FromServices] IStreamableHttpHandler handler) =>
17+
handler.HandlePostRequestAsync(HttpContext);
18+
19+
[HttpGet]
20+
public Task Get([FromServices] IStreamableHttpHandler handler) =>
21+
handler.HandleGetRequestAsync(HttpContext);
22+
23+
[HttpDelete]
24+
public Task Delete([FromServices] IStreamableHttpHandler handler) =>
25+
handler.HandleDeleteRequestAsync(HttpContext);
26+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using AspNetCoreMcpControllerServer.Tools;
2+
3+
var builder = WebApplication.CreateBuilder(args);
4+
builder.Services.AddControllers();
5+
builder.Services.AddMcpServer()
6+
.WithHttpTransport()
7+
.WithTools<EchoTool>();
8+
9+
var app = builder.Build();
10+
11+
app.MapControllers();
12+
13+
app.Run();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"profiles": {
3+
"http": {
4+
"commandName": "Project",
5+
"dotnetRunMessages": true,
6+
"launchBrowser": false,
7+
"applicationUrl": "http://localhost:3001",
8+
"environmentVariables": {
9+
"ASPNETCORE_ENVIRONMENT": "Development"
10+
}
11+
}
12+
}
13+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using ModelContextProtocol.Server;
2+
using System.ComponentModel;
3+
4+
namespace AspNetCoreMcpControllerServer.Tools;
5+
6+
[McpServerToolType]
7+
public sealed class EchoTool
8+
{
9+
[McpServerTool, Description("Echoes the input back to the client.")]
10+
public static string Echo(string message)
11+
{
12+
return "hello " + message;
13+
}
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public static IMcpServerBuilder WithHttpTransport(this IMcpServerBuilder builder
2929

3030
builder.Services.TryAddSingleton<StatefulSessionManager>();
3131
builder.Services.TryAddSingleton<StreamableHttpHandler>();
32+
builder.Services.TryAddSingleton<IStreamableHttpHandler>(sp => sp.GetRequiredService<StreamableHttpHandler>());
3233
builder.Services.TryAddSingleton<SseHandler>();
3334
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
3435

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace ModelContextProtocol.AspNetCore;
4+
5+
/// <summary>
6+
/// Handles MCP Streamable HTTP transport requests (POST, GET, DELETE) for an ASP.NET Core server.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// This handler is registered as a singleton service by <c>WithHttpTransport()</c>
11+
/// and is used internally by <c>MapMcp()</c> to map MCP endpoints using minimal APIs.
12+
/// </para>
13+
/// <para>
14+
/// It can also be injected directly into MVC controllers or other request-handling code
15+
/// to support scenarios where minimal APIs are not used:
16+
/// </para>
17+
/// <code>
18+
/// [ApiController]
19+
/// [Route("mcp")]
20+
/// public class McpController : ControllerBase
21+
/// {
22+
/// [HttpPost]
23+
/// public Task Post([FromServices] IStreamableHttpHandler handler) =&gt; handler.HandlePostRequestAsync(HttpContext);
24+
///
25+
/// [HttpGet]
26+
/// public Task Get([FromServices] IStreamableHttpHandler handler) =&gt; handler.HandleGetRequestAsync(HttpContext);
27+
///
28+
/// [HttpDelete]
29+
/// public Task Delete([FromServices] IStreamableHttpHandler handler) =&gt; handler.HandleDeleteRequestAsync(HttpContext);
30+
/// }
31+
/// </code>
32+
/// </remarks>
33+
public interface IStreamableHttpHandler
34+
{
35+
/// <summary>
36+
/// Gets the configured <see cref="HttpServerTransportOptions"/> for the MCP server.
37+
/// </summary>
38+
HttpServerTransportOptions HttpServerTransportOptions { get; }
39+
40+
/// <summary>
41+
/// Handles an MCP Streamable HTTP POST request containing a JSON-RPC message.
42+
/// </summary>
43+
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
44+
/// <returns>A task that represents the asynchronous operation.</returns>
45+
/// <remarks>
46+
/// The response will be either a streamed SSE response containing JSON-RPC messages
47+
/// or a 202 Accepted response if the request contained only notifications.
48+
/// </remarks>
49+
Task HandlePostRequestAsync(HttpContext context);
50+
51+
/// <summary>
52+
/// Handles an MCP Streamable HTTP GET request for receiving unsolicited server-to-client messages via SSE.
53+
/// </summary>
54+
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
55+
/// <returns>A task that represents the asynchronous operation.</returns>
56+
/// <remarks>
57+
/// This endpoint keeps the connection open and streams SSE events to the client.
58+
/// It requires a valid <c>Mcp-Session-Id</c> header and is not available in stateless mode.
59+
/// </remarks>
60+
Task HandleGetRequestAsync(HttpContext context);
61+
62+
/// <summary>
63+
/// Handles an MCP Streamable HTTP DELETE request to terminate an existing session.
64+
/// </summary>
65+
/// <param name="context">The <see cref="HttpContext"/> for the current request.</param>
66+
/// <returns>A task that represents the asynchronous operation.</returns>
67+
Task HandleDeleteRequestAsync(HttpContext context);
68+
}

0 commit comments

Comments
 (0)