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
79 changed files with 2668 additions and 659 deletions

View file

@ -27,7 +27,7 @@ This library aims to provide a clean, specification-compliant implementation of
- Comprehensive logging support
- Compatible with .NET 8.0 and later
## Getting Started
## Getting Started (Client)
To use mcpdotnet, first install it via NuGet:
@ -85,6 +85,75 @@ You can find samples demonstrating how to use mcpdotnet with an LLM SDK in the [
Additional examples and documentation will be added as in the near future.
Remember you can connect to any MCP server, not just ones created using mcpdotnet. The protocol is designed to be server-agnostic, so you can use this library to connect to any compliant server.
## Getting Started (Server)
Here is an example of how to create an MCP server with a single tool. You can also refer to the TestServer in the tests folder, which implements a wider range of capabilities.
```csharp
McpServerOptions options = new McpServerOptions()
{
ServerInfo = new Implementation() { Name = "MyServer", Version = "1.0.0" },
Capabilities = new ServerCapabilities()
{
Tools = new()
}
};
McpServerFactory factory = new McpServerFactory(new StdioServerTransport("MyServer", loggerFactory), options, loggerFactory);
IMcpServer server = factory.CreateServer();
server.ListToolsHandler = (request, cancellationToken) =>
{
return Task.FromResult(new ListToolsResult()
{
Tools =
[
new Tool()
{
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
}
},
}
]
});
};
server.CallToolHandler = async (request, cancellationToken) =>
{
if (request.Name == "echo")
{
if (request.Arguments is null || !request.Arguments.TryGetValue("message", out var message))
{
throw new McpServerException("Missing required argument 'message'");
}
return new CallToolResponse()
{
Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }]
};
}
else
{
throw new McpServerException($"Unknown tool: {request.Name}");
}
};
await server.StartAsync();
// Run until process is stopped by the client (parent process)
while (true)
{
await Task.Delay(1000);
}
```
## Roadmap
- Expand documentation with detailed guides for:
@ -93,8 +162,11 @@ Additional examples and documentation will be added as in the near future.
- Error handling and recovery
- Increase test coverage
- Add additional samples and examples
- Performance optimization and profiling
- Performance optimization
- SSE server support
- Authentication
- Linux/Mac support for stdio transport
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

View file

@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MEAIToolsConsole", "samples
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnthropicToolsConsole", "samples\anthropic\tools\ToolsConsole\AnthropicToolsConsole.csproj", "{CA0BB450-1903-2F92-A68D-1285976551D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet.TestServer", "tests\mcpdotnet.TestServer\mcpdotnet.TestServer.csproj", "{7C229573-A085-4ECC-8131-958CDA2BE731}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -33,6 +35,10 @@ Global
{CA0BB450-1903-2F92-A68D-1285976551D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA0BB450-1903-2F92-A68D-1285976551D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA0BB450-1903-2F92-A68D-1285976551D6}.Release|Any CPU.Build.0 = Release|Any CPU
{7C229573-A085-4ECC-8131-958CDA2BE731}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C229573-A085-4ECC-8131-958CDA2BE731}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C229573-A085-4ECC-8131-958CDA2BE731}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C229573-A085-4ECC-8131-958CDA2BE731}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -43,7 +43,7 @@ public class McpAIFunction : AIFunction
// Call the tool through mcpdotnet
var result = await _client.CallToolAsync(
_tool.Name,
argDict.Count == 0 ? new(): argDict,
argDict.Count == 0 ? new() : argDict,
cancellationToken: cancellationToken
);

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
{

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