Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Project Path="docs/concepts/progress/samples/server/Progress.csproj" />
</Folder>
<Folder Name="/samples/">
<Project Path="samples/AspNetCoreMcpControllerServer/AspNetCoreMcpControllerServer.csproj" />
<Project Path="samples/AspNetCoreMcpPerSessionTools/AspNetCoreMcpPerSessionTools.csproj" />
<Project Path="samples/AspNetCoreMcpServer/AspNetCoreMcpServer.csproj" />
<Project Path="samples/ChatWithTools/ChatWithTools.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
</ItemGroup>

</Project>
22 changes: 22 additions & 0 deletions samples/AspNetCoreMcpControllerServer/Controllers/McpController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ModelContextProtocol.AspNetCore;

namespace AspNetCoreMcpControllerServer.Controllers;

/// <summary>
/// An MVC controller that handles MCP Streamable HTTP transport requests
/// by delegating to a <see cref="RequestDelegate"/> created by
/// <see cref="McpRequestDelegateFactory"/>.
/// </summary>
[ApiController]
[Route("mcp")]
public class McpController : ControllerBase
{
private static readonly RequestDelegate _mcpHandler = McpRequestDelegateFactory.Create();

[HttpPost]
[HttpGet]
[HttpDelete]
public Task Handle() => _mcpHandler(HttpContext);
}
13 changes: 13 additions & 0 deletions samples/AspNetCoreMcpControllerServer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using AspNetCoreMcpControllerServer.Tools;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<EchoTool>();

var app = builder.Build();

app.MapControllers();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
14 changes: 14 additions & 0 deletions samples/AspNetCoreMcpControllerServer/Tools/EchoTool.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/AspNetCoreMcpControllerServer/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
72 changes: 72 additions & 0 deletions src/ModelContextProtocol.AspNetCore/McpRequestDelegateFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace ModelContextProtocol.AspNetCore;

/// <summary>
/// Provides a method to create a <see cref="RequestDelegate"/> that handles MCP Streamable HTTP
/// transport requests for an ASP.NET Core server.
/// </summary>
/// <remarks>
/// <para>
/// This factory creates a <see cref="RequestDelegate"/> that routes MCP requests
/// based on the HTTP method (POST, GET, DELETE) of the incoming request.
/// The required services must be registered by calling <c>WithHttpTransport()</c>
/// during application startup.
/// </para>
/// <para>
/// This is useful for integrating MCP into applications that use traditional MVC controllers
/// or other request-handling patterns instead of minimal APIs:
/// </para>
/// <code>
/// [ApiController]
/// [Route("mcp")]
/// public class McpController : ControllerBase
/// {
/// private static readonly RequestDelegate _mcpHandler = McpRequestDelegateFactory.Create();
///
/// [HttpPost]
/// [HttpGet]
/// [HttpDelete]
/// public Task Handle() =&gt; _mcpHandler(HttpContext);
/// }
/// </code>
/// </remarks>
public static class McpRequestDelegateFactory
{
/// <summary>
/// Creates a <see cref="RequestDelegate"/> that handles MCP Streamable HTTP transport requests.
/// </summary>
/// <returns>
/// A <see cref="RequestDelegate"/> 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.
/// </returns>
/// <remarks>
/// The returned delegate resolves the internal MCP handler from <see cref="HttpContext.RequestServices"/>
/// on each invocation. The required services are registered by
/// calling <c>WithHttpTransport()</c> during application startup.
/// </remarks>
public static RequestDelegate Create()
{
return context =>
{
var handler = context.RequestServices.GetRequiredService<StreamableHttpHandler>();

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;
}
}
38 changes: 38 additions & 0 deletions src/ModelContextProtocol.AspNetCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
```
117 changes: 117 additions & 0 deletions tests/ModelContextProtocol.AspNetCore.Tests/McpControllerTests.cs
Original file line number Diff line number Diff line change
@@ -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<McpClient> 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<ControllerTestTools>();

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<ControllerTestTools>();

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<string, object?> { ["message"] = "Hello from controller!" },
cancellationToken: TestContext.Current.CancellationToken);

var textContent = Assert.Single(result.Content.OfType<TextContentBlock>());
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<ControllerTestTools>();

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);
}