Server support (stdio) (#19)
* Server support (stdio) * TestServer project * Refactored McpClient class to extract shared endpoint logic (with McpServer) to a base class, McpJsonRpcEndpoint. * Improved xmldoc comments for protocol types, mirroring specification schema comments. * Update README.MD
This commit is contained in:
parent
c384c7d3de
commit
8fe797e1f3
79 changed files with 2668 additions and 659 deletions
78
README.MD
78
README.MD
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
13
src/mcpdotnet/Protocol/Transport/IClientTransport.cs
Normal file
13
src/mcpdotnet/Protocol/Transport/IClientTransport.cs
Normal 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);
|
||||
}
|
||||
13
src/mcpdotnet/Protocol/Transport/IServerTransport.cs
Normal file
13
src/mcpdotnet/Protocol/Transport/IServerTransport.cs
Normal 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);
|
||||
}
|
||||
27
src/mcpdotnet/Protocol/Transport/ITransport.cs
Normal file
27
src/mcpdotnet/Protocol/Transport/ITransport.cs
Normal 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);
|
||||
}
|
||||
308
src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
Normal file
308
src/mcpdotnet/Protocol/Transport/SseClientTransport.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
289
src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
Normal file
289
src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
195
src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
Normal file
195
src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue