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:
Peder Holdgaard Pedersen 2025-02-08 16:21:26 +01:00 • committed by GitHub
commit 8fe797e1f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 2668 additions and 659 deletions

View file

@ -71,7 +71,7 @@ public interface IMcpClient : IAsyncDisposable
/// <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 = null, CancellationToken cancellationToken = default);
Task<CallToolResponse> CallToolAsync(string toolName, Dictionary<string, object> arguments, CancellationToken cancellationToken = default);
/// <summary>
/// Retrieves a list of available prompts from the server.
@ -134,8 +134,8 @@ public interface IMcpClient : IAsyncDisposable
/// <summary>
/// Sends a generic JSON-RPC request to the server.
/// NB! This is a temporary method that is available to send not yet implemented feature messages.
/// Once all MCP features are implemented this will be made private, as it is purely a convenience for those who wish to implement features ahead of the library.
/// 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>
/// <param name="request">The JSON-RPC request to send.</param>

View file

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

View file

@ -15,8 +15,8 @@ public class McpClientFactory
private readonly Dictionary<string, McpServerConfig> _serverConfigs;
private readonly McpClientOptions _clientOptions;
private readonly Dictionary<string, IMcpClient> _clients = new();
private readonly Func<McpServerConfig, IMcpTransport> _transportFactoryMethod;
private readonly Func<IMcpTransport, McpServerConfig, McpClientOptions, IMcpClient> _clientFactoryMethod;
private readonly Func<McpServerConfig, IClientTransport> _transportFactoryMethod;
private readonly Func<IClientTransport, McpServerConfig, McpClientOptions, IMcpClient> _clientFactoryMethod;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<McpClientFactory> _logger;
@ -34,8 +34,8 @@ public class McpClientFactory
IEnumerable<McpServerConfig> serverConfigs,
McpClientOptions clientOptions,
ILoggerFactory loggerFactory,
Func<McpServerConfig, IMcpTransport>? transportFactoryMethod = null,
Func<IMcpTransport, McpServerConfig, McpClientOptions, IMcpClient>? clientFactoryMethod = null)
Func<McpServerConfig, IClientTransport>? transportFactoryMethod = null,
Func<IClientTransport, McpServerConfig, McpClientOptions, IMcpClient>? clientFactoryMethod = null)
{
_serverConfigs = serverConfigs.ToDictionary(c => c.Id);
_clientOptions = clientOptions;
@ -71,33 +71,37 @@ public class McpClientFactory
throw new ArgumentException($"Server with ID '{serverId}' not found.", nameof(serverId));
}
string endpointName = $"Client ({serverId}: {config.Name})";
if (_clients.TryGetValue(serverId, out var existingClient))
{
_logger.ClientExists(serverId, config.Name);
_logger.ClientExists(endpointName);
return existingClient;
}
_logger.CreatingClient(serverId, config.Name);
_logger.CreatingClient(endpointName);
var transport = _transportFactoryMethod(config);
var client = _clientFactoryMethod(transport, config, _clientOptions);
await client.ConnectAsync(cancellationToken);
_logger.ClientCreated(serverId, config.Name);
_logger.ClientCreated(endpointName);
_clients[serverId] = client;
return client;
}
internal Func<McpServerConfig, IMcpTransport> TransportFactoryMethod => _transportFactoryMethod;
internal Func<McpServerConfig, IClientTransport> TransportFactoryMethod => _transportFactoryMethod;
private IMcpTransport CreateTransport(McpServerConfig config)
private IClientTransport CreateTransport(McpServerConfig config)
{
string endpointName = $"Client ({config.Id}: {config.Name})";
var options = string.Join(", ", config.TransportOptions?.Select(kv => $"{kv.Key}={kv.Value}") ?? Enumerable.Empty<string>());
_logger.CreatingTransport(config.Id, config.Name, config.TransportType, options);
_logger.CreatingTransport(endpointName, config.TransportType, options);
return config.TransportType.ToLowerInvariant() switch
{
"stdio" => new StdioTransport(new StdioTransportOptions
"stdio" => new StdioClientTransport(new StdioClientTransportOptions
{
Command = GetCommand(config),
Arguments = config.TransportOptions?.GetValueOrDefault("arguments")?.Split(' '),
@ -106,8 +110,8 @@ public class McpClientFactory
.Where(kv => kv.Key.StartsWith("env:"))
.ToDictionary(kv => kv.Key.Substring(4), kv => kv.Value)
}, config, _loggerFactory),
"sse" or "http" => new SseTransport(
new SseTransportOptions
"sse" or "http" => new SseClientTransport(
new SseClientTransportOptions
{
ConnectionTimeout = TimeSpan.FromSeconds(ParseOrDefault(config.TransportOptions, "connectionTimeout", 30)),
MaxReconnectAttempts = ParseOrDefault(config.TransportOptions, "maxReconnectAttempts", 3),
@ -147,6 +151,9 @@ public class McpClientFactory
/// </summary>
private void InitializeCommand(McpServerConfig config)
{
string endpointName = $"Client ({config.Id}: {config.Name})";
// If the command is empty or already contains cmd.exe, we don't need to do anything
var command = config.TransportOptions?.GetValueOrDefault("command");
if (string.IsNullOrEmpty(command))
@ -156,14 +163,14 @@ public class McpClientFactory
if (command.Contains("cmd.exe"))
{
_logger.SkippingShellWrapper(config.Id, config.Name);
_logger.SkippingShellWrapper(endpointName);
return;
}
// If the command is not empty and does not contain cmd.exe, we need to inject /c {command}
if (config.TransportOptions != null && !string.IsNullOrEmpty(command))
{
_logger.PromotingCommandToShellArgumentForStdio(config.Id, config.Name, command, config.TransportOptions.GetValueOrDefault("arguments") ?? "");
_logger.PromotingCommandToShellArgumentForStdio(endpointName, command, config.TransportOptions.GetValueOrDefault("arguments") ?? "");
config.TransportOptions["arguments"] = config.TransportOptions.ContainsKey("arguments")
? $"/c {command} {config.TransportOptions["arguments"]}"
: $"/c {command}";

View file

@ -5,7 +5,7 @@ namespace McpDotNet.Client;
/// <summary>
/// Configuration options for the MCP client. This is passed to servers during the initialization sequence, letting them know about the client's capabilities and
/// protocol version.
/// <see href="https://spec.modelcontextprotocol.io/specification/basic/lifecycle/">See the protocol specification for details on capability negotiation</see>
/// <see href="https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/">See the protocol specification for details on capability negotiation</see>
/// </summary>
public record McpClientOptions
{

View file

@ -2,6 +2,7 @@
/// <summary>
/// Configuration for an MCP server connection.
/// This is passed to the client factory to create a client for a specific server.
/// </summary>
public record McpServerConfig
{

View file

@ -1,4 +1,5 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Messages;
using Microsoft.Extensions.Logging;
namespace McpDotNet.Logging;
@ -17,233 +18,254 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Warning, Message = "Request {requestId} timed out")]
internal static partial void RequestTimeout(this ILogger logger, int requestId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Command for {ServerId} ({Name}) already contains shell wrapper, skipping argument injection")]
internal static partial void SkippingShellWrapper(this ILogger logger, string serverId, string name);
[LoggerMessage(Level = LogLevel.Debug, Message = "Command for {endpointName} already contains shell wrapper, skipping argument injection")]
internal static partial void SkippingShellWrapper(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Server config for Id={serverId} not found")]
[LoggerMessage(Level = LogLevel.Error, Message = "Server connection config for Id={serverId} not found")]
internal static partial void ServerNotFound(this ILogger logger, string serverId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Client for {serverId} ({name}) already created, returning cached client")]
internal static partial void ClientExists(this ILogger logger, string serverId, string name);
[LoggerMessage(Level = LogLevel.Debug, Message = "Client for {endpointName} already created, returning cached client")]
internal static partial void ClientExists(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating client for {serverId} ({name})")]
internal static partial void CreatingClient(this ILogger logger, string serverId, string name);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating client for {endpointName}")]
internal static partial void CreatingClient(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Client for {serverId} ({name}) created and connected")]
internal static partial void ClientCreated(this ILogger logger, string serverId, string name);
[LoggerMessage(Level = LogLevel.Information, Message = "Client for {endpointName} created and connected")]
internal static partial void ClientCreated(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating transport for {serverId} ({name}) with type {transportType} and options {options}")]
internal static partial void CreatingTransport(this ILogger logger, string serverId, string name, string transportType, string options);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating transport for {endpointName} with type {transportType} and options {options}")]
internal static partial void CreatingTransport(this ILogger logger, string endpointName, string transportType, string options);
[LoggerMessage(Level = LogLevel.Debug, Message = "Promoting command for {serverId} ({name}) to shell argument for stdio transport with command {command} and arguments {arguments}")]
internal static partial void PromotingCommandToShellArgumentForStdio(this ILogger logger, string serverId, string name, string command, string arguments);
[LoggerMessage(Level = LogLevel.Debug, Message = "Promoting command for {endpointName} to shell argument for stdio transport with command {command} and arguments {arguments}")]
internal static partial void PromotingCommandToShellArgumentForStdio(this ILogger logger, string endpointName, string command, string arguments);
[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 {serverId} ({serverName}), always set a handler when using this capability")]
internal static partial void SamplingHandlerNotConfigured(this ILogger logger, string serverId, string serverName);
[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 {serverId} ({serverName}), always set a handler when using this capability")]
internal static partial void RootsHandlerNotConfigured(this ILogger logger, string serverId, string serverName);
[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 = "Client server {serverId} ({serverName}) already initializing")]
internal static partial void ClientAlreadyInitializing(this ILogger logger, string serverId, string serverName);
[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);
[LoggerMessage(Level = LogLevel.Information, Message = "Client server {serverId} ({serverName}) already initialized")]
internal static partial void ClientAlreadyInitialized(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Call tool handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void CallToolHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {serverId} ({serverName}) initialization error")]
internal static partial void ClientInitializationError(this ILogger logger, string serverId, string serverName, Exception exception);
[LoggerMessage(Level = LogLevel.Error, Message = "List prompts handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ListPromptsHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Server {serverId} ({serverName}) capabilities received: {capabilities}, server info: {serverInfo}")]
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string serverId, string serverName, string capabilities, string serverInfo);
[LoggerMessage(Level = LogLevel.Error, Message = "Get prompt handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void GetPromptHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Server {serverId} ({serverName}) protocol version mismatch, expected {expected}, received {received}")]
internal static partial void ServerProtocolVersionMismatch(this ILogger logger, string serverId, string serverName, string expected, string received);
[LoggerMessage(Level = LogLevel.Error, Message = "List resources handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ListResourcesHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {serverId} ({serverName}) initialization timeout")]
internal static partial void ClientInitializationTimeout(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Read resource handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ReadResourceHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Pinging server {serverId} ({serverName})")]
internal static partial void PingingServer(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} already initializing")]
internal static partial void ClientAlreadyInitializing(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing tools for server {serverId} ({serverName}) with cursor {cursor}")]
internal static partial void ListingTools(this ILogger logger, string serverId, string serverName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Client server {endpointName} already initialized")]
internal static partial void ClientAlreadyInitialized(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing prompts for server {serverId} ({serverName}) with cursor {cursor}")]
internal static partial void ListingPrompts(this ILogger logger, string serverId, string serverName, string cursor);
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} initialization error")]
internal static partial void ClientInitializationError(this ILogger logger, string endpointName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Getting prompt {name} for server {serverId} ({serverName}) with arguments {arguments}")]
internal static partial void GettingPrompt(this ILogger logger, string serverId, string serverName, string name, string arguments);
[LoggerMessage(Level = LogLevel.Information, Message = "Server {endpointName} capabilities received: {capabilities}, server info: {serverInfo}")]
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string endpointName, string capabilities, string serverInfo);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing resources for server {serverId} ({serverName}) with cursor {cursor}")]
internal static partial void ListingResources(this ILogger logger, string serverId, string serverName, string cursor);
[LoggerMessage(Level = LogLevel.Error, Message = "Server {endpointName} protocol version mismatch, expected {expected}, received {received}")]
internal static partial void ServerProtocolVersionMismatch(this ILogger logger, string endpointName, string expected, string received);
[LoggerMessage(Level = LogLevel.Information, Message = "Reading resource {uri} for server {serverId} ({serverName})")]
internal static partial void ReadingResource(this ILogger logger, string serverId, string serverName, string uri);
[LoggerMessage(Level = LogLevel.Error, Message = "Client {endpointName} initialization timeout")]
internal static partial void ClientInitializationTimeout(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Subscribing to resource {uri} for server {serverId} ({serverName})")]
internal static partial void SubscribingToResource(this ILogger logger, string serverId, string serverName, string uri);
[LoggerMessage(Level = LogLevel.Information, Message = "Pinging sent ({endpointName})")]
internal static partial void PingingServer(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Unsubscribing from resource {uri} for server {serverId} ({serverName})")]
internal static partial void UnsubscribingFromResource(this ILogger logger, string serverId, string serverName, string uri);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing tools for {endpointName} with cursor {cursor}")]
internal static partial void ListingTools(this ILogger logger, string endpointName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Calling tool {toolName} for server {serverId} ({serverName}) with arguments {arguments}")]
internal static partial void CallingTool(this ILogger logger, string serverId, string serverName, string toolName, string arguments);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing prompts for {endpointName} with cursor {cursor}")]
internal static partial void ListingPrompts(this ILogger logger, string endpointName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Client message processing cancelled for server {serverId} ({serverName})")]
internal static partial void ClientMessageProcessingCancelled(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Getting prompt {name} for {endpointName} with arguments {arguments}")]
internal static partial void GettingPrompt(this ILogger logger, string endpointName, string name, string arguments);
[LoggerMessage(Level = LogLevel.Information, Message = "Request handler called for server {serverId} ({serverName}) with method {method}")]
internal static partial void RequestHandlerCalled(this ILogger logger, string serverId, string serverName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing resources for {endpointName} with cursor {cursor}")]
internal static partial void ListingResources(this ILogger logger, string endpointName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Request handler completed for server {serverId} ({serverName}) with method {method}")]
internal static partial void RequestHandlerCompleted(this ILogger logger, string serverId, string serverName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Reading resource {uri} for {endpointName}")]
internal static partial void ReadingResource(this ILogger logger, string endpointName, string uri);
[LoggerMessage(Level = LogLevel.Error, Message = "Request handler error for server {serverId} ({serverName}) with method {method}")]
internal static partial void RequestHandlerError(this ILogger logger, string serverId, string serverName, string method, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Subscribing to resource {uri} for {endpointName}")]
internal static partial void SubscribingToResource(this ILogger logger, string endpointName, string uri);
[LoggerMessage(Level = LogLevel.Warning, Message = "No request found for message with ID {messageWithId} for server {serverId} ({serverName})")]
internal static partial void NoRequestFoundForMessageWithId(this ILogger logger, string serverId, string serverName, string messageWithId);
[LoggerMessage(Level = LogLevel.Information, Message = "Unsubscribing from resource {uri} for {endpointName}")]
internal static partial void UnsubscribingFromResource(this ILogger logger, string endpointName, string uri);
[LoggerMessage(Level = LogLevel.Error, Message = "Notification handler error for server {serverId} ({serverName}) with method {method}")]
internal static partial void NotificationHandlerError(this ILogger logger, string serverId, string serverName, string method, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Calling tool {toolName} for {endpointName} with arguments {arguments}")]
internal static partial void CallingTool(this ILogger logger, string endpointName, string toolName, string arguments);
[LoggerMessage(Level = LogLevel.Error, Message = "Client not connected for server {serverId} ({serverName})")]
internal static partial void ClientNotConnected(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Endpoint message processing cancelled for {endpointName}")]
internal static partial void EndpointMessageProcessingCancelled(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending request payload for server {serverId} ({serverName}): {payload}")]
internal static partial void SendingRequestPayload(this ILogger logger, string serverId, string serverName, string payload);
[LoggerMessage(Level = LogLevel.Information, Message = "Request handler called for {endpointName} with method {method}")]
internal static partial void RequestHandlerCalled(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending request for server {serverId} ({serverName}) with method {method}")]
internal static partial void SendingRequest(this ILogger logger, string serverId, string serverName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Request handler completed for {endpointName} with method {method}")]
internal static partial void RequestHandlerCompleted(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Error, Message = "Request failed for server {serverId} ({serverName}) with method {method}: {message} ({code})")]
internal static partial void RequestFailed(this ILogger logger, string serverId, string serverName, string method, string message, int code);
[LoggerMessage(Level = LogLevel.Error, Message = "Request handler error for {endpointName} with method {method}")]
internal static partial void RequestHandlerError(this ILogger logger, string endpointName, string method, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received payload for server {serverId} ({serverName}): {payload}")]
internal static partial void RequestResponseReceivedPayload(this ILogger logger, string serverId, string serverName, string payload);
[LoggerMessage(Level = LogLevel.Warning, Message = "No request found for message with ID {messageWithId} for {endpointName}")]
internal static partial void NoRequestFoundForMessageWithId(this ILogger logger, string endpointName, string messageWithId);
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received for server {serverId} ({serverName}) with method {method}")]
internal static partial void RequestResponseReceived(this ILogger logger, string serverId, string serverName, string method);
[LoggerMessage(Level = LogLevel.Error, Message = "Notification handler error for {endpointName} with method {method}")]
internal static partial void NotificationHandlerError(this ILogger logger, string endpointName, string method, Exception exception);
[LoggerMessage(Level = LogLevel.Error, Message = "Request response type conversion error for server {serverId} ({serverName}) with method {method}: expected {expectedType}")]
internal static partial void RequestResponseTypeConversionError(this ILogger logger, string serverId, string serverName, string method, Type expectedType);
[LoggerMessage(Level = LogLevel.Error, Message = "Client not connected for {endpointName}")]
internal static partial void ClientNotConnected(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Request invalid response type for server {serverId} ({serverName}) with method {method}")]
internal static partial void RequestInvalidResponseType(this ILogger logger, string serverId, string serverName, string method);
[LoggerMessage(Level = LogLevel.Error, Message = "Endpoint not connected for {endpointName}")]
internal static partial void EndpointNotConnected(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Request params type conversion error for server {serverId} ({serverName}) with method {method}: expected {expectedType}")]
internal static partial void RequestParamsTypeConversionError(this ILogger logger, string serverId, string serverName, string method, Type expectedType);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending request payload for {endpointName}: {payload}")]
internal static partial void SendingRequestPayload(this ILogger logger, string endpointName, string payload);
[LoggerMessage(Level = LogLevel.Information, Message = "Cleaning up client for server {serverId} ({serverName})")]
internal static partial void CleaningUpClient(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending request for {endpointName} with method {method}")]
internal static partial void SendingRequest(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Client cleaned up for server {serverId} ({serverName})")]
internal static partial void ClientCleanedUp(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Request failed for {endpointName} with method {method}: {message} ({code})")]
internal static partial void RequestFailed(this ILogger logger, string endpointName, string method, string message, int code);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for server {serverId} ({serverName}) already connected")]
internal static partial void TransportAlreadyConnected(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received payload for {endpointName}: {payload}")]
internal static partial void RequestResponseReceivedPayload(this ILogger logger, string endpointName, string payload);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for server {serverId} ({serverName}) connecting")]
internal static partial void TransportConnecting(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received for {endpointName} with method {method}")]
internal static partial void RequestResponseReceived(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating process for transport for server {serverId} ({serverName}) with command {command}, arguments {arguments}, environment {environment}, working directory {workingDirectory}, shutdown timeout {shutdownTimeout}")]
internal static partial void CreateProcessForTransport(this ILogger logger, string serverId, string serverName, string command, string arguments, string environment, string workingDirectory, string shutdownTimeout);
[LoggerMessage(Level = LogLevel.Error, Message = "Request response type conversion error for {endpointName} with method {method}: expected {expectedType}")]
internal static partial void RequestResponseTypeConversionError(this ILogger logger, string endpointName, string method, Type expectedType);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport for server {serverId} ({serverName}) error: {data}")]
internal static partial void TransportError(this ILogger logger, string serverId, string serverName, string data);
[LoggerMessage(Level = LogLevel.Error, Message = "Request invalid response type for {endpointName} with method {method}")]
internal static partial void RequestInvalidResponseType(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport process start failed for server {serverId} ({serverName})")]
internal static partial void TransportProcessStartFailed(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Request params type conversion error for {endpointName} with method {method}: expected {expectedType}")]
internal static partial void RequestParamsTypeConversionError(this ILogger logger, string endpointName, string method, Type expectedType);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport process started for server {serverId} ({serverName}) with PID {processId}")]
internal static partial void TransportProcessStarted(this ILogger logger, string serverId, string serverName, int processId);
[LoggerMessage(Level = LogLevel.Information, Message = "Cleaning up endpoint {endpointName}")]
internal static partial void CleaningUpEndpoint(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport reading messages for server {serverId} ({serverName})")]
internal static partial void TransportReadingMessages(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Endpoint cleaned up for {endpointName}")]
internal static partial void EndpointCleanedUp(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport connect failed for server {serverId} ({serverName})")]
internal static partial void TransportConnectFailed(this ILogger logger, string serverId, string serverName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} already connected")]
internal static partial void TransportAlreadyConnected(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport not connected for server {serverId} ({serverName})")]
internal static partial void TransportNotConnected(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} connecting")]
internal static partial void TransportConnecting(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport sending message for server {serverId} ({serverName}) with ID {messageId}, JSON {json}")]
internal static partial void TransportSendingMessage(this ILogger logger, string serverId, string serverName, string messageId, string json);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating process for transport for {endpointName} with command {command}, arguments {arguments}, environment {environment}, working directory {workingDirectory}, shutdown timeout {shutdownTimeout}")]
internal static partial void CreateProcessForTransport(this ILogger logger, string endpointName, string command, string arguments, string environment, string workingDirectory, string shutdownTimeout);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message sent for server {serverId} ({serverName}) with ID {messageId}")]
internal static partial void TransportSentMessage(this ILogger logger, string serverId, string serverName, string messageId);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport for {endpointName} error: {data}")]
internal static partial void TransportError(this ILogger logger, string endpointName, string data);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport send failed for server {serverId} ({serverName}) with ID {messageId}")]
internal static partial void TransportSendFailed(this ILogger logger, string serverId, string serverName, string messageId, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport process start failed for {endpointName}")]
internal static partial void TransportProcessStartFailed(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport entering read messages loop for server {serverId} ({serverName})")]
internal static partial void TransportEnteringReadMessagesLoop(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport process started for {endpointName} with PID {processId}")]
internal static partial void TransportProcessStarted(this ILogger logger, string endpointName, int processId);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport waiting for message for server {serverId} ({serverName})")]
internal static partial void TransportWaitingForMessage(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport reading messages for {endpointName}")]
internal static partial void TransportReadingMessages(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport end of stream for server {serverId} ({serverName})")]
internal static partial void TransportEndOfStream(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport connect failed for {endpointName}")]
internal static partial void TransportConnectFailed(this ILogger logger, string endpointName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport received message for server {serverId} ({serverName}): {line}")]
internal static partial void TransportReceivedMessage(this ILogger logger, string serverId, string serverName, string line);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport not connected for {endpointName}")]
internal static partial void TransportNotConnected(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport received message parsed for server {serverId} ({serverName}): {messageId}")]
internal static partial void TransportReceivedMessageParsed(this ILogger logger, string serverId, string serverName, string messageId);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport sending message for {endpointName} with ID {messageId}, JSON {json}")]
internal static partial void TransportSendingMessage(this ILogger logger, string endpointName, string messageId, string json);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message written for server {serverId} ({serverName}) with ID {messageId}")]
internal static partial void TransportMessageWritten(this ILogger logger, string serverId, string serverName, string messageId);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message sent for {endpointName} with ID {messageId}")]
internal static partial void TransportSentMessage(this ILogger logger, string endpointName, string messageId);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport message parse failed due to unexpected message schema for server {serverId} ({serverName}): {line}")]
internal static partial void TransportMessageParseUnexpectedType(this ILogger logger, string serverId, string serverName, string line);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport send failed for {endpointName} with ID {messageId}")]
internal static partial void TransportSendFailed(this ILogger logger, string endpointName, string messageId, Exception exception);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport message parse failed for server {serverId} ({serverName}): {line}")]
internal static partial void TransportMessageParseFailed(this ILogger logger, string serverId, string serverName, string line, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport entering read messages loop for {endpointName}")]
internal static partial void TransportEnteringReadMessagesLoop(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport exiting read messages loop for server {serverId} ({serverName})")]
internal static partial void TransportExitingReadMessagesLoop(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport waiting for message for {endpointName}")]
internal static partial void TransportWaitingForMessage(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read messages cancelled for server {serverId} ({serverName})")]
internal static partial void TransportReadMessagesCancelled(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport end of stream for {endpointName}")]
internal static partial void TransportEndOfStream(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport read messages failed for server {serverId} ({serverName})")]
internal static partial void TransportReadMessagesFailed(this ILogger logger, string serverId, string serverName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport received message for {endpointName}: {line}")]
internal static partial void TransportReceivedMessage(this ILogger logger, string endpointName, string line);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaning up for server {serverId} ({serverName})")]
internal static partial void TransportCleaningUp(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport received message parsed for {endpointName}: {messageId}")]
internal static partial void TransportReceivedMessageParsed(this ILogger logger, string endpointName, string messageId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport closing stdin for server {serverId} ({serverName})")]
internal static partial void TransportClosingStdin(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport message written for {endpointName} with ID {messageId}")]
internal static partial void TransportMessageWritten(this ILogger logger, string endpointName, string messageId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for shutdown for server {serverId} ({serverName})")]
internal static partial void TransportWaitingForShutdown(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport message parse failed due to unexpected message schema for {endpointName}: {line}")]
internal static partial void TransportMessageParseUnexpectedType(this ILogger logger, string endpointName, string line);
[LoggerMessage(Level = LogLevel.Warning, Message = "Transport killing process for server {serverId} ({serverName})")]
internal static partial void TransportKillingProcess(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport message parse failed for {endpointName}: {line}")]
internal static partial void TransportMessageParseFailed(this ILogger logger, string endpointName, string line, Exception exception);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport shutdown failed for server {serverId} ({serverName})")]
internal static partial void TransportShutdownFailed(this ILogger logger, string serverId, string serverName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport exiting read messages loop for {endpointName}")]
internal static partial void TransportExitingReadMessagesLoop(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for read task for server {serverId} ({serverName})")]
internal static partial void TransportWaitingForReadTask(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read messages cancelled for {endpointName}")]
internal static partial void TransportReadMessagesCancelled(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Warning, Message = "Transport cleanup read task timeout for server {serverId} ({serverName})")]
internal static partial void TransportCleanupReadTaskTimeout(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport read messages failed for {endpointName}")]
internal static partial void TransportReadMessagesFailed(this ILogger logger, string endpointName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleanup read task cancelled for server {serverId} ({serverName})")]
internal static partial void TransportCleanupReadTaskCancelled(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaning up for {endpointName}")]
internal static partial void TransportCleaningUp(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport cleanup read task failed for server {serverId} ({serverName})")]
internal static partial void TransportCleanupReadTaskFailed(this ILogger logger, string serverId, string serverName, Exception exception);
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport closing stdin for {endpointName}")]
internal static partial void TransportClosingStdin(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read task cleaned up for server {serverId} ({serverName})")]
internal static partial void TransportReadTaskCleanedUp(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for shutdown for {endpointName}")]
internal static partial void TransportWaitingForShutdown(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaned up for server {serverId} ({serverName})")]
internal static partial void TransportCleanedUp(this ILogger logger, string serverId, string serverName);
[LoggerMessage(Level = LogLevel.Warning, Message = "Transport killing process for {endpointName}")]
internal static partial void TransportKillingProcess(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport shutdown failed for {endpointName}")]
internal static partial void TransportShutdownFailed(this ILogger logger, string endpointName, Exception exception);
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for read task for {endpointName}")]
internal static partial void TransportWaitingForReadTask(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Warning, Message = "Transport cleanup read task timeout for {endpointName}")]
internal static partial void TransportCleanupReadTaskTimeout(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleanup read task cancelled for {endpointName}")]
internal static partial void TransportCleanupReadTaskCancelled(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport cleanup read task failed for {endpointName}")]
internal static partial void TransportCleanupReadTaskFailed(this ILogger logger, string endpointName, Exception exception);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read task cleaned up for {endpointName}")]
internal static partial void TransportReadTaskCleanedUp(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Transport cleaned up for {endpointName}")]
internal static partial void TransportCleanedUp(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message start object token expected")]
internal static partial void JsonRpcMessageConverterExpectedStartObjectToken(this ILogger logger);
@ -272,105 +294,191 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message write unknown message type: {type}")]
internal static partial void JsonRpcMessageConverterWriteUnknownMessageType(this ILogger logger, string type);
[LoggerMessage(Level = LogLevel.Debug, Message = "Sending notification payload for {serverId} ({serverName}): {payload}")]
internal static partial void SendingNotificationPayload(this ILogger logger, string serverId, string serverName, string payload);
[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.Information, Message = "Sending notification for {serverId} ({serverName}): {method}")]
internal static partial void SendingNotification(this ILogger logger, string serverId, string serverName, string method);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending notification for {endpointName}: {method}")]
internal static partial void SendingNotification(this ILogger logger, string endpointName, string method);
[LoggerMessage(
EventId = 7000,
Level = LogLevel.Error,
Message = "Transport connection error for server {serverId} ({serverName})"
)]
EventId = 7000,
Level = LogLevel.Error,
Message = "Transport connection error for {endpointName}"
)]
public static partial void TransportConnectionError(
this ILogger logger,
string serverId,
string serverName,
Exception exception);
this ILogger logger,
string endpointName,
Exception exception);
[LoggerMessage(
EventId = 7001,
Level = LogLevel.Warning,
Message = "Transport message received before connected for server {serverId} ({serverName}): {data}"
Message = "Transport message received before connected for {endpointName}: {data}"
)]
public static partial void TransportMessageReceivedBeforeConnected(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string data);
[LoggerMessage(
EventId = 7002,
Level = LogLevel.Error,
Message = "Transport endpoint event received out of order for server {serverId} ({serverName}): {data}"
Message = "Transport endpoint event received out of order for {endpointName}: {data}"
)]
public static partial void TransportEndpointEventInvalid(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string data);
[LoggerMessage(
EventId = 7003,
Level = LogLevel.Error,
Message = "Transport event parse failed for server {serverId} ({serverName}): {data}"
Message = "Transport event parse failed for {endpointName}: {data}"
)]
public static partial void TransportEndpointEventParseFailed(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string data,
Exception exception);
// trace _logger.InvalidCompletionReference(_serverConfig.Id, _serverConfig.Name, reference, validationMessage);
[LoggerMessage(
EventId = 7004,
Level = LogLevel.Trace,
Message = "Invalid completion reference {reference} for server {serverId} ({serverName}): {validationMessage}"
Message = "Invalid completion reference {reference} for {endpointName}: {validationMessage}"
)]
public static partial void InvalidCompletionReference(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string reference,
string validationMessage);
// trace _logger.InvalidACompletionArgumentName(_serverConfig.Id, _serverConfig.Name, argumentName);
[LoggerMessage(
EventId = 7005,
Level = LogLevel.Trace,
Message = "Invalid completion argument name {argumentName} for server {serverId} ({serverName})"
Message = "Invalid completion argument name {argumentName} for {endpointName}"
)]
public static partial void InvalidCompletionArgumentName(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string argumentName);
// trace _logger.InvalidCompletionArgumentValue(_serverConfig.Id, _serverConfig.Name, argumentValue);
[LoggerMessage(
EventId = 7006,
Level = LogLevel.Trace,
Message = "Invalid completion argument value {argumentValue} for server {serverId} ({serverName})"
Message = "Invalid completion argument value {argumentValue} for {endpointName}"
)]
public static partial void InvalidCompletionArgumentValue(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string argumentValue);
// debug _logger.GettingCompletion(_serverConfig.Id, _serverConfig.Name, reference, argumentName, argumentValue);
[LoggerMessage(
EventId = 7007,
Level = LogLevel.Debug,
Message = "Getting completion for server {serverId} ({serverName}) with reference {reference}, argument name {argumentName}, argument value {argumentValue}"
Message = "Getting completion for {endpointName} with reference {reference}, argument name {argumentName}, argument value {argumentValue}"
)]
public static partial void GettingCompletion(
this ILogger logger,
string serverId,
string serverName,
string endpointName,
string reference,
string argumentName,
string argumentValue);
[LoggerMessage(
EventId = 7008,
Level = LogLevel.Error,
Message = "Message handler error for {endpointName} with message type {messageType}, payload {payload}"
)]
public static partial void MessageHandlerError(
this ILogger logger,
string endpointName,
string messageType,
string payload,
Exception exception);
[LoggerMessage(
EventId = 7009,
Level = LogLevel.Trace,
Message = "Writing message to channel: {message}"
)]
public static partial void TransportWritingMessageToChannel(
this ILogger logger,
IJsonRpcMessage message);
[LoggerMessage(
EventId = 7010,
Level = LogLevel.Trace,
Message = "Message written to channel"
)]
public static partial void TransportMessageWrittenToChannel(this ILogger logger);
[LoggerMessage(
EventId = 7011,
Level = LogLevel.Trace,
Message = "Message read from channel for {endpointName} with type {messageType}"
)]
public static partial void TransportMessageRead(
this ILogger logger,
string endpointName,
string messageType);
[LoggerMessage(
EventId = 7012,
Level = LogLevel.Warning,
Message = "No handler found for request {method} for server {endpointName}"
)]
public static partial void NoHandlerFoundForRequest(
this ILogger logger,
string endpointName,
string method);
[LoggerMessage(
EventId = 7013,
Level = LogLevel.Trace,
Message = "Response matched pending request for {endpointName} with ID {messageId}"
)]
public static partial void ResponseMatchedPendingRequest(
this ILogger logger,
string endpointName,
string messageId);
[LoggerMessage(
EventId = 7014,
Level = LogLevel.Warning,
Message = "Endpoint handler received unexpected message type for {endpointName}: {messageType}"
)]
public static partial void EndpointHandlerUnexpectedMessageType(
this ILogger logger,
string endpointName,
string messageType);
[LoggerMessage(
EventId = 7015,
Level = LogLevel.Debug,
Message = "Request sent for {endpointName} with method {method}, ID {id}. Waiting for response."
)]
public static partial void RequestSentAwaitingResponse(
this ILogger logger,
string endpointName,
string method,
string id);
[LoggerMessage(
EventId = 7016,
Level = LogLevel.Error,
Message = "{endpointName} already initializing"
)]
public static partial void ServerAlreadyInitializing(
this ILogger logger,
string endpointName);
[LoggerMessage(
EventId = 7017,
Level = LogLevel.Error,
Message = "{endpointName} initialization error"
)]
public static partial void ServerInitializationError(
this ILogger logger,
string endpointName,
Exception e);
}

View file

@ -0,0 +1,13 @@
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Represents a transport mechanism for MCP communication (from the client).
/// </summary>
public interface IClientTransport : ITransport
{
/// <summary>
/// Establishes the transport connection.
/// </summary>
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task ConnectAsync(CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,13 @@
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Represents a transport mechanism for MCP communication (from the server).
/// </summary>
public interface IServerTransport : ITransport
{
/// <summary>
/// Starts listening for incoming messages.
/// </summary>
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task StartListeningAsync(CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,27 @@
using System.Threading.Channels;
using McpDotNet.Protocol.Messages;
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Represents a transport mechanism for MCP communication.
/// </summary>
public interface ITransport : IAsyncDisposable
{
/// <summary>
/// Gets whether the transport is currently connected.
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Channel for receiving messages from the transport.
/// </summary>
ChannelReader<IJsonRpcMessage> MessageReader { get; }
/// <summary>
/// Sends a message through the transport.
/// </summary>
/// <param name="message">The message to send.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,308 @@

using System.Diagnostics.Tracing;
using System.Text.Json;
using System.Text;
using McpDotNet.Protocol.Messages;
using McpDotNet.Configuration;
using Microsoft.Extensions.Logging;
using McpDotNet.Logging;
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using McpDotNet.Utils.Json;
using System.Threading;
namespace McpDotNet.Protocol.Transport;
/// <summary>
///
/// </summary>
public sealed class SseClientTransport : TransportBase, IClientTransport
{
private readonly HttpClient _httpClient;
private readonly SseClientTransportOptions _options;
private readonly Uri _sseEndpoint;
private Uri? _messageEndpoint;
private readonly CancellationTokenSource _connectionCts;
private Task? _receiveTask;
private readonly ILogger<SseClientTransport> _logger;
private readonly McpServerConfig _serverConfig;
private readonly JsonSerializerOptions _jsonOptions;
private TaskCompletionSource<bool> _connectionEstablished;
private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})";
/// <summary>
/// SSE transport for client endpoints. Unlike stdio it does not launch a process, but connects to an existing server.
/// The HTTP server can be local or remote, and must support the SSE protocol.
/// </summary>
/// <param name="transportOptions">Configuration options for the transport.</param>
/// <param name="serverConfig">The configuration object indicating which server to connect to.</param>
/// <param name="loggerFactory">Logger factory for creating loggers.</param>
public SseClientTransport(SseClientTransportOptions transportOptions, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
: base(loggerFactory)
{
_options = transportOptions;
_serverConfig = serverConfig;
_sseEndpoint = new Uri(serverConfig.Location!);
_httpClient = new HttpClient();
_connectionCts = new CancellationTokenSource();
_logger = loggerFactory.CreateLogger<SseClientTransport>();
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
_connectionEstablished = new TaskCompletionSource<bool>();
}
/// <inheritdoc/>
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
try
{
if (IsConnected)
{
_logger.TransportAlreadyConnected(EndpointName);
throw new McpTransportException("Transport is already connected");
}
// Start message receiving loop
_receiveTask = ReceiveMessagesAsync(_connectionCts.Token);
_logger.TransportReadingMessages(EndpointName);
await Task.WhenAny(
_connectionEstablished.Task,
Task.Delay(_options.ConnectionTimeout, cancellationToken)
);
if (!IsConnected)
throw new TimeoutException("Failed to receive endpoint event");
}
catch (McpTransportException)
{
// Rethrow transport exceptions
throw;
}
catch (Exception ex)
{
_logger.TransportConnectFailed(EndpointName, ex);
await CloseAsync();
throw new McpTransportException("Failed to connect transport", ex);
}
}
/// <inheritdoc/>
public override async Task SendMessageAsync(
IJsonRpcMessage message,
CancellationToken cancellationToken = default)
{
if (_messageEndpoint == null)
throw new InvalidOperationException("Transport not connected");
var content = new StringContent(
JsonSerializer.Serialize(message, _jsonOptions),
Encoding.UTF8,
"application/json"
);
var response = await _httpClient.PostAsync(
_messageEndpoint,
content,
cancellationToken
);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
// Handle notifications, which don't have responses
if (message is JsonRpcNotification)
{
await HandleNotificationResponseAsync(message, responseContent, cancellationToken);
return;
}
var responseMessage = JsonSerializer.Deserialize<IJsonRpcMessage>(responseContent, _jsonOptions);
if (responseMessage != null)
{
string messageId = "(no id)";
if (responseMessage is IJsonRpcMessageWithId messageWithId)
{
messageId = messageWithId.Id.ToString();
}
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
await WriteMessageAsync(responseMessage, cancellationToken);
_logger.TransportMessageWritten(EndpointName, messageId);
}
else
{
_logger.TransportMessageParseUnexpectedType(EndpointName, responseContent);
}
}
/// <inheritdoc/>
public async Task CloseAsync()
{
_connectionCts.Cancel();
if (_receiveTask != null)
await _receiveTask;
_httpClient.Dispose();
_connectionCts.Dispose();
SetConnected(false);
}
/// <inheritdoc/>
public override async ValueTask DisposeAsync()
{
await CloseAsync();
GC.SuppressFinalize(this);
}
private async Task HandleNotificationResponseAsync(
IJsonRpcMessage message,
string responseContent,
CancellationToken cancellationToken)
{
// Check for error response, but don't require one
try
{
var errorResponse = JsonSerializer.Deserialize<JsonRpcError>(responseContent, _jsonOptions);
if (errorResponse is not null)
{
await WriteMessageAsync(errorResponse, cancellationToken);
return;
}
}
catch (JsonException)
{
// Ignore deserialization errors for notifications
// This is expected as most notifications won't have responses
}
}
internal Uri? MessageEndpoint => _messageEndpoint;
internal SseClientTransportOptions Options => _options;
private async Task ReceiveMessagesAsync(CancellationToken cancellationToken)
{
int reconnectAttempts = 0;
while (!cancellationToken.IsCancellationRequested)
{
try
{
var request = new HttpRequestMessage(HttpMethod.Get, _sseEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
using var response = await _httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken
);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(stream);
// Reset reconnect attempts on successful connection
reconnectAttempts = 0;
string? currentEvent = null;
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event: "))
{
currentEvent = line.Substring(7);
}
else if (line.StartsWith("data: "))
{
var data = line.Substring(6);
if (currentEvent == "endpoint")
{
HandleEndpointEvent(data);
}
else
{
await ProcessSseMessage(data, cancellationToken);
}
}
}
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
_logger.TransportConnectionError(EndpointName, ex);
reconnectAttempts++;
if (reconnectAttempts >= _options.MaxReconnectAttempts)
{
throw new McpTransportException("Exceeded reconnect limit", ex);
}
await Task.Delay(_options.ReconnectDelay, cancellationToken);
}
}
}
private async Task ProcessSseMessage(string data, CancellationToken cancellationToken)
{
if (!IsConnected)
{
_logger.TransportMessageReceivedBeforeConnected(EndpointName, data);
return;
}
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(data, _jsonOptions);
if (message == null)
{
_logger.TransportMessageParseUnexpectedType(EndpointName, data);
return;
}
string messageId = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
messageId = messageWithId.Id.ToString();
}
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
await WriteMessageAsync(message, cancellationToken);
_logger.TransportMessageWritten(EndpointName, messageId);
}
catch (JsonException ex)
{
_logger.TransportMessageParseFailed(EndpointName, data, ex);
}
}
private void HandleEndpointEvent(string data)
{
try
{
var endpointData = JsonSerializer.Deserialize<EndpointEventData>(data, _jsonOptions);
if (endpointData?.Uri == null)
{
_logger.TransportEndpointEventInvalid(EndpointName, data);
return;
}
_messageEndpoint = new Uri(endpointData.Uri);
SetConnected(true);
_connectionEstablished.TrySetResult(true);
}
catch (JsonException ex)
{
_logger.TransportEndpointEventParseFailed(EndpointName, data, ex);
throw new McpTransportException("Failed to parse endpoint event", ex);
}
}
private record EndpointEventData
{
[JsonPropertyName("uri")]
public string? Uri { get; init; }
}
}

View file

@ -0,0 +1,27 @@
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Options for configuring the SSE transport.
/// </summary>
public record SseClientTransportOptions
{
/// <summary>
/// Timeout for initial connection and endpoint event.
/// </summary>
public TimeSpan ConnectionTimeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Number of reconnection attempts for SSE connection.
/// </summary>
public int MaxReconnectAttempts { get; init; } = 3;
/// <summary>
/// Delay between reconnection attempts.
/// </summary>
public TimeSpan ReconnectDelay { get; init; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Headers to include in HTTP requests.
/// </summary>
public Dictionary<string, string>? AdditionalHeaders { get; init; }
}

View file

@ -0,0 +1,289 @@
using System.Diagnostics;
using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Messages;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging;
using McpDotNet.Logging;
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Implements the MCP transport protocol over standard input/output streams.
/// </summary>
public sealed class StdioClientTransport : TransportBase, IClientTransport
{
private readonly StdioClientTransportOptions _options;
private readonly McpServerConfig _serverConfig;
private readonly ILogger<StdioClientTransport> _logger;
private readonly JsonSerializerOptions _jsonOptions;
private Process? _process;
private Task? _readTask;
private CancellationTokenSource? _shutdownCts;
private bool _processStarted;
private string EndpointName => $"Client (stdio) for ({_serverConfig.Id}: {_serverConfig.Name})";
/// <summary>
/// Initializes a new instance of the StdioTransport class.
/// </summary>
/// <param name="options">Configuration options for the transport.</param>
/// <param name="serverConfig">The server configuration for the transport.</param>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
public StdioClientTransport(StdioClientTransportOptions options, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
: base(loggerFactory)
{
_options = options;
_serverConfig = serverConfig;
_logger = loggerFactory.CreateLogger<StdioClientTransport>();
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
}
/// <inheritdoc/>
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
if (IsConnected)
{
_logger.TransportAlreadyConnected(EndpointName);
throw new McpTransportException("Transport is already connected");
}
try
{
_logger.TransportConnecting(EndpointName);
_shutdownCts = new CancellationTokenSource();
var startInfo = new ProcessStartInfo
{
FileName = _options.Command,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
};
if (_options.Arguments != null)
{
foreach (var arg in _options.Arguments)
{
startInfo.ArgumentList.Add(arg);
}
}
if (_options.EnvironmentVariables != null)
{
foreach (var (key, value) in _options.EnvironmentVariables)
{
startInfo.Environment[key] = value;
}
}
_logger.CreateProcessForTransport(EndpointName, _options.Command,
string.Join(", ", startInfo.ArgumentList), string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)),
startInfo.WorkingDirectory, _options.ShutdownTimeout.ToString());
_process = new Process { StartInfo = startInfo };
// Set up error logging
_process.ErrorDataReceived += (sender, args) =>
{
_logger.TransportError(EndpointName, args.Data ?? "(no data)");
};
if (!_process.Start())
{
_logger.TransportProcessStartFailed(EndpointName);
throw new McpTransportException("Failed to start MCP server process");
}
_logger.TransportProcessStarted(EndpointName, _process.Id);
_processStarted = true;
_process.BeginErrorReadLine();
// Start reading messages in the background
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token));
_logger.TransportReadingMessages(EndpointName);
SetConnected(true);
}
catch (Exception ex)
{
_logger.TransportConnectFailed(EndpointName, ex);
await CleanupAsync(cancellationToken);
throw new McpTransportException("Failed to connect transport", ex);
}
}
/// <inheritdoc/>
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
if (!IsConnected || _process?.HasExited == true)
{
_logger.TransportNotConnected(EndpointName);
throw new McpTransportException("Transport is not connected");
}
string id = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
id = messageWithId.Id.ToString();
}
try
{
var json = JsonSerializer.Serialize(message, _jsonOptions);
_logger.TransportSendingMessage(EndpointName, id, json);
// Write the message followed by a newline
await _process!.StandardInput.WriteLineAsync(json.AsMemory(), cancellationToken);
await _process.StandardInput.FlushAsync();
_logger.TransportSentMessage(EndpointName, id);
}
catch (Exception ex)
{
_logger.TransportSendFailed(EndpointName, id, ex);
throw new McpTransportException("Failed to send message", ex);
}
}
/// <inheritdoc/>
public override async ValueTask DisposeAsync()
{
await CleanupAsync(CancellationToken.None);
GC.SuppressFinalize(this);
}
private async Task ReadMessagesAsync(CancellationToken cancellationToken)
{
try
{
_logger.TransportEnteringReadMessagesLoop(EndpointName);
using var reader = _process!.StandardOutput;
while (!cancellationToken.IsCancellationRequested && !_process.HasExited)
{
_logger.TransportWaitingForMessage(EndpointName);
var line = await reader.ReadLineAsync(cancellationToken);
if (line == null)
{
_logger.TransportEndOfStream(EndpointName);
break;
}
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
_logger.TransportReceivedMessage(EndpointName, line);
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
if (message != null)
{
string messageId = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
messageId = messageWithId.Id.ToString();
}
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
await WriteMessageAsync(message, cancellationToken);
_logger.TransportMessageWritten(EndpointName, messageId);
}
else
{
_logger.TransportMessageParseUnexpectedType(EndpointName, line);
}
}
catch (JsonException ex)
{
_logger.TransportMessageParseFailed(EndpointName, line, ex);
// Continue reading even if we fail to parse a message
}
}
_logger.TransportExitingReadMessagesLoop(EndpointName);
}
catch (OperationCanceledException)
{
_logger.TransportReadMessagesCancelled(EndpointName);
// Normal shutdown
}
catch (Exception ex)
{
_logger.TransportReadMessagesFailed(EndpointName, ex);
}
finally
{
await CleanupAsync(cancellationToken);
}
}
private async Task CleanupAsync(CancellationToken cancellationToken)
{
_logger.TransportCleaningUp(EndpointName);
if (_process != null && _processStarted && !_process.HasExited)
{
try
{
// Try to close stdin to signal the process to exit
_logger.TransportClosingStdin(EndpointName);
_process.StandardInput.Close();
// Wait for the process to exit
_logger.TransportWaitingForShutdown(EndpointName);
if (!_process.WaitForExit((int)_options.ShutdownTimeout.TotalMilliseconds))
{
// If it doesn't exit gracefully, terminate it
_logger.TransportKillingProcess(EndpointName);
_process.Kill(true);
}
}
catch (Exception ex)
{
_logger.TransportShutdownFailed(EndpointName, ex);
}
_process.Dispose();
_process = null;
}
_shutdownCts?.Cancel();
_shutdownCts?.Dispose();
_shutdownCts = null;
if (_readTask != null)
{
try
{
_logger.TransportWaitingForReadTask(EndpointName);
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
}
catch (TimeoutException)
{
_logger.TransportCleanupReadTaskTimeout(EndpointName);
// Continue with cleanup
}
catch (OperationCanceledException)
{
_logger.TransportCleanupReadTaskCancelled(EndpointName);
// Ignore cancellation
}
catch (Exception ex)
{
_logger.TransportCleanupReadTaskFailed(EndpointName, ex);
}
_readTask = null;
_logger.TransportReadTaskCleanedUp(EndpointName);
}
SetConnected(false);
_logger.TransportCleanedUp(EndpointName);
}
}

View file

@ -0,0 +1,33 @@
// Protocol/Transport/StdioTransport.cs
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Represents configuration options for the stdio transport.
/// </summary>
public record StdioClientTransportOptions
{
/// <summary>
/// The command to execute to start the server process.
/// </summary>
public required string Command { get; set; }
/// <summary>
/// Arguments to pass to the server process.
/// </summary>
public string[]? Arguments { get; set; } = Array.Empty<string>();
/// <summary>
/// The working directory for the server process.
/// </summary>
public string? WorkingDirectory { get; set; }
/// <summary>
/// Environment variables to set for the server process.
/// </summary>
public Dictionary<string, string>? EnvironmentVariables { get; set; }
/// <summary>
/// The timeout to wait for the server to shut down gracefully.
/// </summary>
public TimeSpan ShutdownTimeout { get; init; } = TimeSpan.FromSeconds(5);
}

View file

@ -0,0 +1,195 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using McpDotNet.Protocol.Messages;
using McpDotNet.Utils.Json;
using McpDotNet.Logging;
namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Implements the MCP transport protocol over standard input/output streams.
/// </summary>
public sealed class StdioServerTransport : TransportBase, IServerTransport
{
private readonly string _serverName;
private readonly ILogger<StdioClientTransport> _logger;
private readonly JsonSerializerOptions _jsonOptions;
private Task? _readTask;
private CancellationTokenSource? _shutdownCts;
private string EndpointName => $"Server (stdio) ({_serverName})";
/// <summary>
/// Initializes a new instance of the StdioServerTransport class.
/// </summary>
/// <param name="serverName">The name of the server.</param>
/// <param name="loggerFactory">A logger factory for creating loggers.</param>
public StdioServerTransport(string serverName, ILoggerFactory loggerFactory)
: base(loggerFactory)
{
_serverName = serverName;
_logger = loggerFactory.CreateLogger<StdioClientTransport>();
_jsonOptions = new JsonSerializerOptions().ConfigureForMcp(loggerFactory);
}
/// <inheritdoc/>
public Task StartListeningAsync(CancellationToken cancellationToken = default)
{
_shutdownCts = new CancellationTokenSource();
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token));
SetConnected(true);
return Task.CompletedTask;
}
/// <inheritdoc/>
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
{
if (!IsConnected)
{
_logger.TransportNotConnected(EndpointName);
throw new McpTransportException("Transport is not connected");
}
string id = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
id = messageWithId.Id.ToString();
}
try
{
var json = JsonSerializer.Serialize(message, _jsonOptions);
_logger.TransportSendingMessage(EndpointName, id, json);
using (Console.OpenStandardOutput())
{
await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken);
await Console.Out.FlushAsync();
}
_logger.TransportSentMessage(EndpointName, id);
}
catch (Exception ex)
{
_logger.TransportSendFailed(EndpointName, id, ex);
throw new McpTransportException("Failed to send message", ex);
}
}
/// <inheritdoc/>
public override async ValueTask DisposeAsync()
{
await CleanupAsync(CancellationToken.None);
GC.SuppressFinalize(this);
}
private async Task ReadMessagesAsync(CancellationToken cancellationToken)
{
try
{
_logger.TransportEnteringReadMessagesLoop(EndpointName);
while (!cancellationToken.IsCancellationRequested)
{
_logger.TransportWaitingForMessage(EndpointName);
using (Console.OpenStandardInput())
{
var reader = Console.In;
var line = await reader.ReadLineAsync(cancellationToken);
if (line == null)
{
_logger.TransportEndOfStream(EndpointName);
break;
}
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
_logger.TransportReceivedMessage(EndpointName, line);
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
if (message != null)
{
string messageId = "(no id)";
if (message is IJsonRpcMessageWithId messageWithId)
{
messageId = messageWithId.Id.ToString();
}
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
await WriteMessageAsync(message, cancellationToken);
_logger.TransportMessageWritten(EndpointName, messageId);
}
else
{
_logger.TransportMessageParseUnexpectedType(EndpointName, line);
}
}
catch (JsonException ex)
{
_logger.TransportMessageParseFailed(EndpointName, line, ex);
// Continue reading even if we fail to parse a message
}
}
}
_logger.TransportExitingReadMessagesLoop(EndpointName);
}
catch (OperationCanceledException)
{
_logger.TransportReadMessagesCancelled(EndpointName);
// Normal shutdown
}
catch (Exception ex)
{
_logger.TransportReadMessagesFailed(EndpointName, ex);
}
finally
{
await CleanupAsync(cancellationToken);
}
}
private async Task CleanupAsync(CancellationToken cancellationToken)
{
_logger.TransportCleaningUp(EndpointName);
_shutdownCts?.Cancel();
_shutdownCts?.Dispose();
_shutdownCts = null;
if (_readTask != null)
{
try
{
_logger.TransportWaitingForReadTask(EndpointName);
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
}
catch (TimeoutException)
{
_logger.TransportCleanupReadTaskTimeout(EndpointName);
// Continue with cleanup
}
catch (OperationCanceledException)
{
_logger.TransportCleanupReadTaskCancelled(EndpointName);
// Ignore cancellation
}
catch (Exception ex)
{
_logger.TransportCleanupReadTaskFailed(EndpointName, ex);
}
_readTask = null;
_logger.TransportReadTaskCleanedUp(EndpointName);
}
SetConnected(false);
_logger.TransportCleanedUp(EndpointName);
}
}

View file

@ -1,4 +1,6 @@
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
using McpDotNet.Logging;
using McpDotNet.Protocol.Messages;
namespace McpDotNet.Protocol.Transport;
@ -6,15 +8,16 @@ namespace McpDotNet.Protocol.Transport;
/// <summary>
/// Base class for implementing MCP transports with common functionality.
/// </summary>
public abstract class TransportBase : IMcpTransport
public abstract class TransportBase : ITransport
{
private readonly Channel<IJsonRpcMessage> _messageChannel;
private readonly ILogger<TransportBase> _logger;
private bool _isConnected;
/// <summary>
/// Initializes a new instance of the <see cref="TransportBase"/> class.
/// </summary>
protected TransportBase()
protected TransportBase(ILoggerFactory loggerFactory)
{
// Unbounded channel to prevent blocking on writes
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
@ -22,6 +25,7 @@ public abstract class TransportBase : IMcpTransport
SingleReader = true,
SingleWriter = true,
});
_logger = loggerFactory.CreateLogger<TransportBase>();
}
/// <inheritdoc/>
@ -30,9 +34,6 @@ public abstract class TransportBase : IMcpTransport
/// <inheritdoc/>
public ChannelReader<IJsonRpcMessage> MessageReader => _messageChannel.Reader;
/// <inheritdoc/>
public abstract Task ConnectAsync(CancellationToken cancellationToken = default);
/// <inheritdoc/>
public abstract Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
@ -51,7 +52,9 @@ public abstract class TransportBase : IMcpTransport
throw new McpTransportException("Transport is not connected");
}
_logger.TransportWritingMessageToChannel(message);
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
_logger.TransportMessageWrittenToChannel();
}
/// <summary>

View file

@ -1,9 +1,10 @@

using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Represents annotations that can be attached to content.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record Annotations
{

View file

@ -4,6 +4,7 @@ namespace McpDotNet.Protocol.Types;
/// <summary>
/// Used for completion requests to provide additional context for the completion options.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class Argument
{

View file

@ -0,0 +1,20 @@
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Used by the client to invoke a tool provided by the server.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class CallToolRequestParams
{
/// <summary>
/// Tool name.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("name")]
public required string Name { get; init; }
/// <summary>
/// Optional arguments to pass to the tool.
/// </summary>
[System.Text.Json.Serialization.JsonPropertyName("arguments")]
public Dictionary<string,object>? Arguments { get; init; }
}

View file

@ -1,8 +1,17 @@
namespace McpDotNet.Protocol.Types;
/// <summary>
/// A response to a request to call a tool on the server.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/schema.json">See the schema for details</see>
/// The server's response to a tool call.
///
/// Any errors that originate from the tool SHOULD be reported inside the result
/// object, with `isError` set to true, _not_ as an MCP protocol-level error
/// response. Otherwise, the LLM would not be able to see that an error occurred
/// and self-correct.
///
/// However, any errors in _finding_ the tool, an error indicating that the
/// server does not support tool calls, or any other exceptional conditions,
/// should be reported as an MCP error response.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public class CallToolResponse
{

View file

@ -1,9 +1,9 @@

using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Types;
/// <summary>
/// Represents the capabilities that a client may support.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record ClientCapabilities
{
@ -28,6 +28,7 @@ public record ClientCapabilities
/// <summary>
/// Represents the roots capability configuration.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record RootsCapability
{
@ -40,6 +41,7 @@ public record RootsCapability
/// <summary>
/// Represents the sampling capability configuration.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record SamplingCapability
{
@ -48,6 +50,7 @@ public record SamplingCapability
/// <summary>
/// Represents the logging capability configuration.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record LoggingCapability
{
@ -56,6 +59,7 @@ public record LoggingCapability
/// <summary>
/// Represents the prompts capability configuration.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record PromptsCapability
{
@ -68,6 +72,7 @@ public record PromptsCapability
/// <summary>
/// Represents the resources capability configuration.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record ResourcesCapability
{
@ -86,6 +91,7 @@ public record ResourcesCapability
/// <summary>
/// Represents the tools capability configuration.
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
/// </summary>
public record ToolsCapability
{

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