Improve how to host McpServer (#30)

This commit is contained in:
Toni Wenzel 2025-03-05 21:15:22 +01:00 • committed by GitHub
commit 257a97a659
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 538 additions and 45 deletions

View file

@ -0,0 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet.Configuration;
/// <summary>
/// Default implementation of <see cref="IMcpServerBuilder"/>.
/// </summary>
internal class DefaultMcpServerBuilder : IMcpServerBuilder
{
/// <inheritdoc/>
public IServiceCollection Services { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DefaultMcpServerBuilder"/> class.
/// </summary>
/// <param name="services"></param>
/// <exception cref="ArgumentNullException"></exception>
public DefaultMcpServerBuilder(IServiceCollection services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
}
}

View file

@ -0,0 +1,15 @@
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet.Configuration;
/// <summary>
/// Builder for configuring <see cref="IMcpServer"/> instances.
/// </summary>
public interface IMcpServerBuilder
{
/// <summary>
/// Gets the service collection.
/// </summary>
IServiceCollection Services { get; }
}

View file

@ -0,0 +1,132 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
/// <summary>
/// Extension to configure the MCP server
/// </summary>
public static class McpServerBuilderExtensions
{
/// <summary>
/// Adds a server transport that uses stdin/stdout for communication.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <returns></returns>
public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder builder)
{
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
return builder;
}
/// <summary>
/// Sets the handler for list tools requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func<ListToolsRequestParams, CancellationToken, Task<ListToolsResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ListToolsHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for call tool requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func<CallToolRequestParams, CancellationToken, Task<CallToolResponse>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.CallToolHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for list prompts requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func<ListPromptsRequestParams, CancellationToken, Task<ListPromptsResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ListPromptsHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for get prompt requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func<GetPromptRequestParams, CancellationToken, Task<GetPromptResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.GetPromptHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for list resources requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func<ListResourcesRequestParams, CancellationToken, Task<ListResourcesResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ListResourcesHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for read resources requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func<ReadResourceRequestParams, CancellationToken, Task<ReadResourceResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.ReadResourceHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for get resources requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func<CompleteRequestParams, CancellationToken, Task<CompleteResult>> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.GetCompletionHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for subscribe to resources messages.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func<string, CancellationToken, Task> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.SubscribeToResourcesHandler = handler);
return builder;
}
/// <summary>
/// Sets or sets the handler for subscribe to resources messages.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
/// <returns></returns>
public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func<string, CancellationToken, Task> handler)
{
builder.Services.Configure<McpServerDelegates>(s => s.UnsubscribeFromResourcesHandler = handler);
return builder;
}
}

View file

@ -0,0 +1,63 @@
using System.Reflection;
using McpDotNet.Configuration;
using McpDotNet.Hosting;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
/// <summary>
/// Extension to host the MCP server
/// </summary>
public static class McpServerServiceCollectionExtension
{
/// <summary>
/// Adds the MCP server to the service collection with default options.
/// </summary>
/// <param name="services"></param>
/// <param name="configureOptions"></param>
/// <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.AddSingleton<IMcpServerFactory, McpServerFactory>();
services.AddHostedService<McpServerHostedService>();
services.AddOptions();
services.AddSingleton<IMcpServer>(sp => sp.GetRequiredService<IMcpServerFactory>().CreateServer());
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

@ -0,0 +1,28 @@
using McpDotNet.Server;
using Microsoft.Extensions.Hosting;
namespace McpDotNet.Hosting;
/// <summary>
/// Hosted service for the MCP server.
/// </summary>
public class McpServerHostedService : BackgroundService
{
private readonly IMcpServer _server;
/// <summary>
/// Creates a new instance of the McpServerHostedService.
/// </summary>
/// <param name="server">The MCP server instance</param>
/// <exception cref="ArgumentNullException"></exception>
public McpServerHostedService(IMcpServer server)
{
_server = server ?? throw new ArgumentNullException(nameof(server));
}
/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _server.StartAsync(stoppingToken);
}
}

View file

@ -1,8 +1,9 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using McpDotNet.Protocol.Messages;
using McpDotNet.Utils.Json;
using McpDotNet.Logging;
using McpDotNet.Protocol.Messages;
using McpDotNet.Server;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging;
namespace McpDotNet.Protocol.Transport;
@ -19,6 +20,16 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
private string EndpointName => $"Server (stdio) ({_serverName})";
/// <summary>
/// Initializes a new instance of the StdioServerTransport class.
/// </summary>
/// <param name="serverOptions">The server options.</param>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory loggerFactory)
: this(serverOptions.ServerInfo.Name, loggerFactory)
{
}
/// <summary>
/// Initializes a new instance of the StdioServerTransport class.
/// </summary>
@ -64,7 +75,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
{
var json = JsonSerializer.Serialize(message, _jsonOptions);
_logger.TransportSendingMessage(EndpointName, id, json);
using (Console.OpenStandardOutput())
{
await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken);
@ -159,7 +170,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
private async Task CleanupAsync(CancellationToken cancellationToken)
{
_logger.TransportCleaningUp(EndpointName);
_shutdownCts?.Cancel();
_shutdownCts?.Dispose();
_shutdownCts = null;

View file

@ -0,0 +1,13 @@
namespace McpDotNet.Server;
/// <summary>
/// Factory for creating <see cref="IMcpServer"/> instances.
/// </summary>
public interface IMcpServerFactory
{
/// <summary>
/// Creates a new server instance.
/// </summary>
/// <returns></returns>
IMcpServer CreateServer();
}

View file

@ -1,11 +1,11 @@

using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
using McpDotNet.Protocol.Types;
using McpDotNet.Shared;
using McpDotNet.Protocol.Transport;
using McpDotNet.Logging;
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Shared;
using Microsoft.Extensions.Logging;
namespace McpDotNet.Server;
@ -22,22 +22,22 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
/// </summary>
/// <param name="transport">Transport to use for the server</param>
/// <param name="options">Configuration options for this server, including capabilities.
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
/// <param name="serverInstructions">Optional server instructions to send to clients</param>
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
/// <param name="loggerFactory">Logger factory to use for logging</param>
/// <exception cref="McpServerException"></exception>
public McpServer(IServerTransport transport, McpServerOptions options, string? serverInstructions, ILoggerFactory loggerFactory)
public McpServer(IServerTransport transport, McpServerOptions options, ILoggerFactory loggerFactory)
: base(transport, loggerFactory)
{
_serverTransport = transport;
_options = options;
_logger = loggerFactory.CreateLogger<McpServer>();
ServerInstructions = serverInstructions;
ServerInstructions = options.ServerInstructions;
if (options.Capabilities?.Tools != null)
{
SetRequestHandler<ListToolsRequestParams, ListToolsResult>("tools/list",
async (request) => {
async (request) =>
{
if (ListToolsHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
@ -49,7 +49,8 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
});
SetRequestHandler<CallToolRequestParams, CallToolResponse>("tools/call",
async (request) => {
async (request) =>
{
if (CallToolHandler == null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
@ -265,7 +266,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
if (ClientCapabilities?.Roots == null)
{
throw new McpServerException("Client does not support roots");
}
}
return await SendRequestAsync<ListRootsResult>(
new JsonRpcRequest

View file

@ -0,0 +1,88 @@
using McpDotNet.Protocol.Types;
namespace McpDotNet.Server;
/// <summary>
/// Container for delegates that can be applied to an MCP server.
/// </summary>
public class McpServerDelegates
{
/// <summary>
/// Gets or sets the handler for list tools requests.
/// </summary>
public Func<ListToolsRequestParams, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for call tool requests.
/// </summary>
public Func<CallToolRequestParams, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list prompts requests.
/// </summary>
public Func<ListPromptsRequestParams, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get prompt requests.
/// </summary>
public Func<GetPromptRequestParams, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list resources requests.
/// </summary>
public Func<ListResourcesRequestParams, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for read resources requests.
/// </summary>
public Func<ReadResourceRequestParams, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get resources requests.
/// </summary>
public Func<CompleteRequestParams, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
public Func<string, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
public Func<string, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
/// <summary>
/// Applies the delegates to the server.
/// </summary>
/// <param name="server"></param>
public void Apply(IMcpServer server)
{
if (ListToolsHandler != null)
server.ListToolsHandler = ListToolsHandler;
if (CallToolHandler != null)
server.CallToolHandler = CallToolHandler;
if (ListPromptsHandler != null)
server.ListPromptsHandler = ListPromptsHandler;
if (GetPromptHandler != null)
server.GetPromptHandler = GetPromptHandler;
if (ListResourcesHandler != null)
server.ListResourcesHandler = ListResourcesHandler;
if (ReadResourceHandler != null)
server.ReadResourceHandler = ReadResourceHandler;
if (GetCompletionHandler != null)
server.GetCompletionHandler = GetCompletionHandler;
if (SubscribeToResourcesHandler != null)
server.SubscribeToResourcesHandler = SubscribeToResourcesHandler;
if (UnsubscribeFromResourcesHandler != null)
server.UnsubscribeFromResourcesHandler = UnsubscribeFromResourcesHandler;
}
}

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace McpDotNet.Server;
@ -13,12 +14,12 @@ namespace McpDotNet.Server;
///
/// You must register handlers for all supported capabilities on the server instance, before calling BeginListeningAsync.
/// </summary>
public class McpServerFactory
public class McpServerFactory : IMcpServerFactory
{
private readonly IServerTransport _serverTransport;
private readonly McpServerOptions _options;
private readonly ILoggerFactory _loggerFactory;
private readonly string? _serverInstructions;
private readonly McpServerDelegates? _serverDelegates;
/// <summary>
/// Initializes a new instance of the <see cref="McpServerFactory"/> class.
@ -26,15 +27,14 @@ public class McpServerFactory
/// <param name="serverTransport">Transport to use for the server</param>
/// <param name="options">Configuration options for this server, including capabilities.
/// Make sure to accurately reflect exactly what capabilities the server supports and does not support.</param>
/// <param name="serverInstructions">Optional server instructions to send to clients</param>
/// <param name="loggerFactory">Logger factory to use for logging</param>
public McpServerFactory(IServerTransport serverTransport, McpServerOptions options, ILoggerFactory loggerFactory,
string? serverInstructions = null)
/// <param name="serverDelegates"></param>
public McpServerFactory(IServerTransport serverTransport, McpServerOptions options, ILoggerFactory loggerFactory, IOptions<McpServerDelegates>? serverDelegates = null)
{
_serverTransport = serverTransport;
_options = options;
_loggerFactory = loggerFactory;
_serverInstructions = serverInstructions;
_serverTransport = serverTransport ?? throw new ArgumentNullException(nameof(serverTransport));
_options = options ?? throw new ArgumentNullException(nameof(options));
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_serverDelegates = serverDelegates?.Value;
}
/// <summary>
@ -44,6 +44,10 @@ public class McpServerFactory
/// </summary>
public IMcpServer CreateServer()
{
return new McpServer(_serverTransport, _options, _serverInstructions, _loggerFactory);
var server = new McpServer(_serverTransport, _options, _loggerFactory);
_serverDelegates?.Apply(server);
return server;
}
}

View file

@ -29,4 +29,9 @@ public record McpServerOptions
/// Timeout for initialization sequence.
/// </summary>
public TimeSpan InitializationTimeout { get; init; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Optional server instructions to send to clients
/// </summary>
public string ServerInstructions { get; init; } = string.Empty;
}

View file

@ -20,7 +20,7 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<!-- Assembly properties -->
<AssemblyName>mcpdotnet</AssemblyName>
<RootNamespace>mcpdotnet</RootNamespace>
<RootNamespace>McpDotNet</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
@ -35,6 +35,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
</ItemGroup>