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

Commit 90dfc35

Browse files
authored
Add AspNetCoreSseServer sample (#81)
* Add AspNetCoreSseMcpTransport sample * Fix build errors after rebase * React to McpServerFactory changes * Make Utf8JsonWriter non thread static * Fix test errors caused by not rebasing the HttpLister changes on factory overhaul commit
1 parent b5a26e4 commit 90dfc35

26 files changed

+635
-332
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Cake tools
2-
[Tt]ools/
2+
/[Tt]ools/
33

44
# Build output
55
[Bb]uildArtifacts/

mcpdotnet.sln

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2A77AF5C
2323
EndProject
2424
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet.TestSseServer", "tests\mcpdotnet.TestSseServer\mcpdotnet.TestSseServer.csproj", "{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}"
2525
EndProject
26+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreSseServer", "samples\AspNetCoreSseServer\AspNetCoreSseServer.csproj", "{B6F42305-423F-56FF-090F-B7263547F924}"
27+
EndProject
2628
Global
2729
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2830
Debug|Any CPU = Debug|Any CPU
@@ -61,6 +63,10 @@ Global
6163
{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}.Debug|Any CPU.Build.0 = Debug|Any CPU
6264
{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}.Release|Any CPU.ActiveCfg = Release|Any CPU
6365
{79B94BF9-E557-33DB-3F19-B2C7D9BF8C56}.Release|Any CPU.Build.0 = Release|Any CPU
66+
{B6F42305-423F-56FF-090F-B7263547F924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67+
{B6F42305-423F-56FF-090F-B7263547F924}.Debug|Any CPU.Build.0 = Debug|Any CPU
68+
{B6F42305-423F-56FF-090F-B7263547F924}.Release|Any CPU.ActiveCfg = Release|Any CPU
69+
{B6F42305-423F-56FF-090F-B7263547F924}.Release|Any CPU.Build.0 = Release|Any CPU
6470
EndGlobalSection
6571
GlobalSection(SolutionProperties) = preSolution
6672
HideSolutionNode = FALSE
@@ -71,6 +77,7 @@ Global
7177
{CA0BB450-1903-2F92-A68D-1285976551D6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
7278
{7C229573-A085-4ECC-8131-958CDA2BE731} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
7379
{6499876E-2F76-44A8-B6EB-5B889C6E9B7F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
80+
{B6F42305-423F-56FF-090F-B7263547F924} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
7481
EndGlobalSection
7582
GlobalSection(ExtensibilityGlobals) = postSolution
7683
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\mcpdotnet\mcpdotnet.csproj" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="System.Net.ServerSentEvents" Version="10.0.0-preview.1.25080.5" />
15+
</ItemGroup>
16+
17+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using McpDotNet.Protocol.Messages;
2+
using McpDotNet.Server;
3+
using McpDotNet.Utils.Json;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace AspNetCoreSseServer;
7+
8+
public static class McpEndpointRouteBuilderExtensions
9+
{
10+
public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints)
11+
{
12+
IMcpServer? server = null;
13+
SseServerStreamTransport? transport = null;
14+
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
15+
var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
16+
17+
var routeGroup = endpoints.MapGroup("");
18+
19+
routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) =>
20+
{
21+
await using var localTransport = transport = new SseServerStreamTransport(response.Body);
22+
await using var localServer = server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
23+
24+
await localServer.StartAsync(requestAborted);
25+
26+
response.Headers.ContentType = "text/event-stream";
27+
response.Headers.CacheControl = "no-cache";
28+
29+
try
30+
{
31+
await transport.RunAsync(requestAborted);
32+
}
33+
catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
34+
{
35+
// RequestAborted always triggers when the client disconnects before a complete response body is written,
36+
// but this is how SSE connections are typically closed.
37+
}
38+
});
39+
40+
routeGroup.MapPost("/message", async (HttpContext context) =>
41+
{
42+
if (transport is null)
43+
{
44+
await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context);
45+
return;
46+
}
47+
48+
var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(JsonSerializerOptionsExtensions.DefaultOptions, context.RequestAborted);
49+
if (message is null)
50+
{
51+
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
52+
return;
53+
}
54+
55+
await transport.OnMessageReceivedAsync(message, context.RequestAborted);
56+
context.Response.StatusCode = StatusCodes.Status202Accepted;
57+
await context.Response.WriteAsync("Accepted");
58+
});
59+
60+
return routeGroup;
61+
}
62+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using McpDotNet;
2+
using AspNetCoreSseServer;
3+
4+
var builder = WebApplication.CreateBuilder(args);
5+
builder.Services.AddMcpServer().WithTools();
6+
var app = builder.Build();
7+
8+
app.MapGet("/", () => "Hello World!");
9+
app.MapMcpSse();
10+
11+
app.Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"http": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "http://localhost:3001",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
},
13+
"https": {
14+
"commandName": "Project",
15+
"dotnetRunMessages": true,
16+
"launchBrowser": true,
17+
"applicationUrl": "https://localhost:7133;http://localhost:3001",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
}
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Buffers;
2+
using System.Net.ServerSentEvents;
3+
using System.Text.Json;
4+
using System.Threading.Channels;
5+
using McpDotNet.Protocol.Messages;
6+
using McpDotNet.Protocol.Transport;
7+
using McpDotNet.Utils.Json;
8+
9+
namespace AspNetCoreSseServer;
10+
11+
public class SseServerStreamTransport(Stream sseResponseStream) : ITransport
12+
{
13+
private readonly Channel<IJsonRpcMessage> _incomingChannel = CreateSingleItemChannel<IJsonRpcMessage>();
14+
private readonly Channel<SseItem<IJsonRpcMessage?>> _outgoingSseChannel = CreateSingleItemChannel<SseItem<IJsonRpcMessage?>>();
15+
16+
private Task? _sseWriteTask;
17+
private Utf8JsonWriter? _jsonWriter;
18+
19+
public bool IsConnected => _sseWriteTask?.IsCompleted == false;
20+
21+
public Task RunAsync(CancellationToken cancellationToken)
22+
{
23+
void WriteJsonRpcMessageToBuffer(SseItem<IJsonRpcMessage?> item, IBufferWriter<byte> writer)
24+
{
25+
if (item.EventType == "endpoint")
26+
{
27+
writer.Write("/message"u8);
28+
return;
29+
}
30+
31+
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, JsonSerializerOptionsExtensions.DefaultOptions);
32+
}
33+
34+
// The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type,
35+
// so we fib and special-case the "endpoint" event type in the formatter.
36+
_outgoingSseChannel.Writer.TryWrite(new SseItem<IJsonRpcMessage?>(null, "endpoint"));
37+
38+
var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
39+
return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
40+
}
41+
42+
public ChannelReader<IJsonRpcMessage> MessageReader => _incomingChannel.Reader;
43+
44+
public ValueTask DisposeAsync()
45+
{
46+
_incomingChannel.Writer.TryComplete();
47+
_outgoingSseChannel.Writer.TryComplete();
48+
return new ValueTask(_sseWriteTask ?? Task.CompletedTask);
49+
}
50+
51+
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) =>
52+
_outgoingSseChannel.Writer.WriteAsync(new SseItem<IJsonRpcMessage?>(message), cancellationToken).AsTask();
53+
54+
public Task OnMessageReceivedAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
55+
{
56+
if (!IsConnected)
57+
{
58+
throw new McpTransportException("Transport is not connected");
59+
}
60+
61+
return _incomingChannel.Writer.WriteAsync(message, cancellationToken).AsTask();
62+
}
63+
64+
private static Channel<T> CreateSingleItemChannel<T>() =>
65+
Channel.CreateBounded<T>(new BoundedChannelOptions(1)
66+
{
67+
SingleReader = true,
68+
SingleWriter = false,
69+
});
70+
71+
private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter<byte> writer)
72+
{
73+
if (_jsonWriter is null)
74+
{
75+
_jsonWriter = new Utf8JsonWriter(writer);
76+
}
77+
else
78+
{
79+
_jsonWriter.Reset(writer);
80+
}
81+
82+
return _jsonWriter;
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using McpDotNet.Server;
2+
using System.ComponentModel;
3+
4+
namespace TestServerWithHosting.Tools;
5+
6+
[McpToolType]
7+
public static class EchoTool
8+
{
9+
[McpTool, Description("Echoes the input back to the client.")]
10+
public static string Echo(string message)
11+
{
12+
return "hello " + message;
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using McpDotNet.Protocol.Types;
2+
using McpDotNet.Server;
3+
using System.ComponentModel;
4+
5+
namespace TestServerWithHosting.Tools;
6+
7+
/// <summary>
8+
/// This tool uses depenency injection and async method
9+
/// </summary>
10+
[McpToolType]
11+
public class SampleLlmTool
12+
{
13+
private readonly IMcpServer _server;
14+
15+
public SampleLlmTool(IMcpServer server)
16+
{
17+
_server = server ?? throw new ArgumentNullException(nameof(server));
18+
}
19+
20+
[McpTool("sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
21+
public async Task<string> SampleLLM(
22+
[Description("The prompt to send to the LLM")] string prompt,
23+
[Description("Maximum number of tokens to generate")] int maxTokens,
24+
CancellationToken cancellationToken)
25+
{
26+
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
27+
var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken);
28+
29+
return $"LLM sampling result: {sampleResult.Content.Text}";
30+
}
31+
32+
private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
33+
{
34+
return new CreateMessageRequestParams()
35+
{
36+
Messages = [new SamplingMessage()
37+
{
38+
Role = Role.User,
39+
Content = new Content()
40+
{
41+
Type = "text",
42+
Text = $"Resource {uri} context: {context}"
43+
}
44+
}],
45+
SystemPrompt = "You are a helpful test server.",
46+
MaxTokens = maxTokens,
47+
Temperature = 0.7f,
48+
IncludeContext = ContextInclusion.ThisServer
49+
};
50+
}
51+
}
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+
}
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+
}

samples/TestServerWithHosting/Program.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
rollingInterval: RollingInterval.Day,
99
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
1010
.WriteTo.Debug()
11-
.WriteTo.Console()
11+
.WriteTo.Console(standardErrorFromLevel: Serilog.Events.LogEventLevel.Verbose)
1212
.CreateLogger();
1313

1414
try
@@ -19,8 +19,7 @@
1919
builder.Services.AddSerilog();
2020
builder.Services.AddMcpServer()
2121
.WithStdioServerTransport()
22-
.WithTools()
23-
.WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse()));
22+
.WithTools();
2423

2524
var app = builder.Build();
2625

src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using McpDotNet.Configuration;
2+
using McpDotNet.Hosting;
23
using McpDotNet.Protocol.Transport;
34
using McpDotNet.Utils;
45
using Microsoft.Extensions.DependencyInjection;
@@ -19,6 +20,7 @@ public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder
1920
Throw.IfNull(builder);
2021

2122
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
23+
builder.Services.AddHostedService<McpServerHostedService>();
2224
return builder;
2325
}
2426

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

3638
builder.Services.AddSingleton<IServerTransport, HttpListenerSseServerTransport>();
39+
builder.Services.AddHostedService<McpServerHostedService>();
3740
return builder;
3841
}
3942
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Reflection;
2+
using McpDotNet.Protocol.Types;
3+
using McpDotNet.Server;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace McpDotNet.Configuration;
7+
8+
internal sealed class McpServerOptionsSetup(IOptions<McpServerHandlers> serverHandlers) : IConfigureOptions<McpServerOptions>
9+
{
10+
public void Configure(McpServerOptions options)
11+
{
12+
if (options is null)
13+
{
14+
throw new ArgumentNullException(nameof(options));
15+
}
16+
17+
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
18+
options.ServerInfo = new Implementation
19+
{
20+
Name = assemblyName?.Name ?? "McpServer",
21+
Version = assemblyName?.Version?.ToString() ?? "1.0.0",
22+
};
23+
24+
serverHandlers.Value.OverwriteWithSetHandlers(options);
25+
}
26+
}

0 commit comments

Comments
 (0)