Move functionality out of IMcpClient/Server into extensions (#60)

This commit is contained in:
Stephen Toub 2025-03-13 07:55:06 -04:00 • committed by GitHub
commit 67320b22d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1042 additions and 708 deletions

View file

@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Anthropic.SDK" Version="4.4.3" />
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
</ItemGroup>
<ItemGroup>

View file

@ -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)
{

View file

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25161.3" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="9.3.0-preview.1.25161.3" />
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
</ItemGroup>
<ItemGroup>

View file

@ -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)
{

View file

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

View file

@ -4,9 +4,7 @@ using McpDotNet.Protocol.Types;
namespace McpDotNet.Client;
/// <summary>
/// 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.
/// </summary>
public interface IMcpClient : IAsyncDisposable
{
@ -32,15 +30,35 @@ public interface IMcpClient : IAsyncDisposable
/// </summary>
string? ServerInstructions { get; }
/// <summary>
/// Gets or sets the handler for server sampling requests.
/// </summary>
Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>>? SamplingHandler { get; set; }
/// <summary>Sets a handler for the named operation.</summary>
/// <param name="operationName">The name of the operation.</param>
/// <param name="handler">The handler. Each operation requires a specific delegate signature.</param>
/// <remarks>
/// <para>
/// Each operation may have only a single handler. Setting a handler for an operation that already has one
/// will replace the existing handler.
/// </para>
/// <para>
/// <see cref="OperationNames"> provides constants for common operations.</see>
/// </para>
/// </remarks>
void SetOperationHandler(string operationName, Delegate handler);
/// <summary>
/// Gets or sets the handler for providing root URIs to servers.
/// Adds a handler for server notifications of a specific method.
/// </summary>
Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>>? RootsHandler { get; set; }
/// <param name="method">The notification method to handle.</param>
/// <param name="handler">The async handler function to process notifications.</param>
/// <remarks>
/// <para>
/// Each method may have multiple handlers. Adding a handler for a method that already has one
/// will not replace the existing handler.
/// </para>
/// <para>
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
/// </para>
/// </remarks>
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
/// <summary>
/// Establishes a connection to the server.
@ -49,121 +67,23 @@ public interface IMcpClient : IAsyncDisposable
/// <returns>A task representing the asynchronous operation.</returns>
Task ConnectAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Sends a ping request to verify server connectivity.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that completes when the ping is successful.</returns>
Task PingAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a list of available tools from the server.
/// </summary>
/// <param name="cursor">An optional cursor to paginate the results.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response with tool information.</returns>
Task<ListToolsResult> ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default);
/// <summary>
/// Invokes a tool on the server with optional arguments.
/// </summary>
/// <param name="toolName">The name of the tool to call.</param>
/// <param name="arguments">Optional arguments for the tool.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the tool's response.</returns>
Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object> arguments, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a list of available prompts from the server.
/// </summary>
/// <param name="cursor">An optional cursor to paginate the results.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response with prompt information.</returns>
Task<ListPromptsResult> ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a specific prompt with optional arguments.
/// </summary>
/// <param name="name">The name of the prompt to retrieve</param>
/// <param name="arguments">Optional arguments for the prompt</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the prompt's content and messages.</returns>
Task<GetPromptResult> GetPromptAsync(string name, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a list of available resources from the server.
/// </summary>
/// <param name="cursor">An optional cursor to paginate the results.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns></returns>
Task<ListResourcesResult> ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default);
/// <summary>
/// Reads a resource from the server.
/// </summary>
/// <param name="uri">The uri of the resource.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns></returns>
Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Gets the completion options for a resource or prompt reference and (named) argument.
/// </summary>
/// <param name="reference">A resource (uri) or prompt (name) reference</param>
/// <param name="argumentName">Name of argument. Must be non-null and non-empty.</param>
/// <param name="argumentValue">Value of argument. Must be non-null.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns></returns>
Task<CompleteResult> GetCompletionAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default);
/// <summary>
/// Subscribes to a resource on the server.
/// </summary>
/// <param name="uri">The uri of the resource.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns></returns>
Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default);
/// <summary>
/// Unsubscribes from a resource on the server.
/// </summary>
/// <param name="uri">The uri of the resource.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns></returns>
Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default);
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The expected response type.</typeparam>
/// <typeparam name="TResult">The expected response type.</typeparam>
/// <param name="request">The JSON-RPC request to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response.</returns>
Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class;
/// <remarks>
/// 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.
/// </remarks>
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class;
/// <summary>
/// Registers a handler for server notifications of a specific method.
///
/// <see cref="NotificationMethods">Constants for common notification methods</see>
/// Sends a message to the server.
/// </summary>
/// <param name="method">The notification method to handle.</param>
/// <param name="handler">The async handler function to process notifications.</param>
void OnNotification(string method, Func<JsonRpcNotification, Task> handler);
/// <summary>
/// Sends a notification to the server.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SendNotificationAsync(string method, CancellationToken cancellationToken = default);
/// <summary>
/// Sends a notification to the server with parameters.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="parameters">The parameters to send with the notification.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SendNotificationAsync<T>(string method, T parameters, CancellationToken cancellationToken = default);
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
}

View file

@ -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;
/// <inheritdoc/>
internal class McpClient : McpJsonRpcEndpoint, IMcpClient
internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
{
private readonly McpClientOptions _options;
private readonly McpServerConfig _serverConfig;
private readonly ILogger<McpClient> _logger;
private volatile bool _isInitializing;
private readonly IClientTransport _clientTransport;
private volatile bool _isInitializing;
/// <inheritdoc/>
public ServerCapabilities? ServerCapabilities { get; private set; }
/// <inheritdoc/>
public Implementation? ServerInfo { get; private set; }
/// <inheritdoc/>
public string? ServerInstructions { get; private set; }
private Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>>? _samplingHandler;
private Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>>? _rootsHandler;
/// <summary>
/// Initializes a new instance of the <see cref="McpClient"/> class.
@ -43,53 +39,90 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
_logger = loggerFactory.CreateLogger<McpClient>();
_clientTransport = transport;
if (options.Capabilities?.Sampling != null)
{
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>("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<ListRootsRequestParams, ListRootsResult>("roots/list",
async (request) =>
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
"sampling/createMessage", request => HandleRequest("Sampling", _samplingHandler, request));
}
if (options.Capabilities?.Roots is not null)
{
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
"roots/list", request => HandleRequest("Roots", _rootsHandler, request));
}
}
/// <inheritdoc/>
public ServerCapabilities? ServerCapabilities { get; private set; }
/// <inheritdoc/>
public Implementation? ServerInfo { get; private set; }
/// <inheritdoc/>
public string? ServerInstructions { get; private set; }
/// <inheritdoc/>
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<TFieldRequest, TFieldResponse>(
string targetOperationName,
string operationName,
Delegate handler,
ref Func<TFieldRequest, CancellationToken, Task<TFieldResponse>>? field)
{
if (operationName == targetOperationName)
{
if (handler is Func<TFieldRequest, CancellationToken, Task<TFieldResponse>> 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<TFieldRequest, CancellationToken, Task<TFieldResponse>>)}",
nameof(handler));
}
return false;
}
}
/// <inheritdoc/>
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
}
}
/// <inheritdoc/>
public async Task PingAsync(CancellationToken cancellationToken = default)
private async Task<TResponse> HandleRequest<TRequest, TResponse>(
string friendlyName,
Func<TRequest, CancellationToken, Task<TResponse>>? handler,
TRequest args)
{
_logger.PingingServer(EndpointName);
await SendRequestAsync<dynamic>(
new JsonRpcRequest
{
Method = "ping"
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ListToolsResult> ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default)
{
_logger.ListingTools(EndpointName, cursor ?? "(null)");
return await SendRequestAsync<ListToolsResult>(
new JsonRpcRequest
{
Method = "tools/list",
Params = cursor != null ? new Dictionary<string, object?> { ["cursor"] = cursor } : null
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ListPromptsResult> ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default)
{
_logger.ListingPrompts(EndpointName, cursor ?? "(null)");
return await SendRequestAsync<ListPromptsResult>(
new JsonRpcRequest
{
Method = "prompts/list",
Params = cursor != null ? new Dictionary<string, object?> { ["cursor"] = cursor } : null
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<GetPromptResult> GetPromptAsync(string name, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default)
{
_logger.GettingPrompt(EndpointName, name, arguments == null ? "{}" : JsonSerializer.Serialize(arguments));
return await SendRequestAsync<GetPromptResult>(
new JsonRpcRequest
{
Method = "prompts/get",
Params = CreateParametersDictionary(name, arguments)
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ListResourcesResult> ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default)
{
_logger.ListingResources(EndpointName, cursor ?? "(null)");
return await SendRequestAsync<ListResourcesResult>(
new JsonRpcRequest
{
Method = "resources/list",
Params = cursor != null ? new Dictionary<string, object?> { ["cursor"] = cursor } : null
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken cancellationToken = default)
{
_logger.ReadingResource(EndpointName, uri);
return await SendRequestAsync<ReadResourceResult>(
new JsonRpcRequest
{
Method = "resources/read",
Params = new Dictionary<string, object?>
{
["uri"] = uri
}
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CompleteResult> 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<CompleteResult>(
new JsonRpcRequest
{
Method = "completion/complete",
Params = new Dictionary<string, object?>
{
["ref"] = reference,
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
}
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default)
{
_logger.SubscribingToResource(EndpointName, uri);
await SendRequestAsync<dynamic>(
new JsonRpcRequest
{
Method = "resources/subscribe",
Params = new Dictionary<string, object?>
{
["uri"] = uri
}
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default)
{
_logger.UnsubscribingFromResource(EndpointName, uri);
await SendRequestAsync<dynamic>(
new JsonRpcRequest
{
Method = "resources/unsubscribe",
Params = new Dictionary<string, object?>
{
["uri"] = uri
}
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object> arguments,
CancellationToken cancellationToken = default)
{
_logger.CallingTool(EndpointName, toolName, JsonSerializer.Serialize(arguments));
return await SendRequestAsync<CallToolResponse>(
new JsonRpcRequest
{
Method = "tools/call",
Params = CreateParametersDictionary(toolName, arguments ?? [])
},
cancellationToken
).ConfigureAwait(false);
}
private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? arguments)
{
var parameters = new Dictionary<string, object?>
{
["name"] = nameParameter
};
if (arguments != null)
{
parameters["arguments"] = arguments;
}
return parameters;
}
/// <inheritdoc/>
public Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>>? SamplingHandler { get; set; }
/// <inheritdoc/>
public Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>>? RootsHandler { get; set; }
/// <inheritdoc/>
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.");
}
}

View file

@ -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;
/// <summary>
/// Provides extensions for operating on MCP clients.
/// </summary>
public static class McpClientExtensions
{
/// <summary>
/// Sends a notification to the server with parameters.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="method">The notification method name.</param>
/// <param name="parameters">The parameters to send with the notification.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
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);
}
/// <summary>
/// Sends a ping request to verify server connectivity.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that completes when the ping is successful.</returns>
public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<dynamic>(
CreateRequest("ping", null),
cancellationToken);
}
/// <summary>
/// Retrieves a sequence of available tools from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An asynchronous sequence of tool information.</returns>
public static async IAsyncEnumerable<Tool> 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);
}
/// <summary>
/// Retrieves a sequence of available tools from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cursor">A cursor to paginate the results.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response with tool information.</returns>
public static Task<ListToolsResult> ListToolsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<ListToolsResult>(
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
cancellationToken);
}
/// <summary>
/// Retrieves a list of available prompts from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An asynchronous sequence of prompt information.</returns>
public static async IAsyncEnumerable<Prompt> 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);
}
/// <summary>
/// Retrieves a list of available prompts from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cursor">A cursor to paginate the results.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response with prompt information.</returns>
public static Task<ListPromptsResult> ListPromptsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<ListPromptsResult>(
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
cancellationToken);
}
/// <summary>
/// Retrieves a specific prompt with optional arguments.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="name">The name of the prompt to retrieve</param>
/// <param name="arguments">Optional arguments for the prompt</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the prompt's content and messages.</returns>
public static Task<GetPromptResult> GetPromptAsync(this IMcpClient client, string name, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<GetPromptResult>(
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
cancellationToken);
}
/// <summary>
/// Retrieves a sequence of available resources from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An asynchronous sequence of resource information.</returns>
public static async IAsyncEnumerable<Resource> 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);
}
/// <summary>
/// Retrieves a list of available resources from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="cursor">A cursor to paginate the results.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task<ListResourcesResult> ListResourcesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<ListResourcesResult>(
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
cancellationToken);
}
/// <summary>
/// Reads a resource from the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="uri">The uri of the resource.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task<ReadResourceResult> ReadResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<ReadResourceResult>(
CreateRequest("resources/read", new() { ["uri"] = uri }),
cancellationToken);
}
/// <summary>
/// Gets the completion options for a resource or prompt reference and (named) argument.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="reference">A resource (uri) or prompt (name) reference</param>
/// <param name="argumentName">Name of argument. Must be non-null and non-empty.</param>
/// <param name="argumentValue">Value of argument. Must be non-null.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task<CompleteResult> 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<CompleteResult>(
CreateRequest("completion/complete", new()
{
["ref"] = reference,
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
}),
cancellationToken);
}
/// <summary>
/// Subscribes to a resource on the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="uri">The uri of the resource.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<dynamic>(
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
cancellationToken);
}
/// <summary>
/// Unsubscribes from a resource on the server.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="uri">The uri of the resource.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<dynamic>(
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
cancellationToken);
}
/// <summary>
/// Invokes a tool on the server with optional arguments.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="toolName">The name of the tool to call.</param>
/// <param name="arguments">Optional arguments for the tool.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the tool's response.</returns>
public static Task<CallToolResponse> CallToolAsync(this IMcpClient client, string toolName, Dictionary<string, object> arguments, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
return client.SendRequestAsync<CallToolResponse>(
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
cancellationToken);
}
/// <summary>Sets the handler for server sampling requests.</summary>
/// <param name="client">The client.</param>
/// <param name="handler">The sampling request handler.</param>
public static void SetSamplingHandler(this IMcpClient client, Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>> handler)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
client.SetOperationHandler(OperationNames.Sampling, handler);
}
/// <summary>Sets the handler for server roots requests.</summary>
/// <param name="client">The client.</param>
/// <param name="handler">The roots request handler.</param>
public static void SetRootsHandler(this IMcpClient client, Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>> handler)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
client.SetOperationHandler(OperationNames.Roots, handler);
}
private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
new JsonRpcRequest
{
Method = method,
Params = parameters
};
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
cursor != null ? new() { ["cursor"] = cursor } : null;
private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? arguments)
{
Dictionary<string, object?> parameters = new()
{
["name"] = nameParameter
};
if (arguments != null)
{
parameters["arguments"] = arguments;
}
return parameters;
}
}

View file

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

View file

@ -0,0 +1,47 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.Logging;
namespace McpDotNet.Protocol.Messages;
/// <summary>Provides names of standard operations for use with registering handlers.</summary>
/// <remarks>
/// 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.
/// </remarks>
public static class OperationNames
{
/// <summary>Gets the name of the sampling operation.</summary>
public static string Sampling { get; } = "operation/sampling";
/// <summary>Gets the name of the roots operation.</summary>
public static string Roots { get; } = "operation/roots";
/// <summary>Gets the name of the list tools operation.</summary>
public static string ListTools { get; } = "operation/listTools";
/// <summary>Gets the name of the call tool operation.</summary>
public static string CallTool { get; } = "operation/callTool";
/// <summary>Gets the name of the list prompts operation.</summary>
public static string ListPrompts { get; } = "operation/listPrompts";
/// <summary>Gets the name of the get prompt operation.</summary>
public static string GetPrompt { get; } = "operation/getPrompt";
/// <summary>Gets the name of the list resources operation.</summary>
public static string ListResources { get; } = "operation/listResources";
/// <summary>Gets the name of the read resource operation.</summary>
public static string ReadResource { get; } = "operation/readResource";
/// <summary>Gets the name of the get completion operation.</summary>
public static string GetCompletion { get; } = "operation/getCompletion";
/// <summary>Gets the name of the subscribe to resources operation.</summary>
public static string SubscribeToResources { get; } = "operation/subscribeToResources";
/// <summary>Gets the name of the subscribe to resources operation.</summary>
public static string UnsubscribeFromResources { get; } = "operation/unsubscribeFromResources";
}

View file

@ -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
/// <summary>
/// Validates the reference object.
/// </summary>
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;
}
}

View file

@ -28,67 +28,41 @@ public interface IMcpServer : IAsyncDisposable
/// </summary>
IServiceProvider? ServiceProvider { get; }
/// <summary>Sets a handler for the named operation.</summary>
/// <param name="operationName">The name of the operation.</param>
/// <param name="handler">The handler. Each operation requires a specific delegate signature.</param>
/// <remarks>
/// <para>
/// Each operation may have only a single handler. Setting a handler for an operation that already has one
/// will replace the existing handler.
/// </para>
/// <para>
/// <see cref="OperationNames"> provides constants for common operations.</see>
/// </para>
/// </remarks>
void SetOperationHandler(string operationName, Delegate handler);
/// <summary>
/// Adds a handler for client notifications of a specific method.
/// </summary>
/// <param name="method">The notification method to handle.</param>
/// <param name="handler">The async handler function to process notifications.</param>
/// <remarks>
/// <para>
/// Each method may have multiple handlers. Adding a handler for a method that already has one
/// will not replace the existing handler.
/// </para>
/// <para>
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
/// </para>
/// </remarks>
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
/// <summary>
/// Starts the server and begins listening for client requests.
/// </summary>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets or sets the handler for list tools requests.
/// </summary>
Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for call tool requests.
/// </summary>
Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list prompts requests.
/// </summary>
Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get prompt requests.
/// </summary>
Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
/// <summary>
/// Gets or sets the handler for list resources requests.
/// </summary>
Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for read resources requests.
/// </summary>
Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
/// <summary>
/// Gets or sets the handler for get resources requests.
/// </summary>
Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
/// <summary>
/// Gets or sets the handler for subscribe to resources messages.
/// </summary>
Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
/// <summary>
/// Requests the client to create a new message.
/// </summary>
Task<CreateMessageResult> RequestSamplingAsync(CreateMessageRequestParams request, CancellationToken cancellationToken);
/// <summary>
/// Requests the client to list the roots it exposes.
/// </summary>
Task<ListRootsResult> RequestRootsAsync(ListRootsRequestParams request, CancellationToken cancellationToken);
/// <summary>
/// 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<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class;
/// <summary>
/// Registers a handler for notifications of a specific method.
///
/// <see cref="NotificationMethods">Constants for common notification methods</see>
/// Sends a message to the server.
/// </summary>
/// <param name="method">The notification method to handle.</param>
/// <param name="handler">The async handler function to process notifications.</param>
void OnNotification(string method, Func<JsonRpcNotification, Task> handler);
/// <summary>
/// Sends a notification to the client.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="message">The message.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SendNotificationAsync(string method, CancellationToken cancellationToken = default);
/// <summary>
/// Sends a notification to the client with parameters.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="parameters">The parameters to send with the notification.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
Task SendNotificationAsync<T>(string method, T parameters, CancellationToken cancellationToken = default);
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
}

View file

@ -10,13 +10,23 @@ using Microsoft.Extensions.Logging;
namespace McpDotNet.Server;
/// <inheritdoc />
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<McpServer> _logger;
private Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? _listToolsHandler;
private Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? _callToolHandler;
private Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? _listPromptsHandler;
private Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? _getPromptHandler;
private Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? _listResourcesHandler;
private Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? _readResourceHandler;
private Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? _getCompletionHandler;
private Func<RequestContext<string>, CancellationToken, Task>? _subscribeToResourcesHandler;
private Func<RequestContext<string>, CancellationToken, Task>? _unsubscribeFromResourcesHandler;
/// <summary>
/// Creates a new instance of <see cref="McpServer"/>.
/// </summary>
@ -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; }
/// <inheritdoc />
public Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>>? ListToolsHandler { get; set; }
public override string EndpointName =>
$"Server ({_options.ServerInfo.Name} {_options.ServerInfo.Version}), Client ({ClientInfo?.Name} {ClientInfo?.Version})";
/// <inheritdoc />
public Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>? CallToolHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>>? ListPromptsHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>>? ListResourcesHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>>? ReadResourceHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<string>, CancellationToken, Task>? SubscribeToResourcesHandler { get; set; }
/// <inheritdoc />
public Func<RequestContext<string>, CancellationToken, Task>? UnsubscribeFromResourcesHandler { get; set; }
/// <inheritdoc />
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<TFieldRequest, TFieldResponse>(
string targetOperationName,
string operationName,
Delegate handler,
ref Func<TFieldRequest, CancellationToken, TFieldResponse>? field)
{
if (operationName == targetOperationName)
{
if (handler is Func<TFieldRequest, CancellationToken, TFieldResponse> typed)
{
field = typed;
return true;
}
throw new ArgumentException(
$"Handler must be of type {typeof(Func<TFieldRequest, CancellationToken, TFieldResponse>)}",
nameof(handler));
}
return false;
}
}
@ -127,45 +153,9 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
}
}
/// <inheritdoc />
public async Task<CreateMessageResult> RequestSamplingAsync(CreateMessageRequestParams request, CancellationToken cancellationToken)
private void AddNotificationHandler()
{
if (ClientCapabilities?.Sampling == null)
{
throw new McpServerException("Client does not support sampling");
}
return await SendRequestAsync<CreateMessageResult>(
new JsonRpcRequest
{
Method = "sampling/createMessage",
Params = request
},
cancellationToken
).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<ListRootsResult> RequestRootsAsync(ListRootsRequestParams request, CancellationToken cancellationToken)
{
if (ClientCapabilities?.Roots == null)
{
throw new McpServerException("Client does not support roots");
}
return await SendRequestAsync<ListRootsResult>(
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<JsonNode, PingResult>("ping",
(request) =>
{
return Task.FromResult(new PingResult());
});
SetRequestHandler<JsonNode, PingResult>("ping",
request => Task.FromResult(new PingResult()));
}
private void SetInitializeHandler(McpServerOptions options)
@ -203,7 +190,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
SetRequestHandler<CompleteRequestParams, CompleteResult>("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<ListResourcesRequestParams, ListResourcesResult>("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<ReadResourceRequestParams, ReadResourceResult>("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<ListPromptsRequestParams, ListPromptsResult>("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<GetPromptRequestParams, GetPromptResult>("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<ListToolsRequestParams, ListToolsResult>("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<CallToolRequestParams, CallToolResponse>("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);
});
}
}

View file

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

View file

@ -0,0 +1,221 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
namespace McpDotNet.Server;
/// <inheritdoc />
public static class McpServerExtensions
{
/// <summary>
/// Requests the client to create a new message.
/// </summary>
public static Task<CreateMessageResult> 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<CreateMessageResult>(
new JsonRpcRequest { Method = "sampling/createMessage", Params = request },
cancellationToken);
}
/// <summary>
/// Requests the client to list the roots it exposes.
/// </summary>
public static Task<ListRootsResult> 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<ListRootsResult>(
new JsonRpcRequest { Method = "roots/list", Params = request },
cancellationToken);
}
/// <summary>
/// Sets the handler for list tools requests.
/// </summary>
public static void SetListToolsHandler(
this IMcpServer server, Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.ListTools, handler);
}
/// <summary>
/// Sets the handler for call tool requests.
/// </summary>
public static void SetCallToolHandler(
this IMcpServer server, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.CallTool, handler);
}
/// <summary>
/// Sets the handler for list prompts requests.
/// </summary>
public static void SetListPromptsHandler(
this IMcpServer server, Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.ListPrompts, handler);
}
/// <summary>
/// Sets the handler for get prompt requests.
/// </summary>
public static void SetGetPromptHandler(
this IMcpServer server, Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.GetPrompt, handler);
}
/// <summary>
/// Sets the handler for list resources requests.
/// </summary>
public static void SetListResourcesHandler(
this IMcpServer server, Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.ListResources, handler);
}
/// <summary>
/// Sets the handler for read resource requests.
/// </summary>
public static void SetReadResourceHandler(
this IMcpServer server, Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.ReadResource, handler);
}
/// <summary>
/// Sets the handler for get completion requests.
/// </summary>
public static void SetGetCompletionHandler(
this IMcpServer server, Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>> handler)
{
if (server is null)
{
throw new ArgumentNullException(nameof(server));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
server.SetOperationHandler(OperationNames.GetCompletion, handler);
}
/// <summary>
/// Sets the handler for subscribe to resources requests.
/// </summary>
public static void SetSubscribeToResourcesHandler(
this IMcpServer server, Func<RequestContext<string>, 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);
}
/// <summary>
/// Sets the handler for unsubscribe from resources requests.
/// </summary>
public static void SetUnsubscribeFromResourcesHandler(
this IMcpServer server, Func<RequestContext<string>, 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);
}
}

View file

@ -34,14 +34,16 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// <param name="loggerFactory">The logger factory.</param>
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<McpClient>();
}
@ -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.
/// </summary>
/// <typeparam name="T">The expected response type.</typeparam>
/// <typeparam name="TResult">The expected response type.</typeparam>
/// <param name="request">The JSON-RPC request to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response.</returns>
public async Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class
public async Task<TResult> SendRequestAsync<TResult>(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<T>(resultJson, _jsonOptions);
var resultObject = JsonSerializer.Deserialize<TResult>(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
}
}
/// <summary>
/// Sends a notification to the connected endpoint.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
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);
}
/// <summary>
/// Sends a notification to the connected endpoint with parameters.
/// </summary>
/// <param name="method">The notification method name.</param>
/// <param name="parameters">The parameters to send with the notification.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public async Task SendNotificationAsync<T>(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);
}
/// <summary>
/// Sends a notification to the connected endpoint.
/// </summary>
/// <param name="notification">The notification to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
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);
}
/// <summary>
@ -343,7 +308,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// </summary>
/// <param name="method">The notification method to handle.</param>
/// <param name="handler">The async handler function to process notifications.</param>
public void OnNotification(string method, Func<JsonRpcNotification, Task> handler)
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler)
{
var handlers = _notificationHandlers.GetOrAdd(method, _ => []);
lock (handlers)
@ -368,6 +333,11 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// <param name="handler">Handler to be called when a request with specified method identifier is received</param>
protected void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest?, Task<TResponse>> handler)
{
if (method is null)
{
throw new ArgumentNullException(nameof(method));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));

View file

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

View file

@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -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<PromptMessage> 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)

View file

@ -59,11 +59,10 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
var tools = await client.ListToolsAsync();
var tools = await client.ListToolsAsync().ToListAsync();
// assert
Assert.NotNull(tools);
Assert.NotEmpty(tools.Tools);
Assert.NotEmpty(tools);
// We could add more specific assertions about expected tools
}
@ -99,14 +98,13 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// act
var client = await _fixture.Factory.GetClientAsync(clientId);
var prompts = await client.ListPromptsAsync();
var prompts = await client.ListPromptsAsync().ToListAsync();
// assert
Assert.NotNull(prompts);
Assert.NotEmpty(prompts.Prompts);
Assert.NotEmpty(prompts);
// We could add specific assertions for the known prompts
Assert.Contains(prompts.Prompts, p => 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<ClientIntegrationTestFixture
// Set up the sampling handler
int samplingHandlerCalls = 0;
client.SamplingHandler = (_, _) =>
client.SetSamplingHandler((_, _) =>
{
samplingHandlerCalls++;
return Task.FromResult(new CreateMessageResult
@ -278,7 +276,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Text = "Test response"
}
});
};
});
// Call the server's sampleLLM tool which should trigger our sampling handler
var result = await client.CallToolAsync(
@ -311,14 +309,14 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// var client = await _fixture.Factory.GetClientAsync(clientId);
// // Set up the roots handler
// client.RootsHandler = (request, ct) =>
// client.SetRootsHandler((request, ct) =>
// {
// rootsHandlerCalls++;
// return Task.FromResult(new ListRootsResult
// {
// Roots = testRoots
// });
// };
// });
// // Connect
// await client.ConnectAsync(CancellationToken.None);

Some files were not shown because too many files have changed in this diff Show more