Move functionality out of IMcpClient/Server into extensions (#60)
This commit is contained in:
parent
cdcf134e7a
commit
67320b22d5
24 changed files with 1042 additions and 708 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
361
src/mcpdotnet/Client/McpClientExtensions.cs
Normal file
361
src/mcpdotnet/Client/McpClientExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
47
src/mcpdotnet/Protocol/Messages/OperationNames.cs
Normal file
47
src/mcpdotnet/Protocol/Messages/OperationNames.cs
Normal 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";
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
221
src/mcpdotnet/Server/McpServerExtensions.cs
Normal file
221
src/mcpdotnet/Server/McpServerExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue