Move functionality out of IMcpClient/Server into extensions (#60)
This commit is contained in:
parent
cdcf134e7a
commit
67320b22d5
3 changed files with 1042 additions and 708 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue