Skip to content
This repository was archived by the owner on Mar 21, 2025. It is now read-only.

Add AspNetCoreSseServer sample #81

Merged
merged 5 commits into from
Mar 18, 2025
Merged
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Cake tools
[Tt]ools/
/[Tt]ools/

# Build output
[Bb]uildArtifacts/
Expand Down
7 changes: 7 additions & 0 deletions mcpdotnet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2A77AF5C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet.TestSseServer", "tests\mcpdotnet.TestSseServer\mcpdotnet.TestSseServer.csproj", "{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreSseServer", "samples\AspNetCoreSseServer\AspNetCoreSseServer.csproj", "{B6F42305-423F-56FF-090F-B7263547F924}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -61,6 +63,10 @@ Global
{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}.Release|Any CPU.Build.0 = Release|Any CPU
{B6F42305-423F-56FF-090F-B7263547F924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6F42305-423F-56FF-090F-B7263547F924}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6F42305-423F-56FF-090F-B7263547F924}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6F42305-423F-56FF-090F-B7263547F924}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -71,6 +77,7 @@ Global
{CA0BB450-1903-2F92-A68D-1285976551D6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{7C229573-A085-4ECC-8131-958CDA2BE731} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{6499876E-2F76-44A8-B6EB-5B889C6E9B7F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{B6F42305-423F-56FF-090F-B7263547F924} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
Expand Down
17 changes: 17 additions & 0 deletions samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

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

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

<ItemGroup>
<PackageReference Include="System.Net.ServerSentEvents" Version="10.0.0-preview.1.25080.5" />
</ItemGroup>

</Project>
62 changes: 62 additions & 0 deletions samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Server;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Options;

namespace AspNetCoreSseServer;

public static class McpEndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints)
{
IMcpServer? server = null;
SseServerStreamTransport? 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) =>
{
await using var localTransport = transport = new SseServerStreamTransport(response.Body);
await using var localServer = server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);

await localServer.StartAsync(requestAborted);

response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache";

try
{
await transport.RunAsync(requestAborted);
}
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 (HttpContext 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>(JsonSerializerOptionsExtensions.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;
}
}
11 changes: 11 additions & 0 deletions samples/AspNetCoreSseServer/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using McpDotNet;
using AspNetCoreSseServer;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer().WithTools();
var app = builder.Build();

app.MapGet("/", () => "Hello World!");
app.MapMcpSse();

app.Run();
23 changes: 23 additions & 0 deletions samples/AspNetCoreSseServer/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7133;http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
84 changes: 84 additions & 0 deletions samples/AspNetCoreSseServer/SseServerStreamTransport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Buffers;
using System.Net.ServerSentEvents;
using System.Text.Json;
using System.Threading.Channels;
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Transport;
using McpDotNet.Utils.Json;

namespace AspNetCoreSseServer;

public class SseServerStreamTransport(Stream sseResponseStream) : ITransport
{
private readonly Channel<IJsonRpcMessage> _incomingChannel = CreateSingleItemChannel<IJsonRpcMessage>();
private readonly Channel<SseItem<IJsonRpcMessage?>> _outgoingSseChannel = CreateSingleItemChannel<SseItem<IJsonRpcMessage?>>();

private Task? _sseWriteTask;
private Utf8JsonWriter? _jsonWriter;

public bool IsConnected => _sseWriteTask?.IsCompleted == false;

public Task RunAsync(CancellationToken cancellationToken)
{
void WriteJsonRpcMessageToBuffer(SseItem<IJsonRpcMessage?> item, IBufferWriter<byte> writer)
{
if (item.EventType == "endpoint")
{
writer.Write("/message"u8);
return;
}

JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, JsonSerializerOptionsExtensions.DefaultOptions);
}

// The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type,
// so we fib and special-case the "endpoint" event type in the formatter.
_outgoingSseChannel.Writer.TryWrite(new SseItem<IJsonRpcMessage?>(null, "endpoint"));

var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
}

public ChannelReader<IJsonRpcMessage> MessageReader => _incomingChannel.Reader;

public ValueTask DisposeAsync()
{
_incomingChannel.Writer.TryComplete();
_outgoingSseChannel.Writer.TryComplete();
return new ValueTask(_sseWriteTask ?? Task.CompletedTask);
}

public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) =>
_outgoingSseChannel.Writer.WriteAsync(new SseItem<IJsonRpcMessage?>(message), cancellationToken).AsTask();

public Task OnMessageReceivedAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
}

return _incomingChannel.Writer.WriteAsync(message, cancellationToken).AsTask();
}

private static Channel<T> CreateSingleItemChannel<T>() =>
Channel.CreateBounded<T>(new BoundedChannelOptions(1)
{
SingleReader = true,
SingleWriter = false,
});

private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter<byte> writer)
{
if (_jsonWriter is null)
{
_jsonWriter = new Utf8JsonWriter(writer);
}
else
{
_jsonWriter.Reset(writer);
}

return _jsonWriter;
}
}
14 changes: 14 additions & 0 deletions samples/AspNetCoreSseServer/Tools/EchoTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using McpDotNet.Server;
using System.ComponentModel;

namespace TestServerWithHosting.Tools;

[McpToolType]
public static class EchoTool
{
[McpTool, Description("Echoes the input back to the client.")]
public static string Echo(string message)
{
return "hello " + message;
}
}
51 changes: 51 additions & 0 deletions samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using System.ComponentModel;

namespace TestServerWithHosting.Tools;

/// <summary>
/// This tool uses depenency injection and async method
/// </summary>
[McpToolType]
public class SampleLlmTool
{
private readonly IMcpServer _server;

public SampleLlmTool(IMcpServer server)
{
_server = server ?? throw new ArgumentNullException(nameof(server));
}

[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
public async Task<string> SampleLLM(
[Description("The prompt to send to the LLM")] string prompt,
[Description("Maximum number of tokens to generate")] int maxTokens,
CancellationToken cancellationToken)
{
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken);

return $"LLM sampling result: {sampleResult.Content.Text}";
}

private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
{
return new CreateMessageRequestParams()
{
Messages = [new SamplingMessage()
{
Role = Role.User,
Content = new Content()
{
Type = "text",
Text = $"Resource {uri} context: {context}"
}
}],
SystemPrompt = "You are a helpful test server.",
MaxTokens = maxTokens,
Temperature = 0.7f,
IncludeContext = ContextInclusion.ThisServer
};
}
}
8 changes: 8 additions & 0 deletions samples/AspNetCoreSseServer/appsettings.Development.json
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/AspNetCoreSseServer/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
5 changes: 2 additions & 3 deletions samples/TestServerWithHosting/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.Debug()
.WriteTo.Console()
.WriteTo.Console(standardErrorFromLevel: Serilog.Events.LogEventLevel.Verbose)
.CreateLogger();

try
Expand All @@ -19,8 +19,7 @@
builder.Services.AddSerilog();
builder.Services.AddMcpServer()
.WithStdioServerTransport()
.WithTools()
.WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse()));
.WithTools();

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using McpDotNet.Configuration;
using McpDotNet.Hosting;
using McpDotNet.Protocol.Transport;
using McpDotNet.Utils;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -19,6 +20,7 @@ public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder
Throw.IfNull(builder);

builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
builder.Services.AddHostedService<McpServerHostedService>();
return builder;
}

Expand All @@ -34,6 +36,7 @@ public static IMcpServerBuilder WithHttpListenerSseServerTransport(this IMcpServ
}

builder.Services.AddSingleton<IServerTransport, HttpListenerSseServerTransport>();
builder.Services.AddHostedService<McpServerHostedService>();
return builder;
}
}
26 changes: 26 additions & 0 deletions src/mcpdotnet/Configuration/McpServerOptionsSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Reflection;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.Options;

namespace McpDotNet.Configuration;

internal sealed class McpServerOptionsSetup(IOptions<McpServerHandlers> serverHandlers) : IConfigureOptions<McpServerOptions>
{
public void Configure(McpServerOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

var assemblyName = Assembly.GetEntryAssembly()?.GetName();
options.ServerInfo = new Implementation
{
Name = assemblyName?.Name ?? "McpServer",
Version = assemblyName?.Version?.ToString() ?? "1.0.0",
};

serverHandlers.Value.OverwriteWithSetHandlers(options);
}
}
Loading
Loading