csharp-sdk/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs
Stephen Halter 2ab9db0878
Refactor transports to help enable graceful shutdown (#142)
- This also paves the way for better multi-session support
- We should definitely rethink names for the transport API
- For now, I kept the names similar as possible, so we can
  focus on the API shape

Co-authored-by: Stephen Toub <stoub@microsoft.com>
2025-03-30 17:10:14 -07:00

62 lines
2.5 KiB
C#

using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Protocol.Transport;
namespace AspNetCoreSseServer;
public static class McpEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints)
{
SseResponseStreamTransport? transport = null;
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
var routeGroup = endpoints.MapGroup("");
routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) =>
{
response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache";
await using var localTransport = transport = new SseResponseStreamTransport(response.Body);
await using var server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
try
{
var transportTask = transport.RunAsync(cancellationToken: requestAborted);
await server.RunAsync(cancellationToken: requestAborted);
await transportTask;
}
catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
{
// RequestAborted always triggers when the client disconnects before a complete response body is written,
// but this is how SSE connections are typically closed.
}
});
routeGroup.MapPost("/message", async context =>
{
if (transport is null)
{
await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context);
return;
}
var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(McpJsonUtilities.DefaultOptions, context.RequestAborted);
if (message is null)
{
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
return;
}
await transport.OnMessageReceivedAsync(message, context.RequestAborted);
context.Response.StatusCode = StatusCodes.Status202Accepted;
await context.Response.WriteAsync("Accepted");
});
return routeGroup;
}
}