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
This commit is contained in:
Stephen Halter 2025-03-18 03:56:44 -07:00 • committed by GitHub
commit 90dfc35fbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 635 additions and 332 deletions

View file

@ -1,4 +1,5 @@
using McpDotNet.Configuration;
using McpDotNet.Hosting;
using McpDotNet.Protocol.Transport;
using McpDotNet.Utils;
using Microsoft.Extensions.DependencyInjection;
@ -19,6 +20,7 @@ public static partial class McpServerBuilderExtensions
Throw.IfNull(builder);
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
builder.Services.AddHostedService<McpServerHostedService>();
return builder;
}
@ -34,6 +36,7 @@ public static partial class McpServerBuilderExtensions
}
builder.Services.AddSingleton<IServerTransport, HttpListenerSseServerTransport>();
builder.Services.AddHostedService<McpServerHostedService>();
return builder;
}
}

View file

@ -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);
}
}

View file

@ -1,12 +1,9 @@
using McpDotNet.Configuration;
using McpDotNet.Hosting;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Reflection;
namespace McpDotNet;
@ -23,54 +20,22 @@ public static class McpServerServiceCollectionExtension
/// <returns></returns>
public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, Action<McpServerOptions>? configureOptions = null)
{
var options = CreateDefaultServerOptions();
configureOptions?.Invoke(options);
return AddMcpServer(services, options);
}
/// <summary>
/// Adds the MCP server to the service collection with the provided options.
/// </summary>
/// <param name="services"></param>
/// <param name="serverOptions"></param>
/// <returns></returns>
public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, McpServerOptions serverOptions)
{
services.AddSingleton(serverOptions);
services.AddHostedService<McpServerHostedService>();
services.AddOptions();
services.AddSingleton(services =>
{
IServerTransport serverTransport = services.GetRequiredService<IServerTransport>();
McpServerOptions options = services.GetRequiredService<McpServerOptions>();
IOptions<McpServerOptions> options = services.GetRequiredService<IOptions<McpServerOptions>>();
ILoggerFactory? loggerFactory = services.GetService<ILoggerFactory>();
if (services.GetService<IOptions<McpServerHandlers>>() is { } handlersOptions)
{
options = handlersOptions.Value.OverwriteWithSetHandlers(options);
}
return McpServerFactory.Create(serverTransport, options, loggerFactory, services);
return McpServerFactory.Create(serverTransport, options.Value, loggerFactory, services);
});
services.AddOptions();
services.AddTransient<IConfigureOptions<McpServerOptions>, McpServerOptionsSetup>();
if (configureOptions is not null)
{
services.Configure(configureOptions);
}
return new DefaultMcpServerBuilder(services);
}
private static McpServerOptions CreateDefaultServerOptions()
{
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
return new McpServerOptions()
{
ServerInfo = new Implementation() { Name = assemblyName?.Name ?? "McpServer", Version = assemblyName?.Version?.ToString() ?? "1.0.0" },
Capabilities = new ServerCapabilities()
{
Tools = new(),
Resources = new(),
Prompts = new(),
},
ProtocolVersion = "2024-11-05"
};
}
}

View file

@ -235,10 +235,10 @@ internal class HttpListenerServerProvider : IDisposable
response.StatusCode = 202;
// Write "accepted" response
// TODO: Use WriteAsync, add cancellation token and netstandard2.0 support (polyfill?)
#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods
byte[] buffer = Encoding.UTF8.GetBytes("Accepted");
response.OutputStream.Write(buffer, 0, buffer.Length);
#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
#pragma warning restore CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
}
else
{

View file

@ -6,6 +6,7 @@ using McpDotNet.Utils;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
#pragma warning disable CA2208 // Instantiate argument exceptions correctly
#pragma warning disable CA2213 // Disposable fields should be disposed
@ -47,6 +48,24 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StdioServerTransport"/> class, using
/// <see cref="Console.In"/> and <see cref="Console.Out"/> for input and output streams.
/// </summary>
/// <param name="serverOptions">The server options.</param>
/// <param name="loggerFactory">Optional logger factory used for logging employed by the transport.</param>
/// <exception cref="ArgumentNullException"><paramref name="serverOptions"/> is <see langword="null"/> or contains a null name.</exception>
/// <remarks>
/// <para>
/// By default, no logging is performed. If a <paramref name="loggerFactory"/> is supplied, it must not log
/// to <see cref="Console.Out"/>, as that will interfere with the transport's output.
/// </para>
/// </remarks>
public StdioServerTransport(IOptions<McpServerOptions> serverOptions, ILoggerFactory? loggerFactory = null)
: this(GetServerName(serverOptions.Value), loggerFactory)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StdioServerTransport"/> class, using
/// <see cref="Console.In"/> and <see cref="Console.Out"/> for input and output streams.

View file

@ -12,7 +12,7 @@ namespace McpDotNet.Server;
/// <inheritdoc />
internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
{
private readonly IServerTransport _serverTransport;
private readonly IServerTransport? _serverTransport;
private readonly McpServerOptions _options;
private volatile bool _isInitializing;
private readonly ILogger _logger;
@ -26,12 +26,12 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
/// <param name="loggerFactory">Logger factory to use for logging</param>
/// <param name="serviceProvider">Optional service provider to use for dependency injection</param>
/// <exception cref="McpServerException"></exception>
public McpServer(IServerTransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider)
public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider)
: base(transport, loggerFactory)
{
Throw.IfNull(options);
_serverTransport = transport;
_serverTransport = transport as IServerTransport;
_options = options;
_logger = (ILogger?)loggerFactory?.CreateLogger<McpServer>() ?? NullLogger.Instance;
ServerInstructions = options.ServerInstructions;
@ -86,8 +86,11 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
{
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
// Start listening for messages
await _serverTransport.StartListeningAsync(CancellationTokenSource.Token).ConfigureAwait(false);
if (_serverTransport is not null)
{
// Start listening for messages
await _serverTransport.StartListeningAsync(CancellationTokenSource.Token).ConfigureAwait(false);
}
// Start processing messages
MessageProcessingTask = ProcessMessagesAsync(CancellationTokenSource.Token);

View file

@ -23,7 +23,7 @@ public static class McpServerFactory
/// <exception cref="ArgumentNullException"><paramref name="serverTransport"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="serverOptions"/> is <see langword="null"/>.</exception>
public static IMcpServer Create(
IServerTransport serverTransport,
ITransport serverTransport,
McpServerOptions serverOptions,
ILoggerFactory? loggerFactory = null,
IServiceProvider? serviceProvider = null)

View file

@ -57,7 +57,7 @@ public sealed class McpServerHandlers
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
internal McpServerOptions OverwriteWithSetHandlers(McpServerOptions options)
internal void OverwriteWithSetHandlers(McpServerOptions options)
{
PromptsCapability? promptsCapability = options.Capabilities?.Prompts;
if (ListPromptsHandler is not null || GetPromptHandler is not null)
@ -118,22 +118,20 @@ public sealed class McpServerHandlers
};
}
return options with
{
GetCompletionHandler = GetCompletionHandler ?? options.GetCompletionHandler,
Capabilities = options.Capabilities is null ?
new()
{
Prompts = promptsCapability,
Resources = resourcesCapability,
Tools = toolsCapability,
} :
options.Capabilities with
{
Prompts = promptsCapability,
Resources = resourcesCapability,
Tools = toolsCapability,
},
};
options.Capabilities = options.Capabilities is null ?
new()
{
Prompts = promptsCapability,
Resources = resourcesCapability,
Tools = toolsCapability,
} :
options.Capabilities with
{
Prompts = promptsCapability,
Resources = resourcesCapability,
Tools = toolsCapability,
};
options.GetCompletionHandler = GetCompletionHandler ?? options.GetCompletionHandler;
}
}

View file

@ -9,36 +9,36 @@ namespace McpDotNet.Server;
/// protocol version.
/// <see href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/">See the protocol specification for details on capability negotiation</see>
/// </summary>
public record McpServerOptions
public class McpServerOptions
{
/// <summary>
/// Information about this server implementation.
/// </summary>
public required Implementation ServerInfo { get; init; }
public required Implementation ServerInfo { get; set; }
/// <summary>
/// Server capabilities to advertise to the server.
/// </summary>
public ServerCapabilities? Capabilities { get; init; }
public ServerCapabilities? Capabilities { get; set; }
/// <summary>
/// Protocol version to request from the server.
/// </summary>
public string ProtocolVersion { get; init; } = "2024-11-05";
public string ProtocolVersion { get; set; } = "2024-11-05";
/// <summary>
/// Timeout for initialization sequence.
/// </summary>
public TimeSpan InitializationTimeout { get; init; } = TimeSpan.FromSeconds(60);
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Optional server instructions to send to clients
/// </summary>
public string ServerInstructions { get; init; } = string.Empty;
public string ServerInstructions { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the handler for get completion requests.
/// </summary>
[JsonIgnore]
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; init; }
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
}

View file

@ -35,6 +35,7 @@
<ItemGroup>
<InternalsVisibleTo Include="McpDotNet.Tests" />
<InternalsVisibleTo Include="AspNetCoreSseServer" />
</ItemGroup>
<ItemGroup>