Server support (stdio) (#19)
* Server support (stdio) * TestServer project * Refactored McpClient class to extract shared endpoint logic (with McpServer) to a base class, McpJsonRpcEndpoint. * Improved xmldoc comments for protocol types, mirroring specification schema comments. * Update README.MD
This commit is contained in:
parent
c384c7d3de
commit
8fe797e1f3
1 changed files with 2668 additions and 659 deletions
|
|
@ -1,35 +1,23 @@
|
|||
|
||||
using McpDotNet.Protocol.Types;
|
||||
using McpDotNet.Protocol.Transport;
|
||||
using System.Collections.Concurrent;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using System.Text.Json;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using McpDotNet.Logging;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Shared;
|
||||
|
||||
namespace McpDotNet.Client;
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal class McpClient : IMcpClient
|
||||
internal class McpClient : McpJsonRpcEndpoint, IMcpClient
|
||||
{
|
||||
private readonly IMcpTransport _transport;
|
||||
private readonly McpClientOptions _options;
|
||||
private readonly ConcurrentDictionary<RequestId, TaskCompletionSource<IJsonRpcMessage>> _pendingRequests;
|
||||
private readonly ConcurrentDictionary<string, List<Func<JsonRpcNotification,Task>>> _notificationHandlers;
|
||||
private readonly Dictionary<string, Func<JsonRpcRequest, Task<object>>> _requestHandlers = new();
|
||||
private int _nextRequestId;
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private bool _isInitialized;
|
||||
private Task? _messageProcessingTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly ILogger<McpClient> _logger;
|
||||
private volatile bool _isInitializing;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInitialized => _isInitialized;
|
||||
private readonly IClientTransport _clientTransport;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ServerCapabilities? ServerCapabilities { get; private set; }
|
||||
|
|
@ -43,20 +31,17 @@ internal class McpClient : IMcpClient
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClient"/> class.
|
||||
/// </summary>
|
||||
/// <param name="transport">An MCP transport implementation.</param>
|
||||
/// <param name="transport">The transport to use for communication with the server.</param>
|
||||
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
||||
/// <param name="serverConfig">The server configuration.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public McpClient(IMcpTransport transport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
|
||||
public McpClient(IClientTransport transport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
|
||||
: base(transport, loggerFactory)
|
||||
{
|
||||
_transport = transport;
|
||||
_options = options;
|
||||
_pendingRequests = new();
|
||||
_notificationHandlers = new();
|
||||
_nextRequestId = 1;
|
||||
_serverConfig = serverConfig;
|
||||
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
|
||||
_logger = loggerFactory.CreateLogger<McpClient>();
|
||||
_clientTransport = transport;
|
||||
|
||||
if (options.Capabilities?.Sampling != null)
|
||||
{
|
||||
|
|
@ -65,11 +50,11 @@ internal class McpClient : IMcpClient
|
|||
if (SamplingHandler == null)
|
||||
{
|
||||
// Setting the capability, but not a handler means we have nothing to return to the server
|
||||
_logger.SamplingHandlerNotConfigured(_serverConfig.Id, _serverConfig.Name);
|
||||
_logger.SamplingHandlerNotConfigured(EndpointName);
|
||||
throw new McpClientException("Sampling handler not configured");
|
||||
}
|
||||
|
||||
return await SamplingHandler(request, _cts?.Token ?? CancellationToken.None);
|
||||
return await SamplingHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
|
||||
});
|
||||
}
|
||||
if (options.Capabilities?.Roots != null)
|
||||
|
|
@ -80,10 +65,10 @@ internal class McpClient : IMcpClient
|
|||
if (RootsHandler == null)
|
||||
{
|
||||
// Setting the capability, but not a handler means we have nothing to return to the server
|
||||
_logger.RootsHandlerNotConfigured(_serverConfig.Id, _serverConfig.Name);
|
||||
_logger.RootsHandlerNotConfigured(EndpointName);
|
||||
throw new McpClientException("Roots handler not configured");
|
||||
}
|
||||
return await RootsHandler(request, _cts?.Token ?? CancellationToken.None);
|
||||
return await RootsHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -93,35 +78,35 @@ internal class McpClient : IMcpClient
|
|||
{
|
||||
if (_isInitializing)
|
||||
{
|
||||
_logger.ClientAlreadyInitializing(_serverConfig.Id, _serverConfig.Name);
|
||||
_logger.ClientAlreadyInitializing(EndpointName);
|
||||
throw new InvalidOperationException("Client is already initializing");
|
||||
}
|
||||
_isInitializing = true;
|
||||
|
||||
if (_isInitialized)
|
||||
if (IsInitialized)
|
||||
{
|
||||
_logger.ClientAlreadyInitialized(_serverConfig.Id, _serverConfig.Name);
|
||||
_logger.ClientAlreadyInitialized(EndpointName);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
// Connect transport
|
||||
await _transport.ConnectAsync(_cts.Token).ConfigureAwait(false);
|
||||
await _clientTransport.ConnectAsync(CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
// Start processing messages
|
||||
_messageProcessingTask = ProcessMessagesAsync(_cts.Token);
|
||||
MessageProcessingTask = ProcessMessagesAsync(CancellationTokenSource.Token);
|
||||
|
||||
// Perform initialization sequence
|
||||
await InitializeAsync(_cts.Token).ConfigureAwait(false);
|
||||
await InitializeAsync(CancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
_isInitialized = true;
|
||||
IsInitialized = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.ClientInitializationError(_serverConfig.Id, _serverConfig.Name, e);
|
||||
_logger.ClientInitializationError(EndpointName, e);
|
||||
await CleanupAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
|
|
@ -154,7 +139,7 @@ internal class McpClient : IMcpClient
|
|||
).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
_logger.ServerCapabilitiesReceived(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(initializeResponse.Capabilities), JsonSerializer.Serialize(initializeResponse.ServerInfo));
|
||||
_logger.ServerCapabilitiesReceived(EndpointName, JsonSerializer.Serialize(initializeResponse.Capabilities), JsonSerializer.Serialize(initializeResponse.ServerInfo));
|
||||
ServerCapabilities = initializeResponse.Capabilities;
|
||||
ServerInfo = initializeResponse.ServerInfo;
|
||||
ServerInstructions = initializeResponse.Instructions;
|
||||
|
|
@ -162,7 +147,7 @@ internal class McpClient : IMcpClient
|
|||
// Validate protocol version
|
||||
if (initializeResponse.ProtocolVersion != _options.ProtocolVersion)
|
||||
{
|
||||
_logger.ServerProtocolVersionMismatch(_serverConfig.Id, _serverConfig.Name, _options.ProtocolVersion, initializeResponse.ProtocolVersion);
|
||||
_logger.ServerProtocolVersionMismatch(EndpointName, _options.ProtocolVersion, initializeResponse.ProtocolVersion);
|
||||
throw new McpClientException($"Server protocol version mismatch. Expected {_options.ProtocolVersion}, got {initializeResponse.ProtocolVersion}");
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +162,7 @@ internal class McpClient : IMcpClient
|
|||
}
|
||||
catch (OperationCanceledException) when (initializationCts.IsCancellationRequested)
|
||||
{
|
||||
_logger.ClientInitializationTimeout(_serverConfig.Id, _serverConfig.Name);
|
||||
_logger.ClientInitializationTimeout(EndpointName);
|
||||
throw new McpClientException("Initialization timed out");
|
||||
}
|
||||
}
|
||||
|
|
@ -185,7 +170,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task PingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.PingingServer(_serverConfig.Id, _serverConfig.Name);
|
||||
_logger.PingingServer(EndpointName);
|
||||
await SendRequestAsync<dynamic>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -198,7 +183,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task<ListToolsResult> ListToolsAsync(string? cursor = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.ListingTools(_serverConfig.Id, _serverConfig.Name, cursor ?? "(null)");
|
||||
_logger.ListingTools(EndpointName, cursor ?? "(null)");
|
||||
return await SendRequestAsync<ListToolsResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -212,7 +197,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task<ListPromptsResult> ListPromptsAsync(string? cursor = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.ListingPrompts(_serverConfig.Id, _serverConfig.Name, cursor ?? "(null)");
|
||||
_logger.ListingPrompts(EndpointName, cursor ?? "(null)");
|
||||
return await SendRequestAsync<ListPromptsResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -226,7 +211,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task<GetPromptResult> GetPromptAsync(string name, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.GettingPrompt(_serverConfig.Id, _serverConfig.Name, name, arguments == null ? "{}" : JsonSerializer.Serialize(arguments));
|
||||
_logger.GettingPrompt(EndpointName, name, arguments == null ? "{}" : JsonSerializer.Serialize(arguments));
|
||||
return await SendRequestAsync<GetPromptResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -240,7 +225,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task<ListResourcesResult> ListResourcesAsync(string? cursor = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.ListingResources(_serverConfig.Id, _serverConfig.Name, cursor ?? "(null)");
|
||||
_logger.ListingResources(EndpointName, cursor ?? "(null)");
|
||||
return await SendRequestAsync<ListResourcesResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -254,7 +239,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.ReadingResource(_serverConfig.Id, _serverConfig.Name, uri);
|
||||
_logger.ReadingResource(EndpointName, uri);
|
||||
return await SendRequestAsync<ReadResourceResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -273,21 +258,21 @@ internal class McpClient : IMcpClient
|
|||
{
|
||||
if (!reference.Validate(out string validationMessage))
|
||||
{
|
||||
_logger.InvalidCompletionReference(_serverConfig.Id, _serverConfig.Name, reference.ToString(), validationMessage);
|
||||
_logger.InvalidCompletionReference(EndpointName, reference.ToString(), validationMessage);
|
||||
throw new McpClientException($"Invalid reference: {validationMessage}");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(argumentName))
|
||||
{
|
||||
_logger.InvalidCompletionArgumentName(_serverConfig.Id, _serverConfig.Name, argumentName);
|
||||
_logger.InvalidCompletionArgumentName(EndpointName, argumentName);
|
||||
throw new McpClientException("Argument name cannot be null or empty");
|
||||
}
|
||||
if (argumentValue is null)
|
||||
{
|
||||
_logger.InvalidCompletionArgumentValue(_serverConfig.Id, _serverConfig.Name, argumentValue);
|
||||
_logger.InvalidCompletionArgumentValue(EndpointName, argumentValue);
|
||||
throw new McpClientException("Argument value cannot be null");
|
||||
}
|
||||
|
||||
_logger.GettingCompletion(_serverConfig.Id, _serverConfig.Name, reference.ToString(), argumentName, argumentValue);
|
||||
_logger.GettingCompletion(EndpointName, reference.ToString(), argumentName, argumentValue);
|
||||
return await SendRequestAsync<CompleteResult>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -305,7 +290,7 @@ internal class McpClient : IMcpClient
|
|||
/// <inheritdoc/>
|
||||
public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.SubscribingToResource(_serverConfig.Id, _serverConfig.Name, uri);
|
||||
_logger.SubscribingToResource(EndpointName, uri);
|
||||
await SendRequestAsync<dynamic>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -319,11 +304,10 @@ internal class McpClient : IMcpClient
|
|||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.UnsubscribingFromResource(_serverConfig.Id, _serverConfig.Name, uri);
|
||||
_logger.UnsubscribingFromResource(EndpointName, uri);
|
||||
await SendRequestAsync<dynamic>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -338,10 +322,10 @@ internal class McpClient : IMcpClient
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object>? arguments = null,
|
||||
public async Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object> arguments,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.CallingTool(_serverConfig.Id, _serverConfig.Name, toolName, arguments == null ? "{}" : JsonSerializer.Serialize(arguments));
|
||||
_logger.CallingTool(EndpointName, toolName, JsonSerializer.Serialize(arguments));
|
||||
return await SendRequestAsync<CallToolResponse>(
|
||||
new JsonRpcRequest
|
||||
{
|
||||
|
|
@ -352,301 +336,33 @@ internal class McpClient : IMcpClient
|
|||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal IMcpTransport Transport => _transport;
|
||||
|
||||
private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object>? optionalArguments = null)
|
||||
private static Dictionary<string, object?> CreateParametersDictionary(string nameParameter, Dictionary<string, object> arguments)
|
||||
{
|
||||
var parameters = new Dictionary<string, object?>
|
||||
{
|
||||
["name"] = nameParameter
|
||||
};
|
||||
|
||||
if (optionalArguments != null)
|
||||
if (arguments != null)
|
||||
{
|
||||
parameters["arguments"] = optionalArguments;
|
||||
parameters["arguments"] = arguments;
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private async Task ProcessMessagesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (var message in _transport.MessageReader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
await HandleMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Normal shutdown
|
||||
_logger.ClientMessageProcessingCancelled(_serverConfig.Id, _serverConfig.Name);
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
// Ignore reader disposal and mocked transport
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case JsonRpcRequest request:
|
||||
if (_requestHandlers.TryGetValue(request.Method, out var handler))
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.RequestHandlerCalled(_serverConfig.Id, _serverConfig.Name, request.Method);
|
||||
var result = await handler(request);
|
||||
_logger.RequestHandlerCompleted(_serverConfig.Id, _serverConfig.Name, request.Method);
|
||||
await _transport.SendMessageAsync(new JsonRpcResponse
|
||||
{
|
||||
Id = request.Id,
|
||||
JsonRpc = "2.0",
|
||||
Result = result
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.RequestHandlerError(_serverConfig.Id, _serverConfig.Name, request.Method, ex);
|
||||
// Send error response
|
||||
await _transport.SendMessageAsync(new JsonRpcError
|
||||
{
|
||||
Id = request.Id,
|
||||
JsonRpc = "2.0",
|
||||
Error = new JsonRpcErrorDetail
|
||||
{
|
||||
Code = -32000, // Implementation defined error
|
||||
Message = ex.Message
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IJsonRpcMessageWithId messageWithId:
|
||||
if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
|
||||
{
|
||||
tcs.TrySetResult(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.NoRequestFoundForMessageWithId(_serverConfig.Id, _serverConfig.Name, messageWithId.Id.ToString());
|
||||
}
|
||||
break;
|
||||
|
||||
case JsonRpcNotification notification:
|
||||
if (_notificationHandlers.TryGetValue(notification.Method, out var handlers))
|
||||
{
|
||||
foreach (var notificationHandler in handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
await notificationHandler(notification).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log handler error but continue processing
|
||||
_logger.NotificationHandlerError(_serverConfig.Id, _serverConfig.Name, notification.Method, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
_logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Transport is not connected");
|
||||
}
|
||||
|
||||
// Set request ID
|
||||
request.Id = RequestId.FromNumber(Interlocked.Increment(ref _nextRequestId));
|
||||
|
||||
var tcs = new TaskCompletionSource<IJsonRpcMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_pendingRequests[request.Id] = tcs;
|
||||
|
||||
try
|
||||
{
|
||||
// Expensive logging, use the logging framework to check if the logger is enabled
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingRequestPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(request));
|
||||
}
|
||||
|
||||
// Less expensive information logging
|
||||
_logger.SendingRequest(_serverConfig.Id, _serverConfig.Name, request.Method);
|
||||
|
||||
await _transport.SendMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
var response = await tcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response is JsonRpcError error)
|
||||
{
|
||||
_logger.RequestFailed(_serverConfig.Id, _serverConfig.Name, request.Method, error.Error.Message, error.Error.Code);
|
||||
throw new McpClientException($"Request failed: {error.Error.Message}", error.Error.Code);
|
||||
}
|
||||
|
||||
if (response is JsonRpcResponse success)
|
||||
{
|
||||
// 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);
|
||||
|
||||
// Not expensive logging because we're already converting to JSON in order to get the result object
|
||||
_logger.RequestResponseReceivedPayload(_serverConfig.Id, _serverConfig.Name, resultJson);
|
||||
_logger.RequestResponseReceived(_serverConfig.Id, _serverConfig.Name, request.Method);
|
||||
|
||||
if (resultObject != null)
|
||||
{
|
||||
return resultObject;
|
||||
}
|
||||
|
||||
// Result object was null, this is unexpected
|
||||
_logger.RequestResponseTypeConversionError(_serverConfig.Id, _serverConfig.Name, request.Method, typeof(T));
|
||||
throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result)}, expected {typeof(T)}");
|
||||
}
|
||||
|
||||
// Unexpected response type
|
||||
_logger.RequestInvalidResponseType(_serverConfig.Id, _serverConfig.Name, request.Method);
|
||||
throw new McpClientException("Invalid response type");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingRequests.TryRemove(request.Id, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendNotificationAsync(string method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
_logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Transport is not connected");
|
||||
}
|
||||
|
||||
var notification = new JsonRpcNotification { Method = method };
|
||||
|
||||
// Log if enabled
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
|
||||
}
|
||||
|
||||
// Log basic info
|
||||
_logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method);
|
||||
|
||||
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendNotificationAsync<T>(string method, T parameters, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
_logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Transport is not connected");
|
||||
}
|
||||
var notification = new JsonRpcNotification
|
||||
{
|
||||
Method = method,
|
||||
Params = parameters
|
||||
};
|
||||
// Log if enabled
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
|
||||
}
|
||||
// Log basic info
|
||||
_logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method);
|
||||
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendNotificationAsync(JsonRpcNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
_logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Transport is not connected");
|
||||
}
|
||||
|
||||
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnNotification(string method, Func<JsonRpcNotification,Task> handler)
|
||||
{
|
||||
var handlers = _notificationHandlers.GetOrAdd(method, _ => new());
|
||||
lock (handlers)
|
||||
{
|
||||
handlers.Add(handler);
|
||||
}
|
||||
}
|
||||
|
||||
public Func<CreateMessageRequestParams, CancellationToken, Task<CreateMessageResult>>? SamplingHandler { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Func<ListRootsRequestParams, CancellationToken, Task<ListRootsResult>>? RootsHandler { get; set; }
|
||||
|
||||
private void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest, Task<TResponse>> handler)
|
||||
{
|
||||
_requestHandlers[method] = async (request) => {
|
||||
// Convert the params JsonElement to our type using the same options
|
||||
var jsonString = JsonSerializer.Serialize(request.Params);
|
||||
var typedRequest = JsonSerializer.Deserialize<TRequest>(jsonString, _jsonOptions);
|
||||
|
||||
if (typedRequest == null)
|
||||
{
|
||||
_logger.RequestParamsTypeConversionError(_serverConfig.Id, _serverConfig.Name, method, typeof(TRequest));
|
||||
throw new McpClientException($"Invalid request parameters type {jsonString}, expected {typeof(TRequest)}");
|
||||
}
|
||||
|
||||
return await handler(typedRequest);
|
||||
};
|
||||
}
|
||||
|
||||
private async Task CleanupAsync()
|
||||
{
|
||||
_logger.CleaningUpClient(_serverConfig.Id, _serverConfig.Name);
|
||||
|
||||
_cts?.Cancel();
|
||||
|
||||
if (_messageProcessingTask != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _messageProcessingTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignore cancellation
|
||||
}
|
||||
}
|
||||
|
||||
// Complete all pending requests with cancellation
|
||||
foreach (var (_, tcs) in _pendingRequests)
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
_pendingRequests.Clear();
|
||||
|
||||
await _transport.DisposeAsync().ConfigureAwait(false);
|
||||
_cts?.Dispose();
|
||||
|
||||
_isInitialized = false;
|
||||
|
||||
_logger.ClientCleanedUp(_serverConfig.Id, _serverConfig.Name);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
public override string EndpointName
|
||||
{
|
||||
await CleanupAsync().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
get
|
||||
{
|
||||
return $"Client ({_serverConfig.Id}: {_serverConfig.Name})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue