diff --git a/.gitignore b/.gitignore
index 5e03da8..1e5489a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Cake tools
-[Tt]ools/
+/[Tt]ools/
# Build output
[Bb]uildArtifacts/
diff --git a/mcpdotnet.sln b/mcpdotnet.sln
index 09efe66..818e0e8 100644
--- a/mcpdotnet.sln
+++ b/mcpdotnet.sln
@@ -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
@@ -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
@@ -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}
diff --git a/samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj b/samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj
new file mode 100644
index 0000000..5fc6fc9
--- /dev/null
+++ b/samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs b/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs
new file mode 100644
index 0000000..7b5060f
--- /dev/null
+++ b/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs
@@ -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();
+ var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService>();
+
+ 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(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;
+ }
+}
diff --git a/samples/AspNetCoreSseServer/Program.cs b/samples/AspNetCoreSseServer/Program.cs
new file mode 100644
index 0000000..1a4d9a3
--- /dev/null
+++ b/samples/AspNetCoreSseServer/Program.cs
@@ -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();
diff --git a/samples/AspNetCoreSseServer/Properties/launchSettings.json b/samples/AspNetCoreSseServer/Properties/launchSettings.json
new file mode 100644
index 0000000..d1d3370
--- /dev/null
+++ b/samples/AspNetCoreSseServer/Properties/launchSettings.json
@@ -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"
+ }
+ }
+ }
+}
diff --git a/samples/AspNetCoreSseServer/SseServerStreamTransport.cs b/samples/AspNetCoreSseServer/SseServerStreamTransport.cs
new file mode 100644
index 0000000..c8d5331
--- /dev/null
+++ b/samples/AspNetCoreSseServer/SseServerStreamTransport.cs
@@ -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 _incomingChannel = CreateSingleItemChannel();
+ private readonly Channel> _outgoingSseChannel = CreateSingleItemChannel>();
+
+ private Task? _sseWriteTask;
+ private Utf8JsonWriter? _jsonWriter;
+
+ public bool IsConnected => _sseWriteTask?.IsCompleted == false;
+
+ public Task RunAsync(CancellationToken cancellationToken)
+ {
+ void WriteJsonRpcMessageToBuffer(SseItem item, IBufferWriter 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(null, "endpoint"));
+
+ var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
+ return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
+ }
+
+ public ChannelReader 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(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 CreateSingleItemChannel() =>
+ Channel.CreateBounded(new BoundedChannelOptions(1)
+ {
+ SingleReader = true,
+ SingleWriter = false,
+ });
+
+ private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter writer)
+ {
+ if (_jsonWriter is null)
+ {
+ _jsonWriter = new Utf8JsonWriter(writer);
+ }
+ else
+ {
+ _jsonWriter.Reset(writer);
+ }
+
+ return _jsonWriter;
+ }
+}
diff --git a/samples/AspNetCoreSseServer/Tools/EchoTool.cs b/samples/AspNetCoreSseServer/Tools/EchoTool.cs
new file mode 100644
index 0000000..25c9f22
--- /dev/null
+++ b/samples/AspNetCoreSseServer/Tools/EchoTool.cs
@@ -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;
+ }
+}
diff --git a/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs b/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs
new file mode 100644
index 0000000..a49669f
--- /dev/null
+++ b/samples/AspNetCoreSseServer/Tools/SampleLlmTool.cs
@@ -0,0 +1,51 @@
+using McpDotNet.Protocol.Types;
+using McpDotNet.Server;
+using System.ComponentModel;
+
+namespace TestServerWithHosting.Tools;
+
+///
+/// This tool uses depenency injection and async method
+///
+[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 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
+ };
+ }
+}
diff --git a/samples/AspNetCoreSseServer/appsettings.Development.json b/samples/AspNetCoreSseServer/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/samples/AspNetCoreSseServer/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/AspNetCoreSseServer/appsettings.json b/samples/AspNetCoreSseServer/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/samples/AspNetCoreSseServer/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/TestServerWithHosting/Program.cs b/samples/TestServerWithHosting/Program.cs
index f1bdf7c..007a85b 100644
--- a/samples/TestServerWithHosting/Program.cs
+++ b/samples/TestServerWithHosting/Program.cs
@@ -8,7 +8,7 @@ Log.Logger = new LoggerConfiguration()
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
@@ -19,8 +19,7 @@ try
builder.Services.AddSerilog();
builder.Services.AddMcpServer()
.WithStdioServerTransport()
- .WithTools()
- .WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse()));
+ .WithTools();
var app = builder.Build();
diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs
index 2a44d40..147b9aa 100644
--- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs
+++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs
@@ -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();
+ builder.Services.AddHostedService();
return builder;
}
@@ -34,6 +36,7 @@ public static partial class McpServerBuilderExtensions
}
builder.Services.AddSingleton();
+ builder.Services.AddHostedService();
return builder;
}
}
diff --git a/src/mcpdotnet/Configuration/McpServerOptionsSetup.cs b/src/mcpdotnet/Configuration/McpServerOptionsSetup.cs
new file mode 100644
index 0000000..50bc89d
--- /dev/null
+++ b/src/mcpdotnet/Configuration/McpServerOptionsSetup.cs
@@ -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 serverHandlers) : IConfigureOptions
+{
+ 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);
+ }
+}
diff --git a/src/mcpdotnet/Configuration/McpServerServiceCollectionExtension.cs b/src/mcpdotnet/Configuration/McpServerServiceCollectionExtension.cs
index 79b1776..e90ed1a 100644
--- a/src/mcpdotnet/Configuration/McpServerServiceCollectionExtension.cs
+++ b/src/mcpdotnet/Configuration/McpServerServiceCollectionExtension.cs
@@ -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
///
public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, Action? configureOptions = null)
{
- var options = CreateDefaultServerOptions();
- configureOptions?.Invoke(options);
-
- return AddMcpServer(services, options);
- }
-
- ///
- /// Adds the MCP server to the service collection with the provided options.
- ///
- ///
- ///
- ///
- public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, McpServerOptions serverOptions)
- {
- services.AddSingleton(serverOptions);
- services.AddHostedService();
- services.AddOptions();
services.AddSingleton(services =>
{
IServerTransport serverTransport = services.GetRequiredService();
- McpServerOptions options = services.GetRequiredService();
+ IOptions options = services.GetRequiredService>();
ILoggerFactory? loggerFactory = services.GetService();
- if (services.GetService>() 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, 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"
- };
- }
}
diff --git a/src/mcpdotnet/Protocol/Transport/HttpListenerServerProvider.cs b/src/mcpdotnet/Protocol/Transport/HttpListenerServerProvider.cs
index a9fc8d2..e253359 100644
--- a/src/mcpdotnet/Protocol/Transport/HttpListenerServerProvider.cs
+++ b/src/mcpdotnet/Protocol/Transport/HttpListenerServerProvider.cs
@@ -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
{
diff --git a/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
index 17585ff..46785ca 100644
--- a/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
+++ b/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
@@ -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
{
}
+ ///
+ /// Initializes a new instance of the class, using
+ /// and for input and output streams.
+ ///
+ /// The server options.
+ /// Optional logger factory used for logging employed by the transport.
+ /// is or contains a null name.
+ ///
+ ///
+ /// By default, no logging is performed. If a is supplied, it must not log
+ /// to , as that will interfere with the transport's output.
+ ///
+ ///
+ public StdioServerTransport(IOptions serverOptions, ILoggerFactory? loggerFactory = null)
+ : this(GetServerName(serverOptions.Value), loggerFactory)
+ {
+ }
+
///
/// Initializes a new instance of the class, using
/// and for input and output streams.
diff --git a/src/mcpdotnet/Server/McpServer.cs b/src/mcpdotnet/Server/McpServer.cs
index e0bd9d7..b1308bb 100644
--- a/src/mcpdotnet/Server/McpServer.cs
+++ b/src/mcpdotnet/Server/McpServer.cs
@@ -12,7 +12,7 @@ namespace McpDotNet.Server;
///
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
/// Logger factory to use for logging
/// Optional service provider to use for dependency injection
///
- 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() ?? 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);
diff --git a/src/mcpdotnet/Server/McpServerFactory.cs b/src/mcpdotnet/Server/McpServerFactory.cs
index a8e37d2..bc5a95b 100644
--- a/src/mcpdotnet/Server/McpServerFactory.cs
+++ b/src/mcpdotnet/Server/McpServerFactory.cs
@@ -23,7 +23,7 @@ public static class McpServerFactory
/// is .
/// is .
public static IMcpServer Create(
- IServerTransport serverTransport,
+ ITransport serverTransport,
McpServerOptions serverOptions,
ILoggerFactory? loggerFactory = null,
IServiceProvider? serviceProvider = null)
diff --git a/src/mcpdotnet/Server/McpServerHandlers.cs b/src/mcpdotnet/Server/McpServerHandlers.cs
index a4edc98..94e33f4 100644
--- a/src/mcpdotnet/Server/McpServerHandlers.cs
+++ b/src/mcpdotnet/Server/McpServerHandlers.cs
@@ -57,7 +57,7 @@ public sealed class McpServerHandlers
///
///
///
- 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;
}
}
diff --git a/src/mcpdotnet/Server/McpServerOptions.cs b/src/mcpdotnet/Server/McpServerOptions.cs
index 40bfa4f..ad109c5 100644
--- a/src/mcpdotnet/Server/McpServerOptions.cs
+++ b/src/mcpdotnet/Server/McpServerOptions.cs
@@ -9,36 +9,36 @@ namespace McpDotNet.Server;
/// protocol version.
/// See the protocol specification for details on capability negotiation
///
-public record McpServerOptions
+public class McpServerOptions
{
///
/// Information about this server implementation.
///
- public required Implementation ServerInfo { get; init; }
+ public required Implementation ServerInfo { get; set; }
///
/// Server capabilities to advertise to the server.
///
- public ServerCapabilities? Capabilities { get; init; }
+ public ServerCapabilities? Capabilities { get; set; }
///
/// Protocol version to request from the server.
///
- public string ProtocolVersion { get; init; } = "2024-11-05";
+ public string ProtocolVersion { get; set; } = "2024-11-05";
///
/// Timeout for initialization sequence.
///
- public TimeSpan InitializationTimeout { get; init; } = TimeSpan.FromSeconds(60);
+ public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);
///
/// Optional server instructions to send to clients
///
- public string ServerInstructions { get; init; } = string.Empty;
+ public string ServerInstructions { get; set; } = string.Empty;
///
/// Gets or sets the handler for get completion requests.
///
[JsonIgnore]
- public Func, CancellationToken, Task>? GetCompletionHandler { get; init; }
+ public Func, CancellationToken, Task>? GetCompletionHandler { get; set; }
}
diff --git a/src/mcpdotnet/mcpdotnet.csproj b/src/mcpdotnet/mcpdotnet.csproj
index 9e67c34..4e1b538 100644
--- a/src/mcpdotnet/mcpdotnet.csproj
+++ b/src/mcpdotnet/mcpdotnet.csproj
@@ -35,6 +35,7 @@
+
diff --git a/tests/mcpdotnet.TestSseServer/Program.cs b/tests/mcpdotnet.TestSseServer/Program.cs
index 438a3a8..dd77be8 100644
--- a/tests/mcpdotnet.TestSseServer/Program.cs
+++ b/tests/mcpdotnet.TestSseServer/Program.cs
@@ -41,12 +41,10 @@ internal class Program
ProtocolVersion = "2024-11-05",
ServerInstructions = "This is a test server with only stub functionality"
};
- var loggerFactory = CreateLoggerFactory();
- McpServerFactory factory = new McpServerFactory(new HttpListenerSseServerTransport("TestServer", 3001, loggerFactory), options, loggerFactory);
- IMcpServer server = factory.CreateServer();
+ IMcpServer? server = null;
- Console.WriteLine("Server object created, registering handlers.");
+ Console.WriteLine("Registering handlers.");
#region Helped method
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
@@ -70,85 +68,6 @@ internal class Program
}
#endregion
- #region Tools
- server.SetListToolsHandler((request, cancellationToken) =>
- {
- return Task.FromResult(new ListToolsResult()
- {
- Tools =
- [
- new Tool()
- {
- Name = "echo",
- Description = "Echoes the input back to the client.",
- InputSchema = new JsonSchema()
- {
- Type = "object",
- Properties = new Dictionary()
- {
- ["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
- }
- },
- },
- new Tool()
- {
- Name = "sampleLLM",
- Description = "Samples from an LLM using MCP's sampling feature.",
- InputSchema = new JsonSchema()
- {
- Type = "object",
- Properties = new Dictionary()
- {
- ["prompt"] = new JsonSchemaProperty() { Type = "string", Description = "The prompt to send to the LLM" },
- ["maxTokens"] = new JsonSchemaProperty() { Type = "number", Description = "Maximum number of tokens to generate" }
- }
- },
- }
- ]
- });
- });
-
- server.SetCallToolHandler(async (request, cancellationToken) =>
- {
- if (request.Params is null)
- {
- throw new McpServerException("Missing required parameter 'name'");
- }
- if (request.Params.Name == "echo")
- {
- if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
- {
- throw new McpServerException("Missing required argument 'message'");
- }
- return new CallToolResponse()
- {
- Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
- };
- }
- else if (request.Params.Name == "sampleLLM")
- {
- if (request.Params.Arguments is null ||
- !request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
- !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
- {
- throw new McpServerException("Missing required arguments 'prompt' and 'maxTokens'");
- }
- var sampleResult = await server.RequestSamplingAsync(CreateRequestSamplingParams(prompt?.ToString() ?? "", "sampleLLM", Convert.ToInt32(maxTokens?.ToString())),
- cancellationToken);
-
- return new CallToolResponse()
- {
- Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }]
- };
- }
- else
- {
- throw new McpServerException($"Unknown tool: {request.Params.Name}");
- }
- });
- #endregion
-
- #region Resources
List resources = new();
List resourceContents = new();
for (int i = 0; i < 100; ++i)
@@ -189,156 +108,236 @@ internal class Program
const int pageSize = 10;
- server.SetListResourcesHandler((request, cancellationToken) =>
+ options.Capabilities = new()
{
- int startIndex = 0;
- var requestParams = request.Params ?? new();
- if (requestParams.Cursor is not null)
+ Tools = new()
{
- try
+ ListToolsHandler = (request, cancellationToken) =>
{
- var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor));
- startIndex = Convert.ToInt32(startIndexAsString);
- }
- catch
- {
- throw new McpServerException("Invalid cursor");
- }
- }
-
- int endIndex = Math.Min(startIndex + pageSize, resources.Count);
- string? nextCursor = null;
-
- if (endIndex < resources.Count)
- {
- nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
- }
- return Task.FromResult(new ListResourcesResult()
- {
- NextCursor = nextCursor,
- Resources = resources.GetRange(startIndex, endIndex - startIndex)
- });
- });
-
- server.SetReadResourceHandler((request, cancellationToken) =>
- {
- if (request.Params?.Uri is null)
- {
- throw new McpServerException("Missing required argument 'uri'");
- }
- ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri);
- if (contents is null)
- {
- throw new McpServerException("Resource not found");
- }
- return Task.FromResult(new ReadResourceResult()
- {
- Contents = [contents]
- });
- });
- #endregion
-
- #region Prompts
- server.SetListPromptsHandler((request, cancellationToken) =>
- {
- return Task.FromResult(new ListPromptsResult()
- {
- Prompts = [
- new Prompt()
+ return Task.FromResult(new ListToolsResult()
{
- Name = "simple_prompt",
- Description = "A prompt without arguments"
- },
- new Prompt()
- {
- Name = "complex_prompt",
- Description = "A prompt with arguments",
- Arguments = new()
- {
- new PromptArgument()
+ Tools =
+ [
+ new Tool()
{
- Name = "temperature",
- Description = "Temperature setting",
- Required = true
+ Name = "echo",
+ Description = "Echoes the input back to the client.",
+ InputSchema = new JsonSchema()
+ {
+ Type = "object",
+ Properties = new Dictionary()
+ {
+ ["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
+ }
+ },
},
- new PromptArgument()
+ new Tool()
{
- Name = "style",
- Description = "Output style",
- Required = false
+ Name = "sampleLLM",
+ Description = "Samples from an LLM using MCP's sampling feature.",
+ InputSchema = new JsonSchema()
+ {
+ Type = "object",
+ Properties = new Dictionary()
+ {
+ ["prompt"] = new JsonSchemaProperty() { Type = "string", Description = "The prompt to send to the LLM" },
+ ["maxTokens"] = new JsonSchemaProperty() { Type = "number", Description = "Maximum number of tokens to generate" }
+ }
+ },
}
+ ]
+ });
+ },
+ CallToolHandler = async (request, cancellationToken) =>
+ {
+ if (request.Params is null)
+ {
+ throw new McpServerException("Missing required parameter 'name'");
+ }
+ if (request.Params.Name == "echo")
+ {
+ if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message))
+ {
+ throw new McpServerException("Missing required argument 'message'");
+ }
+ return new CallToolResponse()
+ {
+ Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
+ };
+ }
+ else if (request.Params.Name == "sampleLLM")
+ {
+ if (request.Params.Arguments is null ||
+ !request.Params.Arguments.TryGetValue("prompt", out var prompt) ||
+ !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens))
+ {
+ throw new McpServerException("Missing required arguments 'prompt' and 'maxTokens'");
+ }
+ var sampleResult = await server!.RequestSamplingAsync(CreateRequestSamplingParams(prompt?.ToString() ?? "", "sampleLLM", Convert.ToInt32(maxTokens?.ToString())),
+ cancellationToken);
+
+ return new CallToolResponse()
+ {
+ Content = [new Content() { Text = $"LLM sampling result: {sampleResult.Content.Text}", Type = "text" }]
+ };
+ }
+ else
+ {
+ throw new McpServerException($"Unknown tool: {request.Params.Name}");
+ }
+ }
+ },
+ Resources = new()
+ {
+ ListResourcesHandler = (request, cancellationToken) =>
+ {
+ int startIndex = 0;
+ var requestParams = request.Params ?? new();
+ if (requestParams.Cursor is not null)
+ {
+ try
+ {
+ var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor));
+ startIndex = Convert.ToInt32(startIndexAsString);
+ }
+ catch
+ {
+ throw new McpServerException("Invalid cursor");
}
}
- ]
- });
- });
+
+ int endIndex = Math.Min(startIndex + pageSize, resources.Count);
+ string? nextCursor = null;
+
+ if (endIndex < resources.Count)
+ {
+ nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString()));
+ }
+ return Task.FromResult(new ListResourcesResult()
+ {
+ NextCursor = nextCursor,
+ Resources = resources.GetRange(startIndex, endIndex - startIndex)
+ });
+ },
+ ReadResourceHandler =(request, cancellationToken) =>
+ {
+ if (request.Params?.Uri is null)
+ {
+ throw new McpServerException("Missing required argument 'uri'");
+ }
+ ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri);
+ if (contents is null)
+ {
+ throw new McpServerException("Resource not found");
+ }
+ return Task.FromResult(new ReadResourceResult()
+ {
+ Contents = [contents]
+ });
+ }
+ },
+ Prompts = new()
+ {
+ ListPromptsHandler = (request, cancellationToken) =>
+ {
+ return Task.FromResult(new ListPromptsResult()
+ {
+ Prompts = [
+ new Prompt()
+ {
+ Name = "simple_prompt",
+ Description = "A prompt without arguments"
+ },
+ new Prompt()
+ {
+ Name = "complex_prompt",
+ Description = "A prompt with arguments",
+ Arguments = new()
+ {
+ new PromptArgument()
+ {
+ Name = "temperature",
+ Description = "Temperature setting",
+ Required = true
+ },
+ new PromptArgument()
+ {
+ Name = "style",
+ Description = "Output style",
+ Required = false
+ }
+ }
+ }
+ ]
+ });
+ },
+ GetPromptHandler = (request, cancellationToken) =>
+ {
+ if (request.Params is null)
+ {
+ throw new McpServerException("Missing required parameter 'name'");
+ }
+ List messages = new();
+ if (request.Params.Name == "simple_prompt")
+ {
+ messages.Add(new PromptMessage()
+ {
+ Role = Role.User,
+ Content = new Content()
+ {
+ Type = "text",
+ Text = "This is a simple prompt without arguments."
+ }
+ });
+ }
+ else if (request.Params.Name == "complex_prompt")
+ {
+ string temperature = request.Params.Arguments?["temperature"]?.ToString() ?? "unknown";
+ string style = request.Params.Arguments?["style"]?.ToString() ?? "unknown";
+ messages.Add(new PromptMessage()
+ {
+ Role = Role.User,
+ Content = new Content()
+ {
+ Type = "text",
+ Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}"
+ }
+ });
+ messages.Add(new PromptMessage()
+ {
+ Role = Role.Assistant,
+ Content = new Content()
+ {
+ Type = "text",
+ Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"
+ }
+ });
+ messages.Add(new PromptMessage()
+ {
+ Role = Role.User,
+ Content = new Content()
+ {
+ Type = "image",
+ Data = MCP_TINY_IMAGE,
+ MimeType = "image/png"
+ }
+ });
+ }
+ else
+ {
+ throw new McpServerException($"Unknown prompt: {request.Params.Name}");
+ }
- server.SetGetPromptHandler((request, cancellationToken) =>
- {
- if (request.Params is null)
- {
- throw new McpServerException("Missing required parameter 'name'");
- }
- List messages = new();
- if (request.Params.Name == "simple_prompt")
- {
- messages.Add(new PromptMessage()
- {
- Role = Role.User,
- Content = new Content()
+ return Task.FromResult(new GetPromptResult()
{
- Type = "text",
- Text = "This is a simple prompt without arguments."
- }
- });
- }
- else if (request.Params.Name == "complex_prompt")
- {
- string temperature = request.Params.Arguments?["temperature"]?.ToString() ?? "unknown";
- string style = request.Params.Arguments?["style"]?.ToString() ?? "unknown";
- messages.Add(new PromptMessage()
- {
- Role = Role.User,
- Content = new Content()
- {
- Type = "text",
- Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}"
- }
- });
- messages.Add(new PromptMessage()
- {
- Role = Role.Assistant,
- Content = new Content()
- {
- Type = "text",
- Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"
- }
- });
- messages.Add(new PromptMessage()
- {
- Role = Role.User,
- Content = new Content()
- {
- Type = "image",
- Data = MCP_TINY_IMAGE,
- MimeType = "image/png"
- }
- });
- }
- else
- {
- throw new McpServerException($"Unknown prompt: {request.Params.Name}");
- }
+ Messages = messages
+ });
+ }
+ },
+ };
- return Task.FromResult(new GetPromptResult()
- {
- Messages = messages
- });
- });
- #endregion
-
- #region Sampling
- #endregion
+ var loggerFactory = CreateLoggerFactory();
+ server = McpServerFactory.Create(new HttpListenerSseServerTransport("TestServer", 3001, loggerFactory), options, loggerFactory);
Console.WriteLine("Server initialized.");
diff --git a/tests/mcpdotnet.Tests/Server/McpServerTests.cs b/tests/mcpdotnet.Tests/Server/McpServerTests.cs
index e8edec0..53579bf 100644
--- a/tests/mcpdotnet.Tests/Server/McpServerTests.cs
+++ b/tests/mcpdotnet.Tests/Server/McpServerTests.cs
@@ -280,9 +280,9 @@ public class McpServerTests
await Can_Handle_Requests(
serverCapabilities: null,
method: "completion/complete",
- configureOptions: options => options with
+ configureOptions: options =>
{
- GetCompletionHandler = (request, ct) =>
+ options.GetCompletionHandler = (request, ct) =>
Task.FromResult(new CompleteResult
{
Completion = new()
@@ -291,7 +291,7 @@ public class McpServerTests
Total = 2,
HasMore = true
}
- })
+ });
},
assertResult: response =>
{
@@ -518,14 +518,11 @@ public class McpServerTests
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, "tools/call", "CallTool handler not configured");
}
- private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Func? configureOptions, Action