diff --git a/samples/anthropic/tools/ToolsConsole/AnthropicToolsConsole.csproj b/samples/anthropic/tools/ToolsConsole/AnthropicToolsConsole.csproj
index 04a14b4..a34ee4b 100644
--- a/samples/anthropic/tools/ToolsConsole/AnthropicToolsConsole.csproj
+++ b/samples/anthropic/tools/ToolsConsole/AnthropicToolsConsole.csproj
@@ -9,6 +9,7 @@
+
diff --git a/samples/anthropic/tools/ToolsConsole/Program.cs b/samples/anthropic/tools/ToolsConsole/Program.cs
index 8801ffd..c24e34d 100644
--- a/samples/anthropic/tools/ToolsConsole/Program.cs
+++ b/samples/anthropic/tools/ToolsConsole/Program.cs
@@ -1,6 +1,7 @@
using Anthropic.SDK;
using Anthropic.SDK.Constants;
using Anthropic.SDK.Messaging;
+using System.Linq;
using McpDotNet;
using McpDotNet.Client;
using McpDotNet.Configuration;
@@ -46,8 +47,8 @@ internal class Program
var client = await GetMcpClientAsync();
Console.WriteLine("MCP 'everything' server initialized");
Console.WriteLine("Listing tools...");
- var tools = await client.ListToolsAsync();
- var anthropicTools = tools.Tools.ToAnthropicTools();
+ var tools = await client.ListToolsAsync().ToListAsync();
+ var anthropicTools = tools.ToAnthropicTools();
Console.WriteLine("Tools available:");
foreach (var tool in anthropicTools)
{
diff --git a/samples/microsoft.extensions.ai/tools/ToolsConsole/MEAIToolsConsole.csproj b/samples/microsoft.extensions.ai/tools/ToolsConsole/MEAIToolsConsole.csproj
index 8a2b33e..decc435 100644
--- a/samples/microsoft.extensions.ai/tools/ToolsConsole/MEAIToolsConsole.csproj
+++ b/samples/microsoft.extensions.ai/tools/ToolsConsole/MEAIToolsConsole.csproj
@@ -10,6 +10,7 @@
+
diff --git a/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs b/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs
index e1e7388..73894b0 100644
--- a/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs
+++ b/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs
@@ -45,8 +45,7 @@ internal class Program
var client = await GetMcpClientAsync();
Console.WriteLine("MCP 'everything' server initialized");
Console.WriteLine("Listing tools...");
- var tools = await client.ListToolsAsync();
- var mappedTools = tools.Tools.Select(t => t.ToAITool(client)).ToList();
+ var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();
Console.WriteLine("Tools available:");
foreach (var tool in mappedTools)
{
diff --git a/src/McpDotNet.Extensions.AI/McpSessionScope.cs b/src/McpDotNet.Extensions.AI/McpSessionScope.cs
index c448c92..0a528e2 100644
--- a/src/McpDotNet.Extensions.AI/McpSessionScope.cs
+++ b/src/McpDotNet.Extensions.AI/McpSessionScope.cs
@@ -46,12 +46,18 @@ public sealed class McpSessionScope : IAsyncDisposable
var scope = new McpSessionScope();
var client = await scope.AddClientAsync(serverConfig, options, loggerFactory).ConfigureAwait(false);
- var tools = await client.ListToolsAsync().ConfigureAwait(false);
- scope.Tools = tools.Tools.Select(t => new McpAIFunction(t, client)).ToList();
+
+ scope.Tools = [];
+ await foreach (var tool in client.ListToolsAsync().ConfigureAwait(false))
+ {
+ scope.Tools.Add(new McpAIFunction(tool, client));
+ }
+
if (!string.IsNullOrEmpty(client.ServerInstructions))
{
scope.ServerInstructions = [client.ServerInstructions!];
}
+
return scope;
}
@@ -72,13 +78,20 @@ public sealed class McpSessionScope : IAsyncDisposable
}
var scope = new McpSessionScope();
+
foreach (var config in serverConfigs)
{
var client = await scope.AddClientAsync(config, options, loggerFactory).ConfigureAwait(false);
- var tools = await client.ListToolsAsync().ConfigureAwait(false);
- scope.Tools = scope.Tools.Concat(tools.Tools.Select(t => new McpAIFunction(t, client))).ToList();
+
+ scope.Tools ??= [];
+ await foreach (var tool in client.ListToolsAsync().ConfigureAwait(false))
+ {
+ scope.Tools.Add(new McpAIFunction(tool, client));
+ }
}
+
scope.ServerInstructions = scope._clients.Select(c => c.ServerInstructions).Where(s => !string.IsNullOrEmpty(s)).ToList()!;
+
return scope;
}
diff --git a/src/mcpdotnet/Client/IMcpClient.cs b/src/mcpdotnet/Client/IMcpClient.cs
index 8a42a8f..0d7442f 100644
--- a/src/mcpdotnet/Client/IMcpClient.cs
+++ b/src/mcpdotnet/Client/IMcpClient.cs
@@ -4,9 +4,7 @@ using McpDotNet.Protocol.Types;
namespace McpDotNet.Client;
///
-/// Represents an instance of the MCP client connecting to a specific server.
-/// Use PingAsync, ListToolsAsync and CallToolAsync instead of SendRequestAsync for common operations.
-/// SendRequestAsync is provided for custom request and those not yet covered explicitly by the client.
+/// Represents an instance of an MCP client connecting to a specific server.
///
public interface IMcpClient : IAsyncDisposable
{
@@ -32,15 +30,35 @@ public interface IMcpClient : IAsyncDisposable
///
string? ServerInstructions { get; }
- ///
- /// Gets or sets the handler for server sampling requests.
- ///
- Func>? SamplingHandler { get; set; }
+ /// Sets a handler for the named operation.
+ /// The name of the operation.
+ /// The handler. Each operation requires a specific delegate signature.
+ ///
+ ///
+ /// Each operation may have only a single handler. Setting a handler for an operation that already has one
+ /// will replace the existing handler.
+ ///
+ ///
+ /// provides constants for common operations.
+ ///
+ ///
+ void SetOperationHandler(string operationName, Delegate handler);
///
- /// Gets or sets the handler for providing root URIs to servers.
+ /// Adds a handler for server notifications of a specific method.
///
- Func>? RootsHandler { get; set; }
+ /// The notification method to handle.
+ /// The async handler function to process notifications.
+ ///
+ ///
+ /// Each method may have multiple handlers. Adding a handler for a method that already has one
+ /// will not replace the existing handler.
+ ///
+ ///
+ /// provides constants for common notification methods.
+ ///
+ ///
+ void AddNotificationHandler(string method, Func handler);
///
/// Establishes a connection to the server.
@@ -49,121 +67,23 @@ public interface IMcpClient : IAsyncDisposable
/// A task representing the asynchronous operation.
Task ConnectAsync(CancellationToken cancellationToken = default);
- ///
- /// Sends a ping request to verify server connectivity.
- ///
- /// A token to cancel the operation.
- /// A task that completes when the ping is successful.
- Task PingAsync(CancellationToken cancellationToken = default);
-
- ///
- /// Retrieves a list of available tools from the server.
- ///
- /// An optional cursor to paginate the results.
- /// A token to cancel the operation.
- /// A task containing the server's response with tool information.
- Task ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default);
-
- ///
- /// Invokes a tool on the server with optional arguments.
- ///
- /// The name of the tool to call.
- /// Optional arguments for the tool.
- /// A token to cancel the operation.
- /// A task containing the tool's response.
- Task CallToolAsync(string toolName, Dictionary arguments, CancellationToken cancellationToken = default);
-
- ///
- /// Retrieves a list of available prompts from the server.
- ///
- /// An optional cursor to paginate the results.
- /// A token to cancel the operation.
- /// A task containing the server's response with prompt information.
- Task ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default);
-
- ///
- /// Retrieves a specific prompt with optional arguments.
- ///
- /// The name of the prompt to retrieve
- /// Optional arguments for the prompt
- /// A token to cancel the operation.
- /// A task containing the prompt's content and messages.
- Task GetPromptAsync(string name, Dictionary? arguments = null, CancellationToken cancellationToken = default);
-
- ///
- /// Retrieves a list of available resources from the server.
- ///
- /// An optional cursor to paginate the results.
- /// A token to cancel the operation.
- ///
- Task ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default);
-
- ///
- /// Reads a resource from the server.
- ///
- /// The uri of the resource.
- /// A token to cancel the operation.
- ///
- Task ReadResourceAsync(string uri, CancellationToken cancellationToken = default);
-
- ///
- /// Gets the completion options for a resource or prompt reference and (named) argument.
- ///
- /// A resource (uri) or prompt (name) reference
- /// Name of argument. Must be non-null and non-empty.
- /// Value of argument. Must be non-null.
- /// A token to cancel the operation.
- ///
- Task GetCompletionAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default);
-
- ///
- /// Subscribes to a resource on the server.
- ///
- /// The uri of the resource.
- /// A token to cancel the operation.
- ///
- Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default);
-
- ///
- /// Unsubscribes from a resource on the server.
- ///
- /// The uri of the resource.
- /// A token to cancel the operation.
- ///
- Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default);
-
///
/// Sends a generic JSON-RPC request to the server.
- /// It is strongly recommended use the capability-specific methods instead of this one.
- /// Use this method for custom requests or those not yet covered explicitly by the client.
///
- /// The expected response type.
+ /// The expected response type.
/// The JSON-RPC request to send.
/// A token to cancel the operation.
/// A task containing the server's response.
- Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where T : class;
+ ///
+ /// It is recommended to use the capability-specific methods that use this one in their implementation.
+ /// Use this method for custom requests or those not yet covered explicitly.
+ ///
+ Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
///
- /// Registers a handler for server notifications of a specific method.
- ///
- /// Constants for common notification methods
+ /// Sends a message to the server.
///
- /// The notification method to handle.
- /// The async handler function to process notifications.
- void OnNotification(string method, Func handler);
-
- ///
- /// Sends a notification to the server.
- ///
- /// The notification method name.
+ /// The message.
/// A token to cancel the operation.
- Task SendNotificationAsync(string method, CancellationToken cancellationToken = default);
-
- ///
- /// Sends a notification to the server with parameters.
- ///
- /// The notification method name.
- /// The parameters to send with the notification.
- /// A token to cancel the operation.
- Task SendNotificationAsync(string method, T parameters, CancellationToken cancellationToken = default);
+ Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
}
\ No newline at end of file
diff --git a/src/mcpdotnet/Client/McpClient.cs b/src/mcpdotnet/Client/McpClient.cs
index acf6036..c7e4e24 100644
--- a/src/mcpdotnet/Client/McpClient.cs
+++ b/src/mcpdotnet/Client/McpClient.cs
@@ -1,5 +1,4 @@
-
-using System.Text.Json;
+using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Logging;
using McpDotNet.Protocol.Messages;
@@ -8,25 +7,22 @@ using McpDotNet.Protocol.Types;
using McpDotNet.Shared;
using Microsoft.Extensions.Logging;
+#pragma warning disable CA1508 // Avoid dead conditional code
+
namespace McpDotNet.Client;
///
-internal class McpClient : McpJsonRpcEndpoint, IMcpClient
+internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
{
private readonly McpClientOptions _options;
private readonly McpServerConfig _serverConfig;
private readonly ILogger _logger;
- private volatile bool _isInitializing;
private readonly IClientTransport _clientTransport;
+
+ private volatile bool _isInitializing;
- ///
- public ServerCapabilities? ServerCapabilities { get; private set; }
-
- ///
- public Implementation? ServerInfo { get; private set; }
-
- ///
- public string? ServerInstructions { get; private set; }
+ private Func>? _samplingHandler;
+ private Func>? _rootsHandler;
///
/// Initializes a new instance of the class.
@@ -43,53 +39,90 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
_logger = loggerFactory.CreateLogger();
_clientTransport = transport;
- if (options.Capabilities?.Sampling != null)
- {
- SetRequestHandler("sampling/createMessage",
- async (request) =>
- {
- if (SamplingHandler == null)
- {
- // Setting the capability, but not a handler means we have nothing to return to the server
- _logger.SamplingHandlerNotConfigured(EndpointName);
- throw new McpClientException("Sampling handler not configured");
- }
+ EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
- return await SamplingHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
- });
- }
- if (options.Capabilities?.Roots != null)
+ if (options.Capabilities?.Sampling is not null)
{
- SetRequestHandler("roots/list",
- async (request) =>
+ SetRequestHandler(
+ "sampling/createMessage", request => HandleRequest("Sampling", _samplingHandler, request));
+ }
+
+ if (options.Capabilities?.Roots is not null)
+ {
+ SetRequestHandler(
+ "roots/list", request => HandleRequest("Roots", _rootsHandler, request));
+ }
+ }
+
+ ///
+ public ServerCapabilities? ServerCapabilities { get; private set; }
+
+ ///
+ public Implementation? ServerInfo { get; private set; }
+
+ ///
+ public string? ServerInstructions { get; private set; }
+
+ ///
+ public override string EndpointName { get; }
+
+ public void SetOperationHandler(string operationName, Delegate handler)
+ {
+ if (operationName is null)
+ {
+ throw new ArgumentNullException(nameof(operationName));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ if (!TrySetOperationHandler(OperationNames.Sampling, operationName, handler, ref _samplingHandler) &&
+ !TrySetOperationHandler(OperationNames.Roots, operationName, handler, ref _rootsHandler))
+ {
+ throw new ArgumentException($"Unknown operation '{operationName}'", nameof(operationName));
+ }
+
+ static bool TrySetOperationHandler(
+ string targetOperationName,
+ string operationName,
+ Delegate handler,
+ ref Func>? field)
+ {
+ if (operationName == targetOperationName)
+ {
+ if (handler is Func> typed)
{
- if (RootsHandler == null)
- {
- // Setting the capability, but not a handler means we have nothing to return to the server
- _logger.RootsHandlerNotConfigured(EndpointName);
- throw new McpClientException("Roots handler not configured");
- }
- return await RootsHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
- });
+ field = typed;
+ return true;
+ }
+
+ throw new ArgumentException(
+ $"Handler must be of type {typeof(Func>)}",
+ nameof(handler));
+ }
+
+ return false;
}
}
///
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
- if (_isInitializing)
- {
- _logger.ClientAlreadyInitializing(EndpointName);
- throw new InvalidOperationException("Client is already initializing");
- }
- _isInitializing = true;
-
if (IsInitialized)
{
_logger.ClientAlreadyInitialized(EndpointName);
return;
}
+ if (_isInitializing)
+ {
+ _logger.ClientAlreadyInitializing(EndpointName);
+ throw new InvalidOperationException("Client is already initializing");
+ }
+
+ _isInitializing = true;
try
{
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
@@ -136,8 +169,7 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
clientInfo = _options.ClientInfo
}
},
- initializationCts.Token
- ).ConfigureAwait(false);
+ initializationCts.Token).ConfigureAwait(false);
// Store server information
_logger.ServerCapabilitiesReceived(EndpointName, JsonSerializer.Serialize(initializeResponse.Capabilities), JsonSerializer.Serialize(initializeResponse.ServerInfo));
@@ -153,13 +185,9 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
}
// Send initialized notification
- await SendNotificationAsync(
- new JsonRpcNotification
- {
- Method = "notifications/initialized"
- },
- initializationCts.Token
- ).ConfigureAwait(false);
+ await SendMessageAsync(
+ new JsonRpcNotification { Method = "notifications/initialized" },
+ initializationCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (initializationCts.IsCancellationRequested)
{
@@ -168,202 +196,18 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
}
}
- ///
- public async Task PingAsync(CancellationToken cancellationToken = default)
+ private async Task HandleRequest(
+ string friendlyName,
+ Func>? handler,
+ TRequest args)
{
- _logger.PingingServer(EndpointName);
- await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "ping"
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default)
- {
- _logger.ListingTools(EndpointName, cursor ?? "(null)");
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "tools/list",
- Params = cursor != null ? new Dictionary { ["cursor"] = cursor } : null
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default)
- {
- _logger.ListingPrompts(EndpointName, cursor ?? "(null)");
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "prompts/list",
- Params = cursor != null ? new Dictionary { ["cursor"] = cursor } : null
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task GetPromptAsync(string name, Dictionary? arguments = null, CancellationToken cancellationToken = default)
- {
- _logger.GettingPrompt(EndpointName, name, arguments == null ? "{}" : JsonSerializer.Serialize(arguments));
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "prompts/get",
- Params = CreateParametersDictionary(name, arguments)
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default)
- {
- _logger.ListingResources(EndpointName, cursor ?? "(null)");
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "resources/list",
- Params = cursor != null ? new Dictionary { ["cursor"] = cursor } : null
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task ReadResourceAsync(string uri, CancellationToken cancellationToken = default)
- {
- _logger.ReadingResource(EndpointName, uri);
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "resources/read",
- Params = new Dictionary
- {
- ["uri"] = uri
- }
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task GetCompletionAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
- {
- if (!reference.Validate(out string validationMessage))
+ if (handler is not null)
{
- _logger.InvalidCompletionReference(EndpointName, reference.ToString(), validationMessage);
- throw new McpClientException($"Invalid reference: {validationMessage}");
- }
- if (string.IsNullOrWhiteSpace(argumentName))
- {
- _logger.InvalidCompletionArgumentName(EndpointName, argumentName);
- throw new McpClientException("Argument name cannot be null or empty");
- }
- if (argumentValue is null)
- {
- _logger.InvalidCompletionArgumentValue(EndpointName, "null", argumentName);
- throw new McpClientException("Argument value cannot be null");
+ return await handler(args, CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
}
- _logger.GettingCompletion(EndpointName, reference.ToString(), argumentName, argumentValue);
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "completion/complete",
- Params = new Dictionary
- {
- ["ref"] = reference,
- ["argument"] = new Argument { Name = argumentName, Value = argumentValue }
- }
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default)
- {
- _logger.SubscribingToResource(EndpointName, uri);
- await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "resources/subscribe",
- Params = new Dictionary
- {
- ["uri"] = uri
- }
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default)
- {
- _logger.UnsubscribingFromResource(EndpointName, uri);
- await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "resources/unsubscribe",
- Params = new Dictionary
- {
- ["uri"] = uri
- }
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task CallToolAsync(string toolName, Dictionary arguments,
- CancellationToken cancellationToken = default)
- {
- _logger.CallingTool(EndpointName, toolName, JsonSerializer.Serialize(arguments));
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "tools/call",
- Params = CreateParametersDictionary(toolName, arguments ?? [])
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? arguments)
- {
- var parameters = new Dictionary
- {
- ["name"] = nameParameter
- };
-
- if (arguments != null)
- {
- parameters["arguments"] = arguments;
- }
-
- return parameters;
- }
-
- ///
- public Func>? SamplingHandler { get; set; }
-
- ///
- public Func>? RootsHandler { get; set; }
-
- ///
- public override string EndpointName
- {
- get
- {
- return $"Client ({_serverConfig.Id}: {_serverConfig.Name})";
- }
+ // Setting the capability, but not a handler means we have nothing to return to the server
+ _logger.HandlerNotConfigured(friendlyName, EndpointName);
+ throw new McpClientException($"{friendlyName} handler not configured.");
}
}
diff --git a/src/mcpdotnet/Client/McpClientExtensions.cs b/src/mcpdotnet/Client/McpClientExtensions.cs
new file mode 100644
index 0000000..6b8a93a
--- /dev/null
+++ b/src/mcpdotnet/Client/McpClientExtensions.cs
@@ -0,0 +1,361 @@
+using McpDotNet.Protocol.Messages;
+using McpDotNet.Protocol.Types;
+using Microsoft.Extensions.Logging;
+using System.Runtime.CompilerServices;
+using System.Xml.XPath;
+
+namespace McpDotNet.Client;
+
+///
+/// Provides extensions for operating on MCP clients.
+///
+public static class McpClientExtensions
+{
+ ///
+ /// Sends a notification to the server with parameters.
+ ///
+ /// The client.
+ /// The notification method name.
+ /// The parameters to send with the notification.
+ /// A token to cancel the operation.
+ public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendMessageAsync(
+ new JsonRpcNotification { Method = method, Params = parameters },
+ cancellationToken);
+ }
+
+ ///
+ /// Sends a ping request to verify server connectivity.
+ ///
+ /// The client.
+ /// A token to cancel the operation.
+ /// A task that completes when the ping is successful.
+ public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("ping", null),
+ cancellationToken);
+ }
+
+ ///
+ /// Retrieves a sequence of available tools from the server.
+ ///
+ /// The client.
+ /// A token to cancel the operation.
+ /// An asynchronous sequence of tool information.
+ public static async IAsyncEnumerable ListToolsAsync(
+ this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ string? cursor = null;
+ do
+ {
+ var tools = await ListToolsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
+ foreach (var tool in tools.Tools)
+ {
+ yield return tool;
+ }
+
+ cursor = tools.NextCursor;
+ }
+ while (cursor is not null);
+ }
+
+ ///
+ /// Retrieves a sequence of available tools from the server.
+ ///
+ /// The client.
+ /// A cursor to paginate the results.
+ /// A token to cancel the operation.
+ /// A task containing the server's response with tool information.
+ public static Task ListToolsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("tools/list", CreateCursorDictionary(cursor)),
+ cancellationToken);
+ }
+
+ ///
+ /// Retrieves a list of available prompts from the server.
+ ///
+ /// The client.
+ /// A token to cancel the operation.
+ /// An asynchronous sequence of prompt information.
+ public static async IAsyncEnumerable ListPromptsAsync(
+ this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ string? cursor = null;
+ do
+ {
+ var prompts = await ListPromptsAsync(client, cursor, cancellationToken).ConfigureAwait(false);
+ foreach (var prompt in prompts.Prompts)
+ {
+ yield return prompt;
+ }
+
+ cursor = prompts.NextCursor;
+ }
+ while (cursor is not null);
+ }
+
+ ///
+ /// Retrieves a list of available prompts from the server.
+ ///
+ /// The client.
+ /// A cursor to paginate the results.
+ /// A token to cancel the operation.
+ /// A task containing the server's response with prompt information.
+ public static Task ListPromptsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
+ cancellationToken);
+ }
+
+ ///
+ /// Retrieves a specific prompt with optional arguments.
+ ///
+ /// The client.
+ /// The name of the prompt to retrieve
+ /// Optional arguments for the prompt
+ /// A token to cancel the operation.
+ /// A task containing the prompt's content and messages.
+ public static Task GetPromptAsync(this IMcpClient client, string name, Dictionary? arguments = null, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
+ cancellationToken);
+ }
+
+ ///
+ /// Retrieves a sequence of available resources from the server.
+ ///
+ /// The client.
+ /// A token to cancel the operation.
+ /// An asynchronous sequence of resource information.
+ public static async IAsyncEnumerable ListResourcesAsync(
+ this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ string? cursor = null;
+ do
+ {
+ var resources = await ListResourcesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
+ foreach (var resource in resources.Resources)
+ {
+ yield return resource;
+ }
+
+ cursor = resources.NextCursor;
+ }
+ while (cursor is not null);
+ }
+
+ ///
+ /// Retrieves a list of available resources from the server.
+ ///
+ /// The client.
+ /// A cursor to paginate the results.
+ /// A token to cancel the operation.
+ public static Task ListResourcesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("resources/list", CreateCursorDictionary(cursor)),
+ cancellationToken);
+ }
+
+ ///
+ /// Reads a resource from the server.
+ ///
+ /// The client.
+ /// The uri of the resource.
+ /// A token to cancel the operation.
+ public static Task ReadResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("resources/read", new() { ["uri"] = uri }),
+ cancellationToken);
+ }
+
+ ///
+ /// Gets the completion options for a resource or prompt reference and (named) argument.
+ ///
+ /// The client.
+ /// A resource (uri) or prompt (name) reference
+ /// Name of argument. Must be non-null and non-empty.
+ /// Value of argument. Must be non-null.
+ /// A token to cancel the operation.
+ public static Task GetCompletionAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ if (reference is null)
+ {
+ throw new ArgumentNullException(nameof(reference));
+ }
+
+ if (string.IsNullOrWhiteSpace(argumentName))
+ {
+ throw argumentName is null ?
+ new ArgumentNullException(nameof(argumentName)) :
+ new ArgumentException("Argument name cannot be empty.", nameof(argumentName));
+ }
+
+ if (!reference.Validate(out string? validationMessage))
+ {
+ throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("completion/complete", new()
+ {
+ ["ref"] = reference,
+ ["argument"] = new Argument { Name = argumentName, Value = argumentValue }
+ }),
+ cancellationToken);
+ }
+
+ ///
+ /// Subscribes to a resource on the server.
+ ///
+ /// The client.
+ /// The uri of the resource.
+ /// A token to cancel the operation.
+ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
+ cancellationToken);
+ }
+
+ ///
+ /// Unsubscribes from a resource on the server.
+ ///
+ /// The client.
+ /// The uri of the resource.
+ /// A token to cancel the operation.
+ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
+ cancellationToken);
+ }
+
+ ///
+ /// Invokes a tool on the server with optional arguments.
+ ///
+ /// The client.
+ /// The name of the tool to call.
+ /// Optional arguments for the tool.
+ /// A token to cancel the operation.
+ /// A task containing the tool's response.
+ public static Task CallToolAsync(this IMcpClient client, string toolName, Dictionary arguments, CancellationToken cancellationToken = default)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return client.SendRequestAsync(
+ CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
+ cancellationToken);
+ }
+
+ /// Sets the handler for server sampling requests.
+ /// The client.
+ /// The sampling request handler.
+ public static void SetSamplingHandler(this IMcpClient client, Func> handler)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ client.SetOperationHandler(OperationNames.Sampling, handler);
+ }
+
+ /// Sets the handler for server roots requests.
+ /// The client.
+ /// The roots request handler.
+ public static void SetRootsHandler(this IMcpClient client, Func> handler)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ client.SetOperationHandler(OperationNames.Roots, handler);
+ }
+
+ private static JsonRpcRequest CreateRequest(string method, Dictionary? parameters) =>
+ new JsonRpcRequest
+ {
+ Method = method,
+ Params = parameters
+ };
+
+ private static Dictionary? CreateCursorDictionary(string? cursor) =>
+ cursor != null ? new() { ["cursor"] = cursor } : null;
+
+ private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? arguments)
+ {
+ Dictionary parameters = new()
+ {
+ ["name"] = nameParameter
+ };
+
+ if (arguments != null)
+ {
+ parameters["arguments"] = arguments;
+ }
+
+ return parameters;
+ }
+}
\ No newline at end of file
diff --git a/src/mcpdotnet/Logging/Log.cs b/src/mcpdotnet/Logging/Log.cs
index c00c959..551c5aa 100644
--- a/src/mcpdotnet/Logging/Log.cs
+++ b/src/mcpdotnet/Logging/Log.cs
@@ -41,11 +41,8 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Debug, Message = "Initializing stdio commands")]
internal static partial void InitializingStdioCommands(this ILogger logger);
- [LoggerMessage(Level = LogLevel.Error, Message = "Sampling handler not configured for server {endpointName}, always set a handler when using this capability")]
- internal static partial void SamplingHandlerNotConfigured(this ILogger logger, string endpointName);
-
- [LoggerMessage(Level = LogLevel.Error, Message = "Roots handler not configured for server {endpointName}, always set a handler when using this capability")]
- internal static partial void RootsHandlerNotConfigured(this ILogger logger, string endpointName);
+ [LoggerMessage(Level = LogLevel.Error, Message = "{handlerName} handler not configured for server {endpointName}, always set a handler when using this capability")]
+ internal static partial void HandlerNotConfigured(this ILogger logger, string handlerName, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "List tools handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ListToolsHandlerNotConfigured(this ILogger logger, string endpointName);
@@ -266,8 +263,8 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaned up for {endpointName}")]
internal static partial void TransportCleanedUp(this ILogger logger, string endpointName);
- [LoggerMessage(Level = LogLevel.Debug, Message = "Sending notification payload for {endpointName}: {payload}")]
- internal static partial void SendingNotificationPayload(this ILogger logger, string endpointName, string payload);
+ [LoggerMessage(Level = LogLevel.Debug, Message = "Sending message to {endpointName}: {message}")]
+ internal static partial void SendingMessage(this ILogger logger, string endpointName, string message);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending notification for {endpointName}: {method}")]
internal static partial void SendingNotification(this ILogger logger, string endpointName, string method);
diff --git a/src/mcpdotnet/Protocol/Messages/OperationNames.cs b/src/mcpdotnet/Protocol/Messages/OperationNames.cs
new file mode 100644
index 0000000..f8f3bcc
--- /dev/null
+++ b/src/mcpdotnet/Protocol/Messages/OperationNames.cs
@@ -0,0 +1,47 @@
+using McpDotNet.Protocol.Messages;
+using McpDotNet.Protocol.Types;
+using Microsoft.Extensions.Logging;
+
+namespace McpDotNet.Protocol.Messages;
+
+/// Provides names of standard operations for use with registering handlers.
+///
+/// These values should not be inspected or relied on for their exact values.
+/// They serve only as opaque keys. They will be stable for the lifetime of a process
+/// but may change between versions of this library.
+///
+public static class OperationNames
+{
+ /// Gets the name of the sampling operation.
+ public static string Sampling { get; } = "operation/sampling";
+
+ /// Gets the name of the roots operation.
+ public static string Roots { get; } = "operation/roots";
+
+ /// Gets the name of the list tools operation.
+ public static string ListTools { get; } = "operation/listTools";
+
+ /// Gets the name of the call tool operation.
+ public static string CallTool { get; } = "operation/callTool";
+
+ /// Gets the name of the list prompts operation.
+ public static string ListPrompts { get; } = "operation/listPrompts";
+
+ /// Gets the name of the get prompt operation.
+ public static string GetPrompt { get; } = "operation/getPrompt";
+
+ /// Gets the name of the list resources operation.
+ public static string ListResources { get; } = "operation/listResources";
+
+ /// Gets the name of the read resource operation.
+ public static string ReadResource { get; } = "operation/readResource";
+
+ /// Gets the name of the get completion operation.
+ public static string GetCompletion { get; } = "operation/getCompletion";
+
+ /// Gets the name of the subscribe to resources operation.
+ public static string SubscribeToResources { get; } = "operation/subscribeToResources";
+
+ /// Gets the name of the subscribe to resources operation.
+ public static string UnsubscribeFromResources { get; } = "operation/unsubscribeFromResources";
+}
\ No newline at end of file
diff --git a/src/mcpdotnet/Protocol/Types/Reference.cs b/src/mcpdotnet/Protocol/Types/Reference.cs
index 46669cb..d313db4 100644
--- a/src/mcpdotnet/Protocol/Types/Reference.cs
+++ b/src/mcpdotnet/Protocol/Types/Reference.cs
@@ -1,4 +1,5 @@
-using System.Text.Json.Serialization;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Types;
@@ -37,7 +38,7 @@ public class Reference
///
/// Validates the reference object.
///
- public bool Validate(out string validationMessage)
+ public bool Validate([NotNullWhen(false)] out string? validationMessage)
{
if (Type == "ref/resource")
{
@@ -61,7 +62,7 @@ public class Reference
return false;
}
- validationMessage = string.Empty;
+ validationMessage = null;
return true;
}
}
diff --git a/src/mcpdotnet/Server/IMcpServer.cs b/src/mcpdotnet/Server/IMcpServer.cs
index d8cd4c5..8672701 100644
--- a/src/mcpdotnet/Server/IMcpServer.cs
+++ b/src/mcpdotnet/Server/IMcpServer.cs
@@ -28,67 +28,41 @@ public interface IMcpServer : IAsyncDisposable
///
IServiceProvider? ServiceProvider { get; }
+ /// Sets a handler for the named operation.
+ /// The name of the operation.
+ /// The handler. Each operation requires a specific delegate signature.
+ ///
+ ///
+ /// Each operation may have only a single handler. Setting a handler for an operation that already has one
+ /// will replace the existing handler.
+ ///
+ ///
+ /// provides constants for common operations.
+ ///
+ ///
+ void SetOperationHandler(string operationName, Delegate handler);
+
+ ///
+ /// Adds a handler for client notifications of a specific method.
+ ///
+ /// The notification method to handle.
+ /// The async handler function to process notifications.
+ ///
+ ///
+ /// Each method may have multiple handlers. Adding a handler for a method that already has one
+ /// will not replace the existing handler.
+ ///
+ ///
+ /// provides constants for common notification methods.
+ ///
+ ///
+ void AddNotificationHandler(string method, Func handler);
+
///
/// Starts the server and begins listening for client requests.
///
Task StartAsync(CancellationToken cancellationToken = default);
- ///
- /// Gets or sets the handler for list tools requests.
- ///
- Func, CancellationToken, Task>? ListToolsHandler { get; set; }
-
- ///
- /// Gets or sets the handler for call tool requests.
- ///
- Func, CancellationToken, Task>? CallToolHandler { get; set; }
-
- ///
- /// Gets or sets the handler for list prompts requests.
- ///
- Func, CancellationToken, Task>? ListPromptsHandler { get; set; }
-
- ///
- /// Gets or sets the handler for get prompt requests.
- ///
- Func, CancellationToken, Task>? GetPromptHandler { get; set; }
-
- ///
- /// Gets or sets the handler for list resources requests.
- ///
- Func, CancellationToken, Task>? ListResourcesHandler { get; set; }
-
- ///
- /// Gets or sets the handler for read resources requests.
- ///
- Func, CancellationToken, Task>? ReadResourceHandler { get; set; }
-
- ///
- /// Gets or sets the handler for get resources requests.
- ///
- Func, CancellationToken, Task>? GetCompletionHandler { get; set; }
-
- ///
- /// Gets or sets the handler for subscribe to resources messages.
- ///
- Func, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
-
- ///
- /// Gets or sets the handler for subscribe to resources messages.
- ///
- Func, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
-
-
- ///
- /// Requests the client to create a new message.
- ///
- Task RequestSamplingAsync(CreateMessageRequestParams request, CancellationToken cancellationToken);
-
- ///
- /// Requests the client to list the roots it exposes.
- ///
- Task RequestRootsAsync(ListRootsRequestParams request, CancellationToken cancellationToken);
-
///
/// Sends a generic JSON-RPC request to the client.
/// NB! This is a temporary method that is available to send not yet implemented feature messages.
@@ -101,26 +75,9 @@ public interface IMcpServer : IAsyncDisposable
Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where T : class;
///
- /// Registers a handler for notifications of a specific method.
- ///
- /// Constants for common notification methods
+ /// Sends a message to the server.
///
- /// The notification method to handle.
- /// The async handler function to process notifications.
- void OnNotification(string method, Func handler);
-
- ///
- /// Sends a notification to the client.
- ///
- /// The notification method name.
+ /// The message.
/// A token to cancel the operation.
- Task SendNotificationAsync(string method, CancellationToken cancellationToken = default);
-
- ///
- /// Sends a notification to the client with parameters.
- ///
- /// The notification method name.
- /// The parameters to send with the notification.
- /// A token to cancel the operation.
- Task SendNotificationAsync(string method, T parameters, CancellationToken cancellationToken = default);
+ Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
}
diff --git a/src/mcpdotnet/Server/McpServer.cs b/src/mcpdotnet/Server/McpServer.cs
index 4516617..2c525c4 100644
--- a/src/mcpdotnet/Server/McpServer.cs
+++ b/src/mcpdotnet/Server/McpServer.cs
@@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
namespace McpDotNet.Server;
///
-internal class McpServer : McpJsonRpcEndpoint, IMcpServer
+internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
{
private readonly IServerTransport _serverTransport;
private readonly McpServerOptions _options;
private volatile bool _isInitializing;
private readonly ILogger _logger;
+ private Func, CancellationToken, Task>? _listToolsHandler;
+ private Func, CancellationToken, Task>? _callToolHandler;
+ private Func, CancellationToken, Task>? _listPromptsHandler;
+ private Func, CancellationToken, Task>? _getPromptHandler;
+ private Func, CancellationToken, Task>? _listResourcesHandler;
+ private Func, CancellationToken, Task>? _readResourceHandler;
+ private Func, CancellationToken, Task>? _getCompletionHandler;
+ private Func, CancellationToken, Task>? _subscribeToResourcesHandler;
+ private Func, CancellationToken, Task>? _unsubscribeFromResourcesHandler;
+
///
/// Creates a new instance of .
///
@@ -41,7 +51,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
SetCompletionHandler();
SetInitializeHandler(options);
SetPingHandler();
- SetNotificationHandler();
+ AddNotificationHandler();
}
public ClientCapabilities? ClientCapabilities { get; set; }
@@ -56,38 +66,54 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
public IServiceProvider? ServiceProvider { get; }
///
- public Func, CancellationToken, Task>? ListToolsHandler { get; set; }
+ public override string EndpointName =>
+ $"Server ({_options.ServerInfo.Name} {_options.ServerInfo.Version}), Client ({ClientInfo?.Name} {ClientInfo?.Version})";
- ///
- public Func, CancellationToken, Task>? CallToolHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? ListPromptsHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? GetPromptHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? ListResourcesHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? ReadResourceHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? GetCompletionHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
-
- ///
- public Func, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
-
- ///
- public override string EndpointName
+ public void SetOperationHandler(string operationName, Delegate handler)
{
- get
+ if (operationName is null)
{
- return $"Server ({_options.ServerInfo.Name} {_options.ServerInfo.Version}), Client ({ClientInfo?.Name} {ClientInfo?.Version})";
+ throw new ArgumentNullException(nameof(operationName));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ if (!TrySetOperationHandler(OperationNames.ListTools, operationName, handler, ref _listToolsHandler) &&
+ !TrySetOperationHandler(OperationNames.CallTool, operationName, handler, ref _callToolHandler) &&
+ !TrySetOperationHandler(OperationNames.ListPrompts, operationName, handler, ref _listPromptsHandler) &&
+ !TrySetOperationHandler(OperationNames.GetPrompt, operationName, handler, ref _getPromptHandler) &&
+ !TrySetOperationHandler(OperationNames.ListResources, operationName, handler, ref _listResourcesHandler) &&
+ !TrySetOperationHandler(OperationNames.ReadResource, operationName, handler, ref _readResourceHandler) &&
+ !TrySetOperationHandler(OperationNames.GetCompletion, operationName, handler, ref _getCompletionHandler) &&
+ !TrySetOperationHandler(OperationNames.SubscribeToResources, operationName, handler, ref _subscribeToResourcesHandler) &&
+ !TrySetOperationHandler(OperationNames.UnsubscribeFromResources, operationName, handler, ref _unsubscribeFromResourcesHandler))
+ {
+ throw new ArgumentException($"Unknown operation '{operationName}'", nameof(operationName));
+ }
+
+ static bool TrySetOperationHandler(
+ string targetOperationName,
+ string operationName,
+ Delegate handler,
+ ref Func? field)
+ {
+ if (operationName == targetOperationName)
+ {
+ if (handler is Func typed)
+ {
+ field = typed;
+ return true;
+ }
+
+ throw new ArgumentException(
+ $"Handler must be of type {typeof(Func)}",
+ nameof(handler));
+ }
+
+ return false;
}
}
@@ -127,45 +153,9 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
}
}
- ///
- public async Task RequestSamplingAsync(CreateMessageRequestParams request, CancellationToken cancellationToken)
+ private void AddNotificationHandler()
{
- if (ClientCapabilities?.Sampling == null)
- {
- throw new McpServerException("Client does not support sampling");
- }
-
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "sampling/createMessage",
- Params = request
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- ///
- public async Task RequestRootsAsync(ListRootsRequestParams request, CancellationToken cancellationToken)
- {
- if (ClientCapabilities?.Roots == null)
- {
- throw new McpServerException("Client does not support roots");
- }
-
- return await SendRequestAsync(
- new JsonRpcRequest
- {
- Method = "roots/list",
- Params = request
- },
- cancellationToken
- ).ConfigureAwait(false);
- }
-
- private void SetNotificationHandler()
- {
- OnNotification("notifications/initialized", (notification) =>
+ AddNotificationHandler("notifications/initialized", (notification) =>
{
IsInitialized = true;
return Task.CompletedTask;
@@ -174,11 +164,8 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
private void SetPingHandler()
{
- SetRequestHandler("ping",
- (request) =>
- {
- return Task.FromResult(new PingResult());
- });
+ SetRequestHandler("ping",
+ request => Task.FromResult(new PingResult()));
}
private void SetInitializeHandler(McpServerOptions options)
@@ -203,7 +190,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
SetRequestHandler("completion/complete",
async (request) =>
{
- if (GetCompletionHandler == null)
+ if (_getCompletionHandler is null)
{
// This capability is not optional, so return an empty result if there is no handler
return new CompleteResult()
@@ -217,102 +204,102 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
};
}
- return await GetCompletionHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _getCompletionHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
}
private void SetResourcesHandler(McpServerOptions options)
{
- if (options.Capabilities?.Resources != null)
+ if (options.Capabilities?.Resources is not null)
{
SetRequestHandler("resources/list",
async (request) =>
{
- if (ListResourcesHandler == null)
+ if (_listResourcesHandler is null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListResourcesHandlerNotConfigured(EndpointName);
throw new McpServerException("ListResources handler not configured");
}
- return await ListResourcesHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _listResourcesHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
SetRequestHandler("resources/read",
async (request) =>
{
- if (ReadResourceHandler == null)
+ if (_readResourceHandler is null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ReadResourceHandlerNotConfigured(EndpointName);
throw new McpServerException("ReadResource handler not configured");
}
- return await ReadResourceHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _readResourceHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
}
}
private void SetPromptsHandler(McpServerOptions options)
{
- if (options.Capabilities?.Prompts != null)
+ if (options.Capabilities?.Prompts is not null)
{
SetRequestHandler("prompts/list",
async (request) =>
{
- if (ListPromptsHandler == null)
+ if (_listPromptsHandler is null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListPromptsHandlerNotConfigured(EndpointName);
throw new McpServerException("ListPrompts handler not configured");
}
- return await ListPromptsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _listPromptsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
SetRequestHandler("prompts/get",
async (request) =>
{
- if (GetPromptHandler == null)
+ if (_getPromptHandler is null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.GetPromptHandlerNotConfigured(EndpointName);
throw new McpServerException("GetPrompt handler not configured");
}
- return await GetPromptHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _getPromptHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
}
}
private void SetToolsHandler(McpServerOptions options)
{
- if (options.Capabilities?.Tools != null)
+ if (options.Capabilities?.Tools is not null)
{
SetRequestHandler("tools/list",
async (request) =>
{
- if (ListToolsHandler == null)
+ if (_listToolsHandler is null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.ListToolsHandlerNotConfigured(EndpointName);
throw new McpServerException("ListTools handler not configured");
}
- return await ListToolsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _listToolsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
SetRequestHandler("tools/call",
async (request) =>
{
- if (CallToolHandler == null)
+ if (_callToolHandler is null)
{
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.CallToolHandlerNotConfigured(EndpointName);
throw new McpServerException("CallTool handler not configured");
}
- return await CallToolHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
+ return await _callToolHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
});
}
}
diff --git a/src/mcpdotnet/Server/McpServerDelegates.cs b/src/mcpdotnet/Server/McpServerDelegates.cs
index 5c2a539..419eabd 100644
--- a/src/mcpdotnet/Server/McpServerDelegates.cs
+++ b/src/mcpdotnet/Server/McpServerDelegates.cs
@@ -64,30 +64,30 @@ public class McpServerDelegates
}
if (ListToolsHandler != null)
- server.ListToolsHandler = ListToolsHandler;
+ server.SetListToolsHandler(ListToolsHandler);
if (CallToolHandler != null)
- server.CallToolHandler = CallToolHandler;
+ server.SetCallToolHandler(CallToolHandler);
if (ListPromptsHandler != null)
- server.ListPromptsHandler = ListPromptsHandler;
+ server.SetListPromptsHandler(ListPromptsHandler);
if (GetPromptHandler != null)
- server.GetPromptHandler = GetPromptHandler;
+ server.SetGetPromptHandler(GetPromptHandler);
if (ListResourcesHandler != null)
- server.ListResourcesHandler = ListResourcesHandler;
+ server.SetListResourcesHandler(ListResourcesHandler);
if (ReadResourceHandler != null)
- server.ReadResourceHandler = ReadResourceHandler;
+ server.SetReadResourceHandler(ReadResourceHandler);
if (GetCompletionHandler != null)
- server.GetCompletionHandler = GetCompletionHandler;
+ server.SetGetCompletionHandler(GetCompletionHandler);
if (SubscribeToResourcesHandler != null)
- server.SubscribeToResourcesHandler = SubscribeToResourcesHandler;
+ server.SetSubscribeToResourcesHandler(SubscribeToResourcesHandler);
if (UnsubscribeFromResourcesHandler != null)
- server.UnsubscribeFromResourcesHandler = UnsubscribeFromResourcesHandler;
+ server.SetUnsubscribeFromResourcesHandler(UnsubscribeFromResourcesHandler);
}
}
diff --git a/src/mcpdotnet/Server/McpServerExtensions.cs b/src/mcpdotnet/Server/McpServerExtensions.cs
new file mode 100644
index 0000000..5ed5f4c
--- /dev/null
+++ b/src/mcpdotnet/Server/McpServerExtensions.cs
@@ -0,0 +1,221 @@
+using McpDotNet.Protocol.Messages;
+using McpDotNet.Protocol.Types;
+
+namespace McpDotNet.Server;
+
+///
+public static class McpServerExtensions
+{
+ ///
+ /// Requests the client to create a new message.
+ ///
+ public static Task RequestSamplingAsync(
+ this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (server.ClientCapabilities?.Sampling is null)
+ {
+ throw new McpServerException("Client does not support sampling");
+ }
+
+ return server.SendRequestAsync(
+ new JsonRpcRequest { Method = "sampling/createMessage", Params = request },
+ cancellationToken);
+ }
+
+ ///
+ /// Requests the client to list the roots it exposes.
+ ///
+ public static Task RequestRootsAsync(
+ this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (server.ClientCapabilities?.Roots is null)
+ {
+ throw new McpServerException("Client does not support roots");
+ }
+
+ return server.SendRequestAsync(
+ new JsonRpcRequest { Method = "roots/list", Params = request },
+ cancellationToken);
+ }
+
+ ///
+ /// Sets the handler for list tools requests.
+ ///
+ public static void SetListToolsHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.ListTools, handler);
+ }
+
+ ///
+ /// Sets the handler for call tool requests.
+ ///
+ public static void SetCallToolHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.CallTool, handler);
+ }
+
+ ///
+ /// Sets the handler for list prompts requests.
+ ///
+ public static void SetListPromptsHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.ListPrompts, handler);
+ }
+
+ ///
+ /// Sets the handler for get prompt requests.
+ ///
+ public static void SetGetPromptHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.GetPrompt, handler);
+ }
+
+ ///
+ /// Sets the handler for list resources requests.
+ ///
+ public static void SetListResourcesHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.ListResources, handler);
+ }
+
+ ///
+ /// Sets the handler for read resource requests.
+ ///
+ public static void SetReadResourceHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.ReadResource, handler);
+ }
+
+ ///
+ /// Sets the handler for get completion requests.
+ ///
+ public static void SetGetCompletionHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.GetCompletion, handler);
+ }
+
+ ///
+ /// Sets the handler for subscribe to resources requests.
+ ///
+ public static void SetSubscribeToResourcesHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.SubscribeToResources, handler);
+ }
+
+ ///
+ /// Sets the handler for unsubscribe from resources requests.
+ ///
+ public static void SetUnsubscribeFromResourcesHandler(
+ this IMcpServer server, Func, CancellationToken, Task> handler)
+ {
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+
+ if (handler is null)
+ {
+ throw new ArgumentNullException(nameof(handler));
+ }
+
+ server.SetOperationHandler(OperationNames.UnsubscribeFromResources, handler);
+ }
+}
diff --git a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs
index d9a5e19..85eb384 100644
--- a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs
+++ b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs
@@ -34,14 +34,16 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// The logger factory.
protected McpJsonRpcEndpoint(ITransport transport, ILoggerFactory loggerFactory)
{
- _transport = transport ?? throw new ArgumentNullException(nameof(transport));
+ if (transport is null)
+ {
+ throw new ArgumentNullException(nameof(transport));
+ }
+
+ _transport = transport;
_pendingRequests = new();
_notificationHandlers = new();
_nextRequestId = 1;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
-
- if (loggerFactory is null)
- throw new ArgumentNullException(nameof(loggerFactory));
_logger = loggerFactory.CreateLogger();
}
@@ -72,10 +74,28 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
await foreach (var message in _transport.MessageReader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
_logger.TransportMessageRead(EndpointName, message.GetType().Name);
+
// Fire and forget the message handling task to avoid blocking the transport
// If awaiting the task, the transport will not be able to read more messages,
// which could lead to a deadlock if the handler sends a message back
- FireAndForget(Task.Run(() => HandleMessageAsync(message, cancellationToken), CancellationToken.None), message);
+ _ = ProcessMessageAsync();
+ async Task ProcessMessageAsync()
+ {
+#if NET
+ await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
+#else
+ await Task.Yield(); // TODO: Fix this
+#endif
+ try
+ {
+ await HandleMessageAsync(message, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ var payload = JsonSerializer.Serialize(message);
+ _logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex);
+ }
+ }
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
@@ -89,18 +109,6 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
}
}
- private void FireAndForget(Task task, IJsonRpcMessage message)
- {
- task.ContinueWith(t =>
- {
- if (t.IsFaulted)
- {
- var payload = JsonSerializer.Serialize(message);
- _logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, t.Exception);
- }
- }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
- }
-
private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
{
switch (message)
@@ -108,12 +116,15 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
case JsonRpcRequest request:
await HandleRequest(request, cancellationToken).ConfigureAwait(false);
break;
+
case IJsonRpcMessageWithId messageWithId:
HandleMessageWithId(message, messageWithId);
break;
+
case JsonRpcNotification notification:
await HandleNotification(notification).ConfigureAwait(false);
break;
+
default:
_logger.EndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
break;
@@ -142,7 +153,9 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
{
if (!messageWithId.Id.IsValid)
+ {
_logger.RequestHasInvalidId(EndpointName);
+ }
else if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
{
_logger.ResponseMatchedPendingRequest(EndpointName, messageWithId.Id.ToString());
@@ -197,11 +210,11 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// It is strongly recommended use the capability-specific methods instead of this one.
/// Use this method for custom requests or those not yet covered explicitly by the endpoint implementation.
///
- /// The expected response type.
+ /// The expected response type.
/// The JSON-RPC request to send.
/// A token to cancel the operation.
/// A task containing the server's response.
- public async Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where T : class
+ public async Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where TResult : class
{
if (!_transport.IsConnected)
{
@@ -241,7 +254,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
{
// Convert the Result object to JSON and back to get our strongly-typed result
var resultJson = JsonSerializer.Serialize(success.Result, _jsonOptions);
- var resultObject = JsonSerializer.Deserialize(resultJson, _jsonOptions);
+ var resultObject = JsonSerializer.Deserialize(resultJson, _jsonOptions);
// Not expensive logging because we're already converting to JSON in order to get the result object
_logger.RequestResponseReceivedPayload(EndpointName, resultJson);
@@ -253,8 +266,8 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
}
// Result object was null, this is unexpected
- _logger.RequestResponseTypeConversionError(EndpointName, request.Method, typeof(T));
- throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result)}, expected {typeof(T)}");
+ _logger.RequestResponseTypeConversionError(EndpointName, request.Method, typeof(TResult));
+ throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result)}, expected {typeof(TResult)}");
}
// Unexpected response type
@@ -267,73 +280,25 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
}
}
- ///
- /// Sends a notification to the connected endpoint.
- ///
- /// The notification method name.
- /// A token to cancel the operation.
- public async Task SendNotificationAsync(string method, CancellationToken cancellationToken = default)
+ public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
+ if (message is null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
if (!_transport.IsConnected)
{
_logger.ClientNotConnected(EndpointName);
throw new McpClientException("Transport is not connected");
}
- var notification = new JsonRpcNotification { Method = method };
-
- // Log if enabled
if (_logger.IsEnabled(LogLevel.Debug))
- _logger.SendingNotificationPayload(EndpointName, JsonSerializer.Serialize(notification));
-
- // Log basic info
- _logger.SendingNotification(EndpointName, method);
- await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Sends a notification to the connected endpoint with parameters.
- ///
- /// The notification method name.
- /// The parameters to send with the notification.
- /// A token to cancel the operation.
- public async Task SendNotificationAsync(string method, T parameters, CancellationToken cancellationToken = default)
- {
- if (!_transport.IsConnected)
{
- _logger.ClientNotConnected(EndpointName);
- throw new McpClientException("Transport is not connected");
+ _logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message));
}
- var notification = new JsonRpcNotification
- {
- Method = method,
- Params = parameters
- };
-
- // Log if enabled
- if (_logger.IsEnabled(LogLevel.Debug))
- _logger.SendingNotificationPayload(EndpointName, JsonSerializer.Serialize(notification));
-
- // Log basic info
- _logger.SendingNotification(EndpointName, method);
- await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// Sends a notification to the connected endpoint.
- ///
- /// The notification to send.
- /// A token to cancel the operation.
- internal async Task SendNotificationAsync(JsonRpcNotification notification, CancellationToken cancellationToken)
- {
- if (!_transport.IsConnected)
- {
- _logger.ClientNotConnected(EndpointName);
- throw new McpClientException("Transport is not connected");
- }
-
- await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
+ return _transport.SendMessageAsync(message, cancellationToken);
}
///
@@ -343,7 +308,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
///
/// The notification method to handle.
/// The async handler function to process notifications.
- public void OnNotification(string method, Func handler)
+ public void AddNotificationHandler(string method, Func handler)
{
var handlers = _notificationHandlers.GetOrAdd(method, _ => []);
lock (handlers)
@@ -368,6 +333,11 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// Handler to be called when a request with specified method identifier is received
protected void SetRequestHandler(string method, Func> handler)
{
+ if (method is null)
+ {
+ throw new ArgumentNullException(nameof(method));
+ }
+
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
diff --git a/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs b/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs
index 752f3dd..b88d6a8 100644
--- a/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs
+++ b/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs
@@ -48,8 +48,7 @@ public class IntegrationTests
public async Task IntegrateWithMeai_UsingEverythingServer_ToolsAreProperlyCalled()
{
var client = await GetMcpClientAsync();
- var tools = await client.ListToolsAsync();
- var mappedTools = tools.Tools.Select(t => t.ToAITool(client)).ToList();
+ var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();
IChatClient openaiClient = new OpenAIClient(_openAIKey)
.AsChatClient("gpt-4o-mini");
diff --git a/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj b/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj
index 3e21b8e..be1bdbe 100644
--- a/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj
+++ b/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj
@@ -15,6 +15,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/tests/mcpdotnet.TestServer/Program.cs b/tests/mcpdotnet.TestServer/Program.cs
index ef65228..7d77bb2 100644
--- a/tests/mcpdotnet.TestServer/Program.cs
+++ b/tests/mcpdotnet.TestServer/Program.cs
@@ -68,7 +68,7 @@ internal static class Program
private static void ConfigureTools(IMcpServer server)
{
- server.ListToolsHandler = (request, cancellationToken) =>
+ server.SetListToolsHandler((request, cancellationToken) =>
{
return Task.FromResult(new ListToolsResult()
{
@@ -103,9 +103,9 @@ internal static class Program
}
]
});
- };
+ });
- server.CallToolHandler = async (request, cancellationToken) =>
+ server.SetCallToolHandler(async (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
@@ -138,12 +138,12 @@ internal static class Program
{
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
}
- };
+ });
}
private static void ConfigurePrompts(IMcpServer server)
{
- server.ListPromptsHandler = (request, cancellationToken) =>
+ server.SetListPromptsHandler((request, cancellationToken) =>
{
return Task.FromResult(new ListPromptsResult()
{
@@ -175,9 +175,9 @@ internal static class Program
}
]
});
- };
+ });
- server.GetPromptHandler = (request, cancellationToken) =>
+ server.SetGetPromptHandler((request, cancellationToken) =>
{
List messages = new();
if (request.Params?.Name == "simple_prompt")
@@ -234,7 +234,7 @@ internal static class Program
{
Messages = messages
});
- };
+ });
}
private static void ConfigureResources(IMcpServer server)
@@ -279,7 +279,7 @@ internal static class Program
const int pageSize = 10;
- server.ListResourcesHandler = (request, cancellationToken) =>
+ server.SetListResourcesHandler((request, cancellationToken) =>
{
int startIndex = 0;
request ??= new(server, new());
@@ -308,9 +308,9 @@ internal static class Program
NextCursor = nextCursor,
Resources = resources.GetRange(startIndex, endIndex - startIndex)
});
- };
+ });
- server.ReadResourceHandler = (request, cancellationToken) =>
+ server.SetReadResourceHandler((request, cancellationToken) =>
{
if (request.Params?.Uri is null)
{
@@ -323,7 +323,7 @@ internal static class Program
{
Contents = [contents]
});
- };
+ });
}
private static void ConfigureCompletion(IMcpServer server)
@@ -335,32 +335,32 @@ internal static class Program
{"temperature", ["0", "0.5", "0.7", "1.0"]},
};
- server.GetCompletionHandler = (request, cancellationToken) =>
+ server.SetGetCompletionHandler((request, cancellationToken) =>
+ {
+ if (request.Params?.Ref?.Type == "ref/resource")
{
- if (request.Params?.Ref?.Type == "ref/resource")
- {
- var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault();
- if (string.IsNullOrEmpty(resourceId))
- return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
+ var resourceId = request.Params?.Ref?.Uri?.Split('/').LastOrDefault();
+ if (string.IsNullOrEmpty(resourceId))
+ return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
- // Filter resource IDs that start with the input value
- var values = sampleResourceIds.Where(id => id.StartsWith(request.Params.Argument.Value)).ToArray();
- return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
+ // Filter resource IDs that start with the input value
+ var values = sampleResourceIds.Where(id => id.StartsWith(request.Params.Argument.Value)).ToArray();
+ return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
- }
+ }
- if (request.Params?.Ref?.Type == "ref/prompt")
- {
- // Handle completion for prompt arguments
- if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
- return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
+ if (request.Params?.Ref?.Type == "ref/prompt")
+ {
+ // Handle completion for prompt arguments
+ if (!exampleCompletions.TryGetValue(request.Params.Argument.Name, out var completions))
+ return Task.FromResult(new CompleteResult() { Completion = new() { Values = [] } });
- var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
- return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
- }
+ var values = completions.Where(value => value.StartsWith(request.Params.Argument.Value)).ToArray();
+ return Task.FromResult(new CompleteResult() { Completion = new() { Values = values, HasMore = false, Total = values.Length } });
+ }
- throw new McpServerException($"Unknown reference type: {request.Params?.Ref.Type}");
- };
+ throw new McpServerException($"Unknown reference type: {request.Params?.Ref.Type}");
+ });
}
static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100)
diff --git a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs
index a7a584f..cb0a728 100644
--- a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs
+++ b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs
@@ -59,11 +59,10 @@ public class ClientIntegrationTests : IClassFixture p.Name == "simple_prompt");
- Assert.Contains(prompts.Prompts, p => p.Name == "complex_prompt");
+ Assert.Contains(prompts, p => p.Name == "simple_prompt");
+ Assert.Contains(prompts, p => p.Name == "complex_prompt");
}
[Theory]
@@ -265,7 +263,7 @@ public class ClientIntegrationTests : IClassFixture
+ client.SetSamplingHandler((_, _) =>
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
@@ -278,7 +276,7 @@ public class ClientIntegrationTests : IClassFixture
+ // client.SetRootsHandler((request, ct) =>
// {
// rootsHandlerCalls++;
// return Task.FromResult(new ListRootsResult
// {
// Roots = testRoots
// });
- // };
+ // });
// // Connect
// await client.ConnectAsync(CancellationToken.None);
diff --git a/tests/mcpdotnet.Tests/Server/McpServerDelegatesTests.cs b/tests/mcpdotnet.Tests/Server/McpServerDelegatesTests.cs
index 7da27b6..95f5891 100644
--- a/tests/mcpdotnet.Tests/Server/McpServerDelegatesTests.cs
+++ b/tests/mcpdotnet.Tests/Server/McpServerDelegatesTests.cs
@@ -1,6 +1,6 @@
-using McpDotNet.Protocol.Types;
+using McpDotNet.Protocol.Messages;
+using McpDotNet.Protocol.Types;
using McpDotNet.Server;
-using Moq;
namespace McpDotNet.Tests.Server;
@@ -10,8 +10,7 @@ public class McpServerDelegatesTests
public void Applies_All_Given_Delegates()
{
var container = new McpServerDelegates();
- var server = new Mock();
- server.SetupAllProperties();
+ var server = new ExposeSetHandlersServer();
container.ListToolsHandler = (p, c) => Task.FromResult(new ListToolsResult());
container.CallToolHandler = (p, c) => Task.FromResult(new CallToolResponse());
@@ -23,16 +22,33 @@ public class McpServerDelegatesTests
container.SubscribeToResourcesHandler = (s, c) => Task.CompletedTask;
container.UnsubscribeFromResourcesHandler = (s, c) => Task.CompletedTask;
- container.Apply(server.Object);
+ container.Apply(server);
- Assert.Equal(container.ListToolsHandler, server.Object.ListToolsHandler);
- Assert.Equal(container.CallToolHandler, server.Object.CallToolHandler);
- Assert.Equal(container.ListPromptsHandler, server.Object.ListPromptsHandler);
- Assert.Equal(container.GetPromptHandler, server.Object.GetPromptHandler);
- Assert.Equal(container.ListResourcesHandler, server.Object.ListResourcesHandler);
- Assert.Equal(container.ReadResourceHandler, server.Object.ReadResourceHandler);
- Assert.Equal(container.GetCompletionHandler, server.Object.GetCompletionHandler);
- Assert.Equal(container.SubscribeToResourcesHandler, server.Object.SubscribeToResourcesHandler);
- Assert.Equal(container.UnsubscribeFromResourcesHandler, server.Object.UnsubscribeFromResourcesHandler);
+ Assert.Equal(container.ListToolsHandler, server.Handlers[OperationNames.ListTools]);
+ Assert.Equal(container.CallToolHandler, server.Handlers[OperationNames.CallTool]);
+ Assert.Equal(container.ListPromptsHandler, server.Handlers[OperationNames.ListPrompts]);
+ Assert.Equal(container.GetPromptHandler, server.Handlers[OperationNames.GetPrompt]);
+ Assert.Equal(container.ListResourcesHandler, server.Handlers[OperationNames.ListResources]);
+ Assert.Equal(container.ReadResourceHandler, server.Handlers[OperationNames.ReadResource]);
+ Assert.Equal(container.GetCompletionHandler, server.Handlers[OperationNames.GetCompletion]);
+ Assert.Equal(container.SubscribeToResourcesHandler, server.Handlers[OperationNames.SubscribeToResources]);
+ Assert.Equal(container.UnsubscribeFromResourcesHandler, server.Handlers[OperationNames.UnsubscribeFromResources]);
+ }
+
+ private sealed class ExposeSetHandlersServer : IMcpServer
+ {
+ public Dictionary Handlers = [];
+
+ public void SetOperationHandler(string operationName, Delegate handler) => Handlers[operationName] = handler;
+
+ public ValueTask DisposeAsync() => default;
+ public bool IsInitialized => throw new NotImplementedException();
+ public ClientCapabilities? ClientCapabilities => throw new NotImplementedException();
+ public Implementation? ClientInfo => throw new NotImplementedException();
+ public IServiceProvider? ServiceProvider => throw new NotImplementedException();
+ public void AddNotificationHandler(string method, Func handler) => throw new NotImplementedException();
+ public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) => throw new NotImplementedException();
+ public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where T : class => throw new NotImplementedException();
+ public Task StartAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
}
}
diff --git a/tests/mcpdotnet.Tests/Server/McpServerTests.cs b/tests/mcpdotnet.Tests/Server/McpServerTests.cs
index 0bf228b..67d5545 100644
--- a/tests/mcpdotnet.Tests/Server/McpServerTests.cs
+++ b/tests/mcpdotnet.Tests/Server/McpServerTests.cs
@@ -273,7 +273,7 @@ public class McpServerTests
await Can_Handle_Requests(null, "completion/complete",
configureServer: server =>
{
- server.GetCompletionHandler = (request, ct) =>
+ server.SetGetCompletionHandler((request, ct) =>
{
return Task.FromResult(new CompleteResult
{
@@ -284,7 +284,7 @@ public class McpServerTests
HasMore = true
}
});
- };
+ });
},
assertResult: response =>
{
@@ -305,13 +305,13 @@ public class McpServerTests
await Can_Handle_Requests(new ServerCapabilities { Resources = new() }, "resources/list",
configureServer: server =>
{
- server.ListResourcesHandler = (request, ct) =>
+ server.SetListResourcesHandler((request, ct) =>
{
return Task.FromResult(new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
- };
+ });
},
assertResult: response =>
@@ -337,13 +337,13 @@ public class McpServerTests
await Can_Handle_Requests(new ServerCapabilities { Resources = new() }, "resources/read",
configureServer: server =>
{
- server.ReadResourceHandler = (request, ct) =>
+ server.SetReadResourceHandler((request, ct) =>
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new() { Text = "test" }]
});
- };
+ });
},
assertResult: response =>
{
@@ -368,13 +368,13 @@ public class McpServerTests
await Can_Handle_Requests(new ServerCapabilities { Prompts = new() }, "prompts/list",
configureServer: server =>
{
- server.ListPromptsHandler = (request, ct) =>
+ server.SetListPromptsHandler((request, ct) =>
{
return Task.FromResult(new ListPromptsResult
{
Prompts = [new() { Name = "test" }]
});
- };
+ });
},
assertResult: response =>
{
@@ -399,13 +399,13 @@ public class McpServerTests
await Can_Handle_Requests(new ServerCapabilities { Prompts = new() }, "prompts/get",
configureServer: server =>
{
- server.GetPromptHandler = (request, ct) =>
+ server.SetGetPromptHandler((request, ct) =>
{
return Task.FromResult(new GetPromptResult
{
Description = "test"
});
- };
+ });
},
assertResult: response =>
{
@@ -428,13 +428,13 @@ public class McpServerTests
await Can_Handle_Requests(new ServerCapabilities { Tools = new() }, "tools/list",
configureServer: server =>
{
- server.ListToolsHandler = (request, ct) =>
+ server.SetListToolsHandler((request, ct) =>
{
return Task.FromResult(new ListToolsResult
{
Tools = [new() { Name = "test" }]
});
- };
+ });
},
assertResult: response =>
{
@@ -458,13 +458,13 @@ public class McpServerTests
await Can_Handle_Requests(new ServerCapabilities { Tools = new() }, "tools/call",
configureServer: server =>
{
- server.CallToolHandler = (request, ct) =>
+ server.SetCallToolHandler((request, ct) =>
{
return Task.FromResult(new CallToolResponse
{
Content = [new Content { Text = "test" }]
});
- };
+ });
},
assertResult: response =>
{
diff --git a/tests/mcpdotnet.Tests/SseIntegrationTests.cs b/tests/mcpdotnet.Tests/SseIntegrationTests.cs
index 5f961fef..7fdec8d 100644
--- a/tests/mcpdotnet.Tests/SseIntegrationTests.cs
+++ b/tests/mcpdotnet.Tests/SseIntegrationTests.cs
@@ -87,11 +87,10 @@ public class SseIntegrationTests
// Create client and run tests
var client = await factory.GetClientAsync("everything");
- var tools = await client.ListToolsAsync();
+ var tools = await client.ListToolsAsync().ToListAsync();
// assert
- Assert.NotNull(tools);
- Assert.NotEmpty(tools.Tools);
+ Assert.NotEmpty(tools);
}
[Fact]
@@ -136,7 +135,7 @@ public class SseIntegrationTests
// Set up the sampling handler
int samplingHandlerCalls = 0;
- client.SamplingHandler = (_, _) =>
+ client.SetSamplingHandler((_, _) =>
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
@@ -149,7 +148,7 @@ public class SseIntegrationTests
Text = "Test response"
}
});
- };
+ });
// Call the server's sampleLLM tool which should trigger our sampling handler
var result = await client.CallToolAsync(
@@ -253,7 +252,7 @@ public class SseIntegrationTests
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
var receivedNotification = new TaskCompletionSource();
- client.OnNotification("test/notification", (args) =>
+ client.AddNotificationHandler("test/notification", (args) =>
{
var msg = ((JsonElement?)args.Params)?.GetProperty("message").GetString();
receivedNotification.SetResult(msg);
diff --git a/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj b/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj
index 00cb111..c1d7664 100644
--- a/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj
+++ b/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj
@@ -16,6 +16,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive