Convert EverythingServer to use Streamable HTTP (#709)
* Convert EverythingServer to use Streamable HTTP * Apply suggestion from @halter73 Co-authored-by: Stephen Halter <halter73@gmail.com> --------- Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
parent
a918960a4d
commit
fb07d666a6
1 changed files with 177 additions and 37 deletions
|
|
@ -3,9 +3,6 @@ using EverythingServer.Prompts;
|
|||
using EverythingServer.Resources;
|
||||
using EverythingServer.Tools;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Protocol;
|
||||
using ModelContextProtocol.Server;
|
||||
|
|
@ -14,20 +11,46 @@ using OpenTelemetry.Logs;
|
|||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Logging.AddConsole(consoleLogOptions =>
|
||||
{
|
||||
// Configure all logs to go to stderr
|
||||
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
|
||||
});
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
HashSet<string> subscriptions = [];
|
||||
var _minimumLoggingLevel = LoggingLevel.Debug;
|
||||
// Dictionary of session IDs to a set of resource URIs they are subscribed to
|
||||
// The value is a ConcurrentDictionary used as a thread-safe HashSet
|
||||
// because .NET does not have a built-in concurrent HashSet
|
||||
ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> subscriptions = new();
|
||||
|
||||
builder.Services
|
||||
.AddMcpServer()
|
||||
.WithStdioServerTransport()
|
||||
.WithHttpTransport(options =>
|
||||
{
|
||||
// Add a RunSessionHandler to remove all subscriptions for the session when it ends
|
||||
options.RunSessionHandler = async (httpContext, mcpServer, token) =>
|
||||
{
|
||||
if (mcpServer.SessionId == null)
|
||||
{
|
||||
// There is no sessionId if the serverOptions.Stateless is true
|
||||
await mcpServer.RunAsync(token);
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
subscriptions[mcpServer.SessionId] = new ConcurrentDictionary<string, byte>();
|
||||
// Start an instance of SubscriptionMessageSender for this session
|
||||
using var subscriptionSender = new SubscriptionMessageSender(mcpServer, subscriptions[mcpServer.SessionId]);
|
||||
await subscriptionSender.StartAsync(token);
|
||||
// Start an instance of LoggingUpdateMessageSender for this session
|
||||
using var loggingSender = new LoggingUpdateMessageSender(mcpServer);
|
||||
await loggingSender.StartAsync(token);
|
||||
await mcpServer.RunAsync(token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// This code runs when the session ends
|
||||
subscriptions.TryRemove(mcpServer.SessionId, out _);
|
||||
}
|
||||
};
|
||||
})
|
||||
.WithTools<AddTool>()
|
||||
.WithTools<AnnotatedMessageTool>()
|
||||
.WithTools<EchoTool>()
|
||||
|
|
@ -40,11 +63,13 @@ builder.Services
|
|||
.WithResources<SimpleResourceType>()
|
||||
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
|
||||
{
|
||||
var uri = ctx.Params?.Uri;
|
||||
|
||||
if (uri is not null)
|
||||
if (ctx.Server.SessionId == null)
|
||||
{
|
||||
subscriptions.Add(uri);
|
||||
throw new McpException("Cannot add subscription for server with null SessionId");
|
||||
}
|
||||
if (ctx.Params?.Uri is { } uri)
|
||||
{
|
||||
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
|
||||
|
||||
await ctx.Server.SampleAsync([
|
||||
new ChatMessage(ChatRole.System, "You are a helpful test server"),
|
||||
|
|
@ -62,10 +87,13 @@ builder.Services
|
|||
})
|
||||
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
|
||||
{
|
||||
var uri = ctx.Params?.Uri;
|
||||
if (uri is not null)
|
||||
if (ctx.Server.SessionId == null)
|
||||
{
|
||||
subscriptions.Remove(uri);
|
||||
throw new McpException("Cannot remove subscription for server with null SessionId");
|
||||
}
|
||||
if (ctx.Params?.Uri is { } uri)
|
||||
{
|
||||
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
|
||||
}
|
||||
return new EmptyResult();
|
||||
})
|
||||
|
|
@ -126,13 +154,13 @@ builder.Services
|
|||
throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams);
|
||||
}
|
||||
|
||||
_minimumLoggingLevel = ctx.Params.Level;
|
||||
// The SDK updates the LoggingLevel field of the IMcpServer
|
||||
|
||||
await ctx.Server.SendNotificationAsync("notifications/message", new
|
||||
{
|
||||
Level = "debug",
|
||||
Logger = "test-server",
|
||||
Data = $"Logging level set to {_minimumLoggingLevel}",
|
||||
Data = $"Logging level set to {ctx.Params.Level}",
|
||||
}, cancellationToken: ct);
|
||||
|
||||
return new EmptyResult();
|
||||
|
|
@ -145,10 +173,8 @@ builder.Services.AddOpenTelemetry()
|
|||
.WithLogging(b => b.SetResourceBuilder(resource))
|
||||
.UseOtlpExporter();
|
||||
|
||||
builder.Services.AddSingleton(subscriptions);
|
||||
builder.Services.AddHostedService<SubscriptionMessageSender>();
|
||||
builder.Services.AddHostedService<LoggingUpdateMessageSender>();
|
||||
var app = builder.Build();
|
||||
|
||||
builder.Services.AddSingleton<Func<LoggingLevel>>(_ => () => _minimumLoggingLevel);
|
||||
app.MapMcp();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
app.Run();
|
||||
|
|
|
|||
Loading…
Reference in a new issue