- 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>
62 lines
2.5 KiB
C#
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;
|
|
}
|
|
}
|