Clean up logging (#286)
This commit is contained in:
parent
dc544f929b
commit
312aede7d6
12 changed files with 401 additions and 570 deletions
|
|
@ -9,6 +9,7 @@ using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace ModelContextProtocol.AspNetCore;
|
namespace ModelContextProtocol.AspNetCore;
|
||||||
|
|
@ -61,18 +62,19 @@ internal sealed class StreamableHttpHandler(
|
||||||
var httpMcpSession = new HttpMcpSession(transport, context.User);
|
var httpMcpSession = new HttpMcpSession(transport, context.User);
|
||||||
if (!_sessions.TryAdd(sessionId, httpMcpSession))
|
if (!_sessions.TryAdd(sessionId, httpMcpSession))
|
||||||
{
|
{
|
||||||
throw new Exception($"Unreachable given good entropy! Session with ID '{sessionId}' has already been created.");
|
Debug.Fail("Unreachable given good entropy!");
|
||||||
}
|
throw new InvalidOperationException($"Session with ID '{sessionId}' has already been created.");
|
||||||
|
|
||||||
var mcpServerOptions = mcpServerOptionsSnapshot.Value;
|
|
||||||
if (httpMcpServerOptions.Value.ConfigureSessionOptions is { } configureSessionOptions)
|
|
||||||
{
|
|
||||||
mcpServerOptions = mcpServerOptionsFactory.Create(Options.DefaultName);
|
|
||||||
await configureSessionOptions(context, mcpServerOptions, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var mcpServerOptions = mcpServerOptionsSnapshot.Value;
|
||||||
|
if (httpMcpServerOptions.Value.ConfigureSessionOptions is { } configureSessionOptions)
|
||||||
|
{
|
||||||
|
mcpServerOptions = mcpServerOptionsFactory.Create(Options.DefaultName);
|
||||||
|
await configureSessionOptions(context, mcpServerOptions, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
var transportTask = transport.RunAsync(cancellationToken);
|
var transportTask = transport.RunAsync(cancellationToken);
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Protocol.Transport;
|
using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
|
|
@ -10,7 +9,7 @@ using System.Text.Json;
|
||||||
namespace ModelContextProtocol.Client;
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
internal sealed class McpClient : McpEndpoint, IMcpClient
|
internal sealed partial class McpClient : McpEndpoint, IMcpClient
|
||||||
{
|
{
|
||||||
private static Implementation DefaultImplementation { get; } = new()
|
private static Implementation DefaultImplementation { get; } = new()
|
||||||
{
|
{
|
||||||
|
|
@ -133,9 +132,12 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// Store server information
|
// Store server information
|
||||||
_logger.ServerCapabilitiesReceived(EndpointName,
|
if (_logger.IsEnabled(LogLevel.Information))
|
||||||
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
|
{
|
||||||
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
|
LogServerCapabilitiesReceived(EndpointName,
|
||||||
|
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
|
||||||
|
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
|
||||||
|
}
|
||||||
|
|
||||||
_serverCapabilities = initializeResponse.Capabilities;
|
_serverCapabilities = initializeResponse.Capabilities;
|
||||||
_serverInfo = initializeResponse.ServerInfo;
|
_serverInfo = initializeResponse.ServerInfo;
|
||||||
|
|
@ -144,7 +146,7 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
// Validate protocol version
|
// Validate protocol version
|
||||||
if (initializeResponse.ProtocolVersion != _options.ProtocolVersion)
|
if (initializeResponse.ProtocolVersion != _options.ProtocolVersion)
|
||||||
{
|
{
|
||||||
_logger.ServerProtocolVersionMismatch(EndpointName, _options.ProtocolVersion, initializeResponse.ProtocolVersion);
|
LogServerProtocolVersionMismatch(EndpointName, _options.ProtocolVersion, initializeResponse.ProtocolVersion);
|
||||||
throw new McpException($"Server protocol version mismatch. Expected {_options.ProtocolVersion}, got {initializeResponse.ProtocolVersion}");
|
throw new McpException($"Server protocol version mismatch. Expected {_options.ProtocolVersion}, got {initializeResponse.ProtocolVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,13 +157,13 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested)
|
catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.ClientInitializationTimeout(EndpointName);
|
LogClientInitializationTimeout(EndpointName);
|
||||||
throw new McpException("Initialization timed out", oce);
|
throw new McpException("Initialization timed out", oce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.ClientInitializationError(EndpointName, e);
|
LogClientInitializationError(EndpointName, e);
|
||||||
await DisposeAsync().ConfigureAwait(false);
|
await DisposeAsync().ConfigureAwait(false);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
@ -188,4 +190,16 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client received server '{ServerInfo}' capabilities: '{Capabilities}'.")]
|
||||||
|
private partial void LogServerCapabilitiesReceived(string endpointName, string capabilities, string serverInfo);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization error.")]
|
||||||
|
private partial void LogClientInitializationError(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization timed out.")]
|
||||||
|
private partial void LogClientInitializationTimeout(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client protocol version mismatch with server. Expected '{Expected}', received '{Received}'.")]
|
||||||
|
private partial void LogServerProtocolVersionMismatch(string endpointName, string expected, string received);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Transport;
|
using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Client;
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
|
|
@ -14,7 +12,7 @@ namespace ModelContextProtocol.Client;
|
||||||
/// that connect to MCP servers. It handles the creation and connection
|
/// that connect to MCP servers. It handles the creation and connection
|
||||||
/// of appropriate implementations through the supplied transport.
|
/// of appropriate implementations through the supplied transport.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static class McpClientFactory
|
public static partial class McpClientFactory
|
||||||
{
|
{
|
||||||
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
||||||
/// <param name="clientTransport">The transport instance used to communicate with the server.</param>
|
/// <param name="clientTransport">The transport instance used to communicate with the server.</param>
|
||||||
|
|
@ -35,21 +33,24 @@ public static class McpClientFactory
|
||||||
{
|
{
|
||||||
Throw.IfNull(clientTransport);
|
Throw.IfNull(clientTransport);
|
||||||
|
|
||||||
string endpointName = clientTransport.Name;
|
|
||||||
var logger = loggerFactory?.CreateLogger(typeof(McpClientFactory)) ?? NullLogger.Instance;
|
|
||||||
logger.CreatingClient(endpointName);
|
|
||||||
|
|
||||||
McpClient client = new(clientTransport, clientOptions, loggerFactory);
|
McpClient client = new(clientTransport, clientOptions, loggerFactory);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||||
logger.ClientCreated(endpointName);
|
if (loggerFactory?.CreateLogger(typeof(McpClientFactory)) is ILogger logger)
|
||||||
return client;
|
{
|
||||||
|
logger.LogClientCreated(client.EndpointName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
await client.DisposeAsync().ConfigureAwait(false);
|
await client.DisposeAsync().ConfigureAwait(false);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client created and connected.")]
|
||||||
|
private static partial void LogClientCreated(this ILogger logger, string endpointName);
|
||||||
}
|
}
|
||||||
|
|
@ -1,350 +0,0 @@
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Logging;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logging methods for the ModelContextProtocol library.
|
|
||||||
/// </summary>
|
|
||||||
internal static partial class Log
|
|
||||||
{
|
|
||||||
[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 = "Creating client for {endpointName}")]
|
|
||||||
internal static partial void CreatingClient(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Information, Message = "Client for {endpointName} created and connected")]
|
|
||||||
internal static partial void ClientCreated(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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.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.Error, Message = "Client {endpointName} initialization timeout")]
|
|
||||||
internal static partial void ClientInitializationTimeout(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 = "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 = "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 handler error for {endpointName} with method {method}")]
|
|
||||||
internal static partial void RequestHandlerError(this ILogger logger, string endpointName, string method, Exception exception);
|
|
||||||
|
|
||||||
[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.Warning, Message = "The request has not valid message ID for {endpointName}")]
|
|
||||||
internal static partial void RequestHasInvalidId(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 = "Endpoint not connected for {endpointName}")]
|
|
||||||
internal static partial void EndpointNotConnected(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 = "Sending request for {endpointName} with method {method}")]
|
|
||||||
internal static partial void SendingRequest(this ILogger logger, string endpointName, string method);
|
|
||||||
|
|
||||||
[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 = "Request '{requestId}' canceled via client notification with reason '{Reason}'.")]
|
|
||||||
internal static partial void RequestCanceled(this ILogger logger, RequestId requestId, string? reason);
|
|
||||||
|
|
||||||
[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 = "Request response received for {endpointName} with method {method}")]
|
|
||||||
internal static partial void RequestResponseReceived(this ILogger logger, string endpointName, string method);
|
|
||||||
|
|
||||||
[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 = "Cleaning up endpoint {endpointName}")]
|
|
||||||
internal static partial void CleaningUpEndpoint(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Information, Message = "Endpoint cleaned up for {endpointName}")]
|
|
||||||
internal static partial void EndpointCleanedUp(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} already connected")]
|
|
||||||
internal static partial void TransportAlreadyConnected(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport for {endpointName} connecting")]
|
|
||||||
internal static partial void TransportConnecting(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 for {endpointName} received stderr log: {data}")]
|
|
||||||
internal static partial void ReadStderr(this ILogger logger, string endpointName, string data);
|
|
||||||
|
|
||||||
[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 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 reading messages for {endpointName}")]
|
|
||||||
internal static partial void TransportReadingMessages(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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.Error, Message = "Transport not connected for {endpointName}")]
|
|
||||||
internal static partial void TransportNotConnected(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 = null);
|
|
||||||
|
|
||||||
[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 send failed for {endpointName} with ID {messageId}")]
|
|
||||||
internal static partial void TransportSendFailed(this ILogger logger, string endpointName, string messageId, 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 waiting for message for {endpointName}")]
|
|
||||||
internal static partial void TransportWaitingForMessage(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport end of stream for {endpointName}")]
|
|
||||||
internal static partial void TransportEndOfStream(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 received message parsed for {endpointName}: {messageId}")]
|
|
||||||
internal static partial void TransportReceivedMessageParsed(this ILogger logger, string endpointName, string messageId);
|
|
||||||
|
|
||||||
[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.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.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.Information, Message = "Transport exiting read messages loop for {endpointName}")]
|
|
||||||
internal static partial void TransportExitingReadMessagesLoop(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Information, Message = "Transport read messages cancelled for {endpointName}")]
|
|
||||||
internal static partial void TransportReadMessagesCancelled(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[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 cleaning up for {endpointName}")]
|
|
||||||
internal static partial void TransportCleaningUp(this ILogger logger, string endpointName);
|
|
||||||
|
|
||||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for shutdown for {endpointName}")]
|
|
||||||
internal static partial void TransportWaitingForShutdown(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.Debug, Message = "Sending message to {endpointName}: {message}")]
|
|
||||||
internal static partial void SendingMessage(this ILogger logger, string endpointName, string message);
|
|
||||||
|
|
||||||
[LoggerMessage(
|
|
||||||
EventId = 7000,
|
|
||||||
Level = LogLevel.Error,
|
|
||||||
Message = "Transport connection error for {endpointName}"
|
|
||||||
)]
|
|
||||||
public static partial void TransportConnectionError(
|
|
||||||
this ILogger logger,
|
|
||||||
string endpointName,
|
|
||||||
Exception exception);
|
|
||||||
|
|
||||||
[LoggerMessage(
|
|
||||||
EventId = 7001,
|
|
||||||
Level = LogLevel.Warning,
|
|
||||||
Message = "Transport message received before connected for {endpointName}: {data}"
|
|
||||||
)]
|
|
||||||
public static partial void TransportMessageReceivedBeforeConnected(
|
|
||||||
this ILogger logger,
|
|
||||||
string endpointName,
|
|
||||||
string data);
|
|
||||||
|
|
||||||
[LoggerMessage(
|
|
||||||
EventId = 7002,
|
|
||||||
Level = LogLevel.Error,
|
|
||||||
Message = "Transport endpoint event received out of order for {endpointName}: {data}"
|
|
||||||
)]
|
|
||||||
public static partial void TransportEndpointEventInvalid(
|
|
||||||
this ILogger logger,
|
|
||||||
string endpointName,
|
|
||||||
string data);
|
|
||||||
|
|
||||||
[LoggerMessage(
|
|
||||||
EventId = 7003,
|
|
||||||
Level = LogLevel.Error,
|
|
||||||
Message = "Transport event parse failed for {endpointName}: {data}"
|
|
||||||
)]
|
|
||||||
public static partial void TransportEndpointEventParseFailed(
|
|
||||||
this ILogger logger,
|
|
||||||
string endpointName,
|
|
||||||
string data,
|
|
||||||
Exception exception);
|
|
||||||
|
|
||||||
[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 = 7018,
|
|
||||||
Level = LogLevel.Debug,
|
|
||||||
Message = "SSE transport POST accepted for {endpointName} with message ID {messageId}"
|
|
||||||
)]
|
|
||||||
public static partial void SSETransportPostAccepted(
|
|
||||||
this ILogger logger,
|
|
||||||
string endpointName,
|
|
||||||
string messageId);
|
|
||||||
|
|
||||||
[LoggerMessage(
|
|
||||||
EventId = 7019,
|
|
||||||
Level = LogLevel.Error,
|
|
||||||
Message = "SSE transport POST not accepted for {endpointName} with message ID {messageId} and server response {responseContent}"
|
|
||||||
)]
|
|
||||||
public static partial void SSETransportPostNotAccepted(
|
|
||||||
this ILogger logger,
|
|
||||||
string endpointName,
|
|
||||||
string messageId,
|
|
||||||
string responseContent);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logs the byte representation of a message in UTF-8 encoding.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger to use.</param>
|
|
||||||
/// <param name="endpointName">The name of the endpoint.</param>
|
|
||||||
/// <param name="byteRepresentation">The byte representation as a hex string.</param>
|
|
||||||
[LoggerMessage(EventId = 39000, Level = LogLevel.Trace, Message = "Transport {EndpointName}: Message bytes (UTF-8): {ByteRepresentation}")]
|
|
||||||
private static partial void TransportMessageBytes(this ILogger logger, string endpointName, string byteRepresentation);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logs the byte representation of a message for diagnostic purposes.
|
|
||||||
/// This is useful for diagnosing encoding issues with non-ASCII characters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger to use.</param>
|
|
||||||
/// <param name="endpointName">The name of the endpoint.</param>
|
|
||||||
/// <param name="message">The message to log bytes for.</param>
|
|
||||||
internal static void TransportMessageBytesUtf8(this ILogger logger, string endpointName, string message)
|
|
||||||
{
|
|
||||||
if (logger.IsEnabled(LogLevel.Trace))
|
|
||||||
{
|
|
||||||
var bytes = System.Text.Encoding.UTF8.GetBytes(message);
|
|
||||||
var byteRepresentation =
|
|
||||||
#if NET
|
|
||||||
Convert.ToHexString(bytes);
|
|
||||||
#else
|
|
||||||
BitConverter.ToString(bytes).Replace("-", " ");
|
|
||||||
#endif
|
|
||||||
logger.TransportMessageBytes(endpointName, byteRepresentation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.ServerSentEvents;
|
using System.Net.ServerSentEvents;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -14,9 +14,8 @@ namespace ModelContextProtocol.Protocol.Transport;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ServerSideEvents client transport implementation
|
/// The ServerSideEvents client transport implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SseClientSessionTransport : TransportBase
|
internal sealed partial class SseClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
private readonly string _endpointName;
|
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly SseClientTransportOptions _options;
|
private readonly SseClientTransportOptions _options;
|
||||||
private readonly Uri _sseEndpoint;
|
private readonly Uri _sseEndpoint;
|
||||||
|
|
@ -35,7 +34,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
/// <param name="loggerFactory">Logger factory for creating loggers.</param>
|
/// <param name="loggerFactory">Logger factory for creating loggers.</param>
|
||||||
/// <param name="endpointName">The endpoint name used for logging purposes.</param>
|
/// <param name="endpointName">The endpoint name used for logging purposes.</param>
|
||||||
public SseClientSessionTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory, string endpointName)
|
public SseClientSessionTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory, string endpointName)
|
||||||
: base(loggerFactory)
|
: base(endpointName, loggerFactory)
|
||||||
{
|
{
|
||||||
Throw.IfNull(transportOptions);
|
Throw.IfNull(transportOptions);
|
||||||
Throw.IfNull(httpClient);
|
Throw.IfNull(httpClient);
|
||||||
|
|
@ -45,36 +44,23 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_connectionCts = new CancellationTokenSource();
|
_connectionCts = new CancellationTokenSource();
|
||||||
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
|
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
|
||||||
_connectionEstablished = new TaskCompletionSource<bool>();
|
_connectionEstablished = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
_endpointName = endpointName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
public async Task ConnectAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(!IsConnected);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsConnected)
|
|
||||||
{
|
|
||||||
_logger.TransportAlreadyConnected(_endpointName);
|
|
||||||
throw new McpTransportException("Transport is already connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start message receiving loop
|
// Start message receiving loop
|
||||||
_receiveTask = ReceiveMessagesAsync(_connectionCts.Token);
|
_receiveTask = ReceiveMessagesAsync(_connectionCts.Token);
|
||||||
|
|
||||||
_logger.TransportReadingMessages(_endpointName);
|
|
||||||
|
|
||||||
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false);
|
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (McpTransportException)
|
catch (Exception ex) when (ex is not McpTransportException) // propagate transport exceptions
|
||||||
{
|
{
|
||||||
// Rethrow transport exceptions
|
LogTransportConnectFailed(Name, ex);
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.TransportConnectFailed(_endpointName, ex);
|
|
||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
throw new McpTransportException("Failed to connect transport", ex);
|
throw new McpTransportException("Failed to connect transport", ex);
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +104,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
// If the response is not a JSON-RPC response, it is an SSE message
|
// If the response is not a JSON-RPC response, it is an SSE message
|
||||||
if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase))
|
if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.SSETransportPostAccepted(_endpointName, messageId);
|
LogAcceptedPost(Name, messageId);
|
||||||
// The response will arrive as an SSE message
|
// The response will arrive as an SSE message
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -126,21 +112,30 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ??
|
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ??
|
||||||
throw new McpTransportException("Failed to initialize client");
|
throw new McpTransportException("Failed to initialize client");
|
||||||
|
|
||||||
_logger.TransportReceivedMessageParsed(_endpointName, messageId);
|
LogTransportReceivedMessage(Name, messageId);
|
||||||
await WriteMessageAsync(initializeResponse, cancellationToken).ConfigureAwait(false);
|
await WriteMessageAsync(initializeResponse, cancellationToken).ConfigureAwait(false);
|
||||||
_logger.TransportMessageWritten(_endpointName, messageId);
|
LogTransportMessageWritten(Name, messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check if the response was accepted (the response will come as an SSE message)
|
// Otherwise, check if the response was accepted (the response will come as an SSE message)
|
||||||
if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase))
|
if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.SSETransportPostAccepted(_endpointName, messageId);
|
LogAcceptedPost(Name, messageId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.SSETransportPostNotAccepted(_endpointName, messageId, responseContent);
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
|
{
|
||||||
|
LogRejectedPostSensitive(Name, messageId, responseContent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogRejectedPost(Name, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
throw new McpTransportException("Failed to send message");
|
throw new McpTransportException("Failed to send message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,12 +146,17 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
await _connectionCts.CancelAsync().ConfigureAwait(false);
|
await _connectionCts.CancelAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (_receiveTask != null)
|
try
|
||||||
{
|
{
|
||||||
await _receiveTask.ConfigureAwait(false);
|
if (_receiveTask != null)
|
||||||
|
{
|
||||||
|
await _receiveTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_connectionCts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_connectionCts.Dispose();
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -216,13 +216,13 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
catch when (cancellationToken.IsCancellationRequested)
|
catch when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Normal shutdown
|
// Normal shutdown
|
||||||
|
LogTransportReadMessagesCancelled(Name);
|
||||||
_connectionEstablished.TrySetCanceled(cancellationToken);
|
_connectionEstablished.TrySetCanceled(cancellationToken);
|
||||||
_logger.TransportReadMessagesCancelled(_endpointName);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
LogTransportReadMessagesFailed(Name, ex);
|
||||||
_connectionEstablished.TrySetException(ex);
|
_connectionEstablished.TrySetException(ex);
|
||||||
_logger.TransportConnectionError(_endpointName, ex);
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -235,7 +235,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
_logger.TransportMessageReceivedBeforeConnected(_endpointName, data);
|
LogTransportMessageReceivedBeforeConnected(Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -244,7 +244,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
|
var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
|
||||||
if (message == null)
|
if (message == null)
|
||||||
{
|
{
|
||||||
_logger.TransportMessageParseUnexpectedType(_endpointName, data);
|
LogTransportMessageParseUnexpectedTypeSensitive(Name, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,13 +254,20 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
messageId = messageWithId.Id.ToString();
|
messageId = messageWithId.Id.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.TransportReceivedMessageParsed(_endpointName, messageId);
|
LogTransportReceivedMessage(Name, messageId);
|
||||||
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||||
_logger.TransportMessageWritten(_endpointName, messageId);
|
LogTransportMessageWritten(Name, messageId);
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_logger.TransportMessageParseFailed(_endpointName, data, ex);
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
|
{
|
||||||
|
LogTransportMessageParseFailedSensitive(Name, data, ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogTransportMessageParseFailed(Name, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +277,7 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(data))
|
if (string.IsNullOrEmpty(data))
|
||||||
{
|
{
|
||||||
_logger.TransportEndpointEventInvalid(_endpointName, data);
|
LogTransportEndpointEventInvalid(Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +290,15 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_logger.TransportEndpointEventParseFailed(_endpointName, data, ex);
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
|
{
|
||||||
|
LogTransportEndpointEventParseFailedSensitive(Name, data, ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogTransportEndpointEventParseFailed(Name, ex);
|
||||||
|
}
|
||||||
|
|
||||||
throw new McpTransportException("Failed to parse endpoint event", ex);
|
throw new McpTransportException("Failed to parse endpoint event", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -301,4 +316,13 @@ internal sealed class SseClientSessionTransport : TransportBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} accepted SSE transport POST for message ID '{MessageId}'.")]
|
||||||
|
private partial void LogAcceptedPost(string endpointName, string messageId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} rejected SSE transport POST for message ID '{MessageId}'.")]
|
||||||
|
private partial void LogRejectedPost(string endpointName, string messageId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} rejected SSE transport POST for message ID '{MessageId}'. Server response: '{responseContent}'.")]
|
||||||
|
private partial void LogRejectedPostSensitive(string endpointName, string messageId, string responseContent);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Transport;
|
namespace ModelContextProtocol.Protocol.Transport;
|
||||||
|
|
@ -49,7 +49,6 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
||||||
|
|
||||||
if (hasExited)
|
if (hasExited)
|
||||||
{
|
{
|
||||||
Logger.TransportNotConnected(EndpointName);
|
|
||||||
throw new McpTransportException("Transport is not connected", processException);
|
throw new McpTransportException("Transport is not connected", processException);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,7 +58,14 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override ValueTask CleanupAsync(CancellationToken cancellationToken)
|
protected override ValueTask CleanupAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
StdioClientTransport.DisposeProcess(_process, processRunning: true, Logger, _options.ShutdownTimeout, EndpointName);
|
try
|
||||||
|
{
|
||||||
|
StdioClientTransport.DisposeProcess(_process, processRunning: true, _options.ShutdownTimeout, Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogTransportShutdownFailed(Name, ex);
|
||||||
|
}
|
||||||
|
|
||||||
return base.CleanupAsync(cancellationToken);
|
return base.CleanupAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
@ -24,7 +23,7 @@ namespace ModelContextProtocol.Protocol.Transport;
|
||||||
/// and environment variables, handling output, and properly terminating the process when the transport is closed.
|
/// and environment variables, handling output, and properly terminating the process when the transport is closed.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class StdioClientTransport : IClientTransport
|
public sealed partial class StdioClientTransport : IClientTransport
|
||||||
{
|
{
|
||||||
private readonly StdioClientTransportOptions _options;
|
private readonly StdioClientTransportOptions _options;
|
||||||
private readonly ILoggerFactory? _loggerFactory;
|
private readonly ILoggerFactory? _loggerFactory;
|
||||||
|
|
@ -68,7 +67,7 @@ public sealed class StdioClientTransport : IClientTransport
|
||||||
ILogger logger = (ILogger?)_loggerFactory?.CreateLogger<StdioClientTransport>() ?? NullLogger.Instance;
|
ILogger logger = (ILogger?)_loggerFactory?.CreateLogger<StdioClientTransport>() ?? NullLogger.Instance;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.TransportConnecting(endpointName);
|
LogTransportConnecting(logger, endpointName);
|
||||||
|
|
||||||
UTF8Encoding noBomUTF8 = new(encoderShouldEmitUTF8Identifier: false);
|
UTF8Encoding noBomUTF8 = new(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
||||||
|
|
@ -114,14 +113,22 @@ public sealed class StdioClientTransport : IClientTransport
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.CreateProcessForTransport(endpointName, _options.Command,
|
if (logger.IsEnabled(LogLevel.Trace))
|
||||||
startInfo.Arguments, string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)),
|
{
|
||||||
startInfo.WorkingDirectory, _options.ShutdownTimeout.ToString());
|
LogCreateProcessForTransportSensitive(logger, endpointName, _options.Command,
|
||||||
|
startInfo.Arguments,
|
||||||
|
string.Join(", ", startInfo.Environment.Select(kvp => $"{kvp.Key}={kvp.Value}")),
|
||||||
|
startInfo.WorkingDirectory);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogCreateProcessForTransport(logger, endpointName, _options.Command);
|
||||||
|
}
|
||||||
|
|
||||||
process = new() { StartInfo = startInfo };
|
process = new() { StartInfo = startInfo };
|
||||||
|
|
||||||
// Set up error logging
|
// Set up error logging
|
||||||
process.ErrorDataReceived += (sender, args) => logger.ReadStderr(endpointName, args.Data ?? "(no data)");
|
process.ErrorDataReceived += (sender, args) => LogReadStderr(logger, endpointName, args.Data ?? "(no data)");
|
||||||
|
|
||||||
// We need both stdin and stdout to use a no-BOM UTF-8 encoding. On .NET Core,
|
// We need both stdin and stdout to use a no-BOM UTF-8 encoding. On .NET Core,
|
||||||
// we can use ProcessStartInfo.StandardOutputEncoding/StandardInputEncoding, but
|
// we can use ProcessStartInfo.StandardOutputEncoding/StandardInputEncoding, but
|
||||||
|
|
@ -146,11 +153,11 @@ public sealed class StdioClientTransport : IClientTransport
|
||||||
|
|
||||||
if (!processStarted)
|
if (!processStarted)
|
||||||
{
|
{
|
||||||
logger.TransportProcessStartFailed(endpointName);
|
LogTransportProcessStartFailed(logger, endpointName);
|
||||||
throw new McpTransportException("Failed to start MCP server process");
|
throw new McpTransportException("Failed to start MCP server process");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.TransportProcessStarted(endpointName, process.Id);
|
LogTransportProcessStarted(logger, endpointName, process.Id);
|
||||||
|
|
||||||
process.BeginErrorReadLine();
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
|
|
@ -158,14 +165,23 @@ public sealed class StdioClientTransport : IClientTransport
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.TransportConnectFailed(endpointName, ex);
|
LogTransportConnectFailed(logger, endpointName, ex);
|
||||||
DisposeProcess(process, processStarted, logger, _options.ShutdownTimeout, endpointName);
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DisposeProcess(process, processStarted, _options.ShutdownTimeout, endpointName);
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
LogTransportShutdownFailed(logger, endpointName, ex2);
|
||||||
|
}
|
||||||
|
|
||||||
throw new McpTransportException("Failed to connect transport", ex);
|
throw new McpTransportException("Failed to connect transport", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void DisposeProcess(
|
internal static void DisposeProcess(
|
||||||
Process? process, bool processRunning, ILogger logger, TimeSpan shutdownTimeout, string endpointName)
|
Process? process, bool processRunning, TimeSpan shutdownTimeout, string endpointName)
|
||||||
{
|
{
|
||||||
if (process is not null)
|
if (process is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -188,18 +204,37 @@ public sealed class StdioClientTransport : IClientTransport
|
||||||
// Wait for the process to exit.
|
// Wait for the process to exit.
|
||||||
// Kill the while process tree because the process may spawn child processes
|
// Kill the while process tree because the process may spawn child processes
|
||||||
// and Node.js does not kill its children when it exits properly.
|
// and Node.js does not kill its children when it exits properly.
|
||||||
logger.TransportWaitingForShutdown(endpointName);
|
|
||||||
process.KillTree(shutdownTimeout);
|
process.KillTree(shutdownTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.TransportShutdownFailed(endpointName, ex);
|
|
||||||
}
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
process.Dispose();
|
process.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} connecting.")]
|
||||||
|
private static partial void LogTransportConnecting(ILogger logger, string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} starting server process. Command: '{Command}'.")]
|
||||||
|
private static partial void LogCreateProcessForTransport(ILogger logger, string endpointName, string command);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} starting server process. Command: '{Command}', Arguments: {Arguments}, Environment: {Environment}, Working directory: {WorkingDirectory}.")]
|
||||||
|
private static partial void LogCreateProcessForTransportSensitive(ILogger logger, string endpointName, string command, string? arguments, string environment, string workingDirectory);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} failed to start server process.")]
|
||||||
|
private static partial void LogTransportProcessStartFailed(ILogger logger, string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} received stderr log: '{Data}'.")]
|
||||||
|
private static partial void LogReadStderr(ILogger logger, string endpointName, string data);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} started server process with PID {ProcessId}.")]
|
||||||
|
private static partial void LogTransportProcessStarted(ILogger logger, string endpointName, int processId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} connect failed.")]
|
||||||
|
private static partial void LogTransportConnectFailed(ILogger logger, string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} shutdown failed.")]
|
||||||
|
private static partial void LogTransportShutdownFailed(ILogger logger, string endpointName, Exception exception);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
|
|
@ -40,17 +38,14 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public StreamClientSessionTransport(
|
public StreamClientSessionTransport(
|
||||||
TextWriter serverInput, TextReader serverOutput, string endpointName, ILoggerFactory? loggerFactory)
|
TextWriter serverInput, TextReader serverOutput, string endpointName, ILoggerFactory? loggerFactory)
|
||||||
: base(loggerFactory)
|
: base(endpointName, loggerFactory)
|
||||||
{
|
{
|
||||||
Logger = (ILogger?)loggerFactory?.CreateLogger<StdioClientTransport>() ?? NullLogger.Instance;
|
|
||||||
_serverOutput = serverOutput;
|
_serverOutput = serverOutput;
|
||||||
_serverInput = serverInput;
|
_serverInput = serverInput;
|
||||||
EndpointName = endpointName;
|
|
||||||
|
|
||||||
// Start reading messages in the background. We use the rarer pattern of new Task + Start
|
// Start reading messages in the background. We use the rarer pattern of new Task + Start
|
||||||
// in order to ensure that the body of the task will always see _readTask initialized.
|
// in order to ensure that the body of the task will always see _readTask initialized.
|
||||||
// It is then able to reliably null it out on completion.
|
// It is then able to reliably null it out on completion.
|
||||||
Logger.TransportReadingMessages(endpointName);
|
|
||||||
var readTask = new Task<Task>(
|
var readTask = new Task<Task>(
|
||||||
thisRef => ((StreamClientSessionTransport)thisRef!).ReadMessagesAsync(_shutdownCts.Token),
|
thisRef => ((StreamClientSessionTransport)thisRef!).ReadMessagesAsync(_shutdownCts.Token),
|
||||||
this,
|
this,
|
||||||
|
|
@ -61,25 +56,6 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
SetConnected(true);
|
SetConnected(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logger instance used by this transport for diagnostic output.
|
|
||||||
/// If no logger factory was provided in the constructor, this will be a NullLogger.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Derived classes can use this logger to emit additional diagnostic information
|
|
||||||
/// specific to their implementation.
|
|
||||||
/// </remarks>
|
|
||||||
protected ILogger Logger { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name that identifies this transport endpoint in logs.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This name is provided during construction and is used in log messages to identify
|
|
||||||
/// the source of transport-related events.
|
|
||||||
/// </remarks>
|
|
||||||
protected string EndpointName { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
@ -102,7 +78,6 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
Logger.TransportNotConnected(EndpointName);
|
|
||||||
throw new McpTransportException("Transport is not connected");
|
throw new McpTransportException("Transport is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,18 +92,13 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.TransportSendingMessage(EndpointName, id, json);
|
|
||||||
Logger.TransportMessageBytesUtf8(EndpointName, json);
|
|
||||||
|
|
||||||
// Write the message followed by a newline using our UTF-8 writer
|
// Write the message followed by a newline using our UTF-8 writer
|
||||||
await _serverInput.WriteLineAsync(json).ConfigureAwait(false);
|
await _serverInput.WriteLineAsync(json).ConfigureAwait(false);
|
||||||
await _serverInput.FlushAsync(cancellationToken).ConfigureAwait(false);
|
await _serverInput.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
Logger.TransportSentMessage(EndpointName, id);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.TransportSendFailed(EndpointName, id, ex);
|
LogTransportSendFailed(Name, id, ex);
|
||||||
throw new McpTransportException("Failed to send message", ex);
|
throw new McpTransportException("Failed to send message", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,14 +121,13 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.TransportEnteringReadMessagesLoop(EndpointName);
|
LogTransportEnteringReadMessagesLoop(Name);
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
Logger.TransportWaitingForMessage(EndpointName);
|
|
||||||
if (await _serverOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) is not string line)
|
if (await _serverOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) is not string line)
|
||||||
{
|
{
|
||||||
Logger.TransportEndOfStream(EndpointName);
|
LogTransportEndOfStream(Name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,20 +136,18 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.TransportReceivedMessage(EndpointName, line);
|
LogTransportReceivedMessageSensitive(Name, line);
|
||||||
Logger.TransportMessageBytesUtf8(EndpointName, line);
|
|
||||||
|
|
||||||
await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false);
|
await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
Logger.TransportExitingReadMessagesLoop(EndpointName);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logger.TransportReadMessagesCancelled(EndpointName);
|
LogTransportReadMessagesCancelled(Name);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.TransportReadMessagesFailed(EndpointName, ex);
|
LogTransportReadMessagesFailed(Name, ex);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -202,24 +169,31 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
messageId = messageWithId.Id.ToString();
|
messageId = messageWithId.Id.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
LogTransportReceivedMessage(Name, messageId);
|
||||||
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||||
Logger.TransportMessageWritten(EndpointName, messageId);
|
LogTransportMessageWritten(Name, messageId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.TransportMessageParseUnexpectedType(EndpointName, line);
|
LogTransportMessageParseUnexpectedTypeSensitive(Name, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
Logger.TransportMessageParseFailed(EndpointName, line, ex);
|
if (Logger.IsEnabled(LogLevel.Trace))
|
||||||
|
{
|
||||||
|
LogTransportMessageParseFailedSensitive(Name, line, ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogTransportMessageParseFailed(Name, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual async ValueTask CleanupAsync(CancellationToken cancellationToken)
|
protected virtual async ValueTask CleanupAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.TransportCleaningUp(EndpointName);
|
LogTransportShuttingDown(Name);
|
||||||
|
|
||||||
if (Interlocked.Exchange(ref _shutdownCts, null) is { } shutdownCts)
|
if (Interlocked.Exchange(ref _shutdownCts, null) is { } shutdownCts)
|
||||||
{
|
{
|
||||||
|
|
@ -231,25 +205,18 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.TransportWaitingForReadTask(EndpointName);
|
|
||||||
await readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
|
await readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
|
||||||
Logger.TransportReadTaskCleanedUp(EndpointName);
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
Logger.TransportCleanupReadTaskTimeout(EndpointName);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logger.TransportCleanupReadTaskCancelled(EndpointName);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.TransportCleanupReadTaskFailed(EndpointName, ex);
|
LogTransportCleanupReadTaskFailed(Name, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetConnected(false);
|
SetConnected(false);
|
||||||
Logger.TransportCleanedUp(EndpointName);
|
LogTransportShutDown(Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
|
|
@ -25,7 +24,6 @@ public class StreamServerTransport : TransportBase
|
||||||
|
|
||||||
private readonly TextReader _inputReader;
|
private readonly TextReader _inputReader;
|
||||||
private readonly Stream _outputStream;
|
private readonly Stream _outputStream;
|
||||||
private readonly string _endpointName;
|
|
||||||
|
|
||||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||||
private readonly CancellationTokenSource _shutdownCts = new();
|
private readonly CancellationTokenSource _shutdownCts = new();
|
||||||
|
|
@ -43,7 +41,7 @@ public class StreamServerTransport : TransportBase
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="inputStream"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="inputStream"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="outputStream"/> is <see langword="null"/>.</exception>
|
/// <exception cref="ArgumentNullException"><paramref name="outputStream"/> is <see langword="null"/>.</exception>
|
||||||
public StreamServerTransport(Stream inputStream, Stream outputStream, string? serverName = null, ILoggerFactory? loggerFactory = null)
|
public StreamServerTransport(Stream inputStream, Stream outputStream, string? serverName = null, ILoggerFactory? loggerFactory = null)
|
||||||
: base(loggerFactory)
|
: base(serverName is not null ? $"Server (stream) ({serverName})" : "Server (stream)", loggerFactory)
|
||||||
{
|
{
|
||||||
Throw.IfNull(inputStream);
|
Throw.IfNull(inputStream);
|
||||||
Throw.IfNull(outputStream);
|
Throw.IfNull(outputStream);
|
||||||
|
|
@ -55,8 +53,6 @@ public class StreamServerTransport : TransportBase
|
||||||
|
|
||||||
SetConnected(true);
|
SetConnected(true);
|
||||||
_readLoopCompleted = Task.Run(ReadMessagesAsync, _shutdownCts.Token);
|
_readLoopCompleted = Task.Run(ReadMessagesAsync, _shutdownCts.Token);
|
||||||
|
|
||||||
_endpointName = serverName is not null ? $"Server (stream) ({serverName})" : "Server (stream)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -64,7 +60,6 @@ public class StreamServerTransport : TransportBase
|
||||||
{
|
{
|
||||||
if (!IsConnected)
|
if (!IsConnected)
|
||||||
{
|
{
|
||||||
_logger.TransportNotConnected(_endpointName);
|
|
||||||
throw new McpTransportException("Transport is not connected");
|
throw new McpTransportException("Transport is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,17 +73,13 @@ public class StreamServerTransport : TransportBase
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.TransportSendingMessage(_endpointName, id);
|
|
||||||
|
|
||||||
await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)), cancellationToken).ConfigureAwait(false);
|
await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)), cancellationToken).ConfigureAwait(false);
|
||||||
await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false);
|
await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false);
|
||||||
await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
_logger.TransportSentMessage(_endpointName, id);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.TransportSendFailed(_endpointName, id, ex);
|
LogTransportSendFailed(Name, id, ex);
|
||||||
throw new McpTransportException("Failed to send message", ex);
|
throw new McpTransportException("Failed to send message", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,26 +89,23 @@ public class StreamServerTransport : TransportBase
|
||||||
CancellationToken shutdownToken = _shutdownCts.Token;
|
CancellationToken shutdownToken = _shutdownCts.Token;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.TransportEnteringReadMessagesLoop(_endpointName);
|
LogTransportEnteringReadMessagesLoop(Name);
|
||||||
|
|
||||||
while (!shutdownToken.IsCancellationRequested)
|
while (!shutdownToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.TransportWaitingForMessage(_endpointName);
|
|
||||||
|
|
||||||
var line = await _inputReader.ReadLineAsync(shutdownToken).ConfigureAwait(false);
|
var line = await _inputReader.ReadLineAsync(shutdownToken).ConfigureAwait(false);
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
{
|
{
|
||||||
if (line is null)
|
if (line is null)
|
||||||
{
|
{
|
||||||
_logger.TransportEndOfStream(_endpointName);
|
LogTransportEndOfStream(Name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.TransportReceivedMessage(_endpointName, line);
|
LogTransportReceivedMessageSensitive(Name, line);
|
||||||
_logger.TransportMessageBytesUtf8(_endpointName, line);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -128,32 +116,38 @@ public class StreamServerTransport : TransportBase
|
||||||
{
|
{
|
||||||
messageId = messageWithId.Id.ToString();
|
messageId = messageWithId.Id.ToString();
|
||||||
}
|
}
|
||||||
_logger.TransportReceivedMessageParsed(_endpointName, messageId);
|
|
||||||
|
|
||||||
|
LogTransportReceivedMessage(Name, messageId);
|
||||||
await WriteMessageAsync(message, shutdownToken).ConfigureAwait(false);
|
await WriteMessageAsync(message, shutdownToken).ConfigureAwait(false);
|
||||||
_logger.TransportMessageWritten(_endpointName, messageId);
|
LogTransportMessageWritten(Name, messageId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.TransportMessageParseUnexpectedType(_endpointName, line);
|
LogTransportMessageParseUnexpectedTypeSensitive(Name, line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_logger.TransportMessageParseFailed(_endpointName, line, ex);
|
if (Logger.IsEnabled(LogLevel.Trace))
|
||||||
|
{
|
||||||
|
LogTransportMessageParseFailedSensitive(Name, line, ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogTransportMessageParseFailed(Name, ex);
|
||||||
|
}
|
||||||
|
|
||||||
// Continue reading even if we fail to parse a message
|
// Continue reading even if we fail to parse a message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.TransportExitingReadMessagesLoop(_endpointName);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_logger.TransportReadMessagesCancelled(_endpointName);
|
LogTransportReadMessagesCancelled(Name);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.TransportReadMessagesFailed(_endpointName, ex);
|
LogTransportReadMessagesFailed(Name, ex);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -171,7 +165,7 @@ public class StreamServerTransport : TransportBase
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.TransportCleaningUp(_endpointName);
|
LogTransportShuttingDown(Name);
|
||||||
|
|
||||||
// Signal to the stdin reading loop to stop.
|
// Signal to the stdin reading loop to stop.
|
||||||
await _shutdownCts.CancelAsync().ConfigureAwait(false);
|
await _shutdownCts.CancelAsync().ConfigureAwait(false);
|
||||||
|
|
@ -185,27 +179,20 @@ public class StreamServerTransport : TransportBase
|
||||||
// Make sure the work has quiesced.
|
// Make sure the work has quiesced.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.TransportWaitingForReadTask(_endpointName);
|
|
||||||
await _readLoopCompleted.ConfigureAwait(false);
|
await _readLoopCompleted.ConfigureAwait(false);
|
||||||
_logger.TransportReadTaskCleanedUp(_endpointName);
|
|
||||||
}
|
|
||||||
catch (TimeoutException)
|
|
||||||
{
|
|
||||||
_logger.TransportCleanupReadTaskTimeout(_endpointName);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
_logger.TransportCleanupReadTaskCancelled(_endpointName);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.TransportCleanupReadTaskFailed(_endpointName, ex);
|
LogTransportCleanupReadTaskFailed(Name, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
SetConnected(false);
|
SetConnected(false);
|
||||||
_logger.TransportCleanedUp(_endpointName);
|
LogTransportShutDown(Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
|
|
@ -20,7 +19,7 @@ namespace ModelContextProtocol.Protocol.Transport;
|
||||||
/// to handle the specific transport mechanism being used.
|
/// to handle the specific transport mechanism being used.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract class TransportBase : ITransport
|
public abstract partial class TransportBase : ITransport
|
||||||
{
|
{
|
||||||
private readonly Channel<IJsonRpcMessage> _messageChannel;
|
private readonly Channel<IJsonRpcMessage> _messageChannel;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
@ -29,17 +28,30 @@ public abstract class TransportBase : ITransport
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TransportBase"/> class.
|
/// Initializes a new instance of the <see cref="TransportBase"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected TransportBase(ILoggerFactory? loggerFactory)
|
protected TransportBase(string name, ILoggerFactory? loggerFactory)
|
||||||
{
|
{
|
||||||
|
Name = name;
|
||||||
|
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
||||||
|
|
||||||
// Unbounded channel to prevent blocking on writes
|
// Unbounded channel to prevent blocking on writes
|
||||||
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
|
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
|
||||||
{
|
{
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = true,
|
SingleWriter = true,
|
||||||
});
|
});
|
||||||
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the logger used by this transport.</summary>
|
||||||
|
private protected ILogger Logger => _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name that identifies this transport endpoint in logs.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This name is used in log messages to identify the source of transport-related events.
|
||||||
|
/// </remarks>
|
||||||
|
protected string Name { get; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsConnected => _isConnected == 1;
|
public bool IsConnected => _isConnected == 1;
|
||||||
|
|
||||||
|
|
@ -64,9 +76,7 @@ public abstract class TransportBase : ITransport
|
||||||
throw new McpTransportException("Transport is not connected");
|
throw new McpTransportException("Transport is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.TransportWritingMessageToChannel(message);
|
|
||||||
await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
|
await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
|
||||||
_logger.TransportMessageWrittenToChannel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -86,4 +96,64 @@ public abstract class TransportBase : ITransport
|
||||||
_messageChannel.Writer.Complete();
|
_messageChannel.Writer.Complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} transport connect failed.")]
|
||||||
|
private protected partial void LogTransportConnectFailed(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} transport send failed for message ID '{MessageId}'.")]
|
||||||
|
private protected partial void LogTransportSendFailed(string endpointName, string messageId, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport reading messages.")]
|
||||||
|
private protected partial void LogTransportEnteringReadMessagesLoop(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport completed reading messages.")]
|
||||||
|
private protected partial void LogTransportEndOfStream(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport received message. Message: '{Message}'.")]
|
||||||
|
private protected partial void LogTransportReceivedMessageSensitive(string endpointName, string message);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} transport received message with ID '{MessageId}'.")]
|
||||||
|
private protected partial void LogTransportReceivedMessage(string endpointName, string messageId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} transport sent message with ID '{MessageId}'.")]
|
||||||
|
private protected partial void LogTransportMessageWritten(string endpointName, string messageId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport received unexpected message. Message: '{Message}'.")]
|
||||||
|
private protected partial void LogTransportMessageParseUnexpectedTypeSensitive(string endpointName, string message);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport message parsing failed.")]
|
||||||
|
private protected partial void LogTransportMessageParseFailed(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} transport message parsing failed. Message: '{Message}'.")]
|
||||||
|
private protected partial void LogTransportMessageParseFailedSensitive(string endpointName, string message, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} transport message reading canceled.")]
|
||||||
|
private protected partial void LogTransportReadMessagesCancelled(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} transport message reading failed.")]
|
||||||
|
private protected partial void LogTransportReadMessagesFailed(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} shutting down.")]
|
||||||
|
private protected partial void LogTransportShuttingDown(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} shutdown failed.")]
|
||||||
|
private protected partial void LogTransportShutdownFailed(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} shutdown failed waiting for message reading completion.")]
|
||||||
|
private protected partial void LogTransportCleanupReadTaskFailed(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} shut down.")]
|
||||||
|
private protected partial void LogTransportShutDown(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} received message before connected.")]
|
||||||
|
private protected partial void LogTransportMessageReceivedBeforeConnected(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} endpoint event received out of order.")]
|
||||||
|
private protected partial void LogTransportEndpointEventInvalid(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} failed to parse event.")]
|
||||||
|
private protected partial void LogTransportEndpointEventParseFailed(string endpointName, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} failed to parse event. Message: '{Message}'.")]
|
||||||
|
private protected partial void LogTransportEndpointEventParseFailedSensitive(string endpointName, string message, Exception exception);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Protocol.Transport;
|
using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
|
|
@ -17,7 +16,7 @@ namespace ModelContextProtocol.Shared;
|
||||||
/// This is especially true as a client represents a connection to one and only one server, and vice versa.
|
/// This is especially true as a client represents a connection to one and only one server, and vice versa.
|
||||||
/// Any multi-client or multi-server functionality should be implemented at a higher level of abstraction.
|
/// Any multi-client or multi-server functionality should be implemented at a higher level of abstraction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class McpEndpoint : IAsyncDisposable
|
internal abstract partial class McpEndpoint : IAsyncDisposable
|
||||||
{
|
{
|
||||||
/// <summary>Cached naming information used for name/version when none is specified.</summary>
|
/// <summary>Cached naming information used for name/version when none is specified.</summary>
|
||||||
internal static AssemblyName DefaultAssemblyName { get; } = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName();
|
internal static AssemblyName DefaultAssemblyName { get; } = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName();
|
||||||
|
|
@ -95,7 +94,7 @@ internal abstract class McpEndpoint : IAsyncDisposable
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async ValueTask DisposeUnsynchronizedAsync()
|
public virtual async ValueTask DisposeUnsynchronizedAsync()
|
||||||
{
|
{
|
||||||
_logger.CleaningUpEndpoint(EndpointName);
|
LogEndpointShuttingDown(EndpointName);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -122,9 +121,15 @@ internal abstract class McpEndpoint : IAsyncDisposable
|
||||||
_sessionCts?.Dispose();
|
_sessionCts?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.EndpointCleanedUp(EndpointName);
|
LogEndpointShutDown(EndpointName);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected McpSession GetSessionOrThrow()
|
protected McpSession GetSessionOrThrow()
|
||||||
=> _session ?? throw new InvalidOperationException($"This should be unreachable from public API! Call {nameof(InitializeSession)} before sending messages.");
|
=> _session ?? throw new InvalidOperationException($"This should be unreachable from public API! Call {nameof(InitializeSession)} before sending messages.");
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} shutting down.")]
|
||||||
|
private partial void LogEndpointShuttingDown(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} shut down.")]
|
||||||
|
private partial void LogEndpointShutDown(string endpointName);
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Protocol.Transport;
|
using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
|
|
@ -19,7 +18,7 @@ namespace ModelContextProtocol.Shared;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class for managing an MCP JSON-RPC session. This covers both MCP clients and servers.
|
/// Class for managing an MCP JSON-RPC session. This covers both MCP clients and servers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class McpSession : IDisposable
|
internal sealed partial class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
private static readonly Histogram<double> s_clientSessionDuration = Diagnostics.CreateDurationHistogram(
|
private static readonly Histogram<double> s_clientSessionDuration = Diagnostics.CreateDurationHistogram(
|
||||||
"mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true);
|
"mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true);
|
||||||
|
|
@ -101,7 +100,7 @@ internal sealed class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
await foreach (var message in _transport.MessageReader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
await foreach (var message in _transport.MessageReader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
_logger.TransportMessageRead(EndpointName, message.GetType().Name);
|
LogMessageRead(EndpointName, message.GetType().Name);
|
||||||
|
|
||||||
_ = ProcessMessageAsync();
|
_ = ProcessMessageAsync();
|
||||||
async Task ProcessMessageAsync()
|
async Task ProcessMessageAsync()
|
||||||
|
|
@ -142,7 +141,7 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
if (!isUserCancellation && message is JsonRpcRequest request)
|
if (!isUserCancellation && message is JsonRpcRequest request)
|
||||||
{
|
{
|
||||||
_logger.RequestHandlerError(EndpointName, request.Method, ex);
|
LogRequestHandlerException(EndpointName, request.Method, ex);
|
||||||
await _transport.SendMessageAsync(new JsonRpcError
|
await _transport.SendMessageAsync(new JsonRpcError
|
||||||
{
|
{
|
||||||
Id = request.Id,
|
Id = request.Id,
|
||||||
|
|
@ -156,8 +155,14 @@ internal sealed class McpSession : IDisposable
|
||||||
}
|
}
|
||||||
else if (ex is not OperationCanceledException)
|
else if (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
var payload = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
_logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex);
|
{
|
||||||
|
LogMessageHandlerExceptionSensitive(EndpointName, message.GetType().Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage), ex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogMessageHandlerException(EndpointName, message.GetType().Name, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -174,7 +179,7 @@ internal sealed class McpSession : IDisposable
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Normal shutdown
|
// Normal shutdown
|
||||||
_logger.EndpointMessageProcessingCancelled(EndpointName);
|
LogEndpointMessageProcessingCanceled(EndpointName);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -226,7 +231,7 @@ internal sealed class McpSession : IDisposable
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
_logger.EndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
|
LogEndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +257,7 @@ internal sealed class McpSession : IDisposable
|
||||||
_handlingRequests.TryGetValue(cn.RequestId, out var cts))
|
_handlingRequests.TryGetValue(cn.RequestId, out var cts))
|
||||||
{
|
{
|
||||||
await cts.CancelAsync().ConfigureAwait(false);
|
await cts.CancelAsync().ConfigureAwait(false);
|
||||||
_logger.RequestCanceled(cn.RequestId, cn.Reason);
|
LogRequestCanceled(EndpointName, cn.RequestId, cn.Reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -267,18 +272,13 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
|
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
|
||||||
{
|
{
|
||||||
if (messageWithId.Id.Id is null)
|
if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
|
||||||
{
|
{
|
||||||
_logger.RequestHasInvalidId(EndpointName);
|
|
||||||
}
|
|
||||||
else if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
|
|
||||||
{
|
|
||||||
_logger.ResponseMatchedPendingRequest(EndpointName, messageWithId.Id.ToString());
|
|
||||||
tcs.TrySetResult(message);
|
tcs.TrySetResult(message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.NoRequestFoundForMessageWithId(EndpointName, messageWithId.Id.ToString());
|
LogNoRequestFoundForMessageWithId(EndpointName, messageWithId.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,13 +286,14 @@ internal sealed class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
if (!_requestHandlers.TryGetValue(request.Method, out var handler))
|
if (!_requestHandlers.TryGetValue(request.Method, out var handler))
|
||||||
{
|
{
|
||||||
_logger.NoHandlerFoundForRequest(EndpointName, request.Method);
|
LogNoHandlerFoundForRequest(EndpointName, request.Method);
|
||||||
throw new McpException("The method does not exist or is not available.", ErrorCodes.MethodNotFound);
|
throw new McpException("The method does not exist or is not available.", ErrorCodes.MethodNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.RequestHandlerCalled(EndpointName, request.Method);
|
LogRequestHandlerCalled(EndpointName, request.Method);
|
||||||
JsonNode? result = await handler(request, cancellationToken).ConfigureAwait(false);
|
JsonNode? result = await handler(request, cancellationToken).ConfigureAwait(false);
|
||||||
_logger.RequestHandlerCompleted(EndpointName, request.Method);
|
LogRequestHandlerCompleted(EndpointName, request.Method);
|
||||||
|
|
||||||
await _transport.SendMessageAsync(new JsonRpcResponse
|
await _transport.SendMessageAsync(new JsonRpcResponse
|
||||||
{
|
{
|
||||||
Id = request.Id,
|
Id = request.Id,
|
||||||
|
|
@ -341,7 +342,6 @@ internal sealed class McpSession : IDisposable
|
||||||
{
|
{
|
||||||
if (!_transport.IsConnected)
|
if (!_transport.IsConnected)
|
||||||
{
|
{
|
||||||
_logger.EndpointNotConnected(EndpointName);
|
|
||||||
throw new McpException("Transport is not connected");
|
throw new McpException("Transport is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -375,21 +375,21 @@ internal sealed class McpSession : IDisposable
|
||||||
AddTags(ref tags, activity, request, method);
|
AddTags(ref tags, activity, request, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expensive logging, use the logging framework to check if the logger is enabled
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
{
|
{
|
||||||
_logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request, McpJsonUtilities.JsonContext.Default.JsonRpcRequest));
|
LogSendingRequestSensitive(EndpointName, request.Method, JsonSerializer.Serialize(request, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogSendingRequest(EndpointName, request.Method);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Less expensive information logging
|
|
||||||
_logger.SendingRequest(EndpointName, request.Method);
|
|
||||||
|
|
||||||
await _transport.SendMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
await _transport.SendMessageAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
_logger.RequestSentAwaitingResponse(EndpointName, request.Method, request.Id.ToString());
|
|
||||||
|
|
||||||
// Now that the request has been sent, register for cancellation. If we registered before,
|
// Now that the request has been sent, register for cancellation. If we registered before,
|
||||||
// a cancellation request could arrive before the server knew about that request ID, in which
|
// a cancellation request could arrive before the server knew about that request ID, in which
|
||||||
// case the server could ignore it.
|
// case the server could ignore it.
|
||||||
|
LogRequestSentAwaitingResponse(EndpointName, request.Method, request.Id);
|
||||||
IJsonRpcMessage? response;
|
IJsonRpcMessage? response;
|
||||||
using (var registration = RegisterCancellation(cancellationToken, request.Id))
|
using (var registration = RegisterCancellation(cancellationToken, request.Id))
|
||||||
{
|
{
|
||||||
|
|
@ -398,8 +398,8 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
if (response is JsonRpcError error)
|
if (response is JsonRpcError error)
|
||||||
{
|
{
|
||||||
_logger.RequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
|
LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code);
|
||||||
throw new McpException($"Request failed (server side): {error.Error.Message}", error.Error.Code);
|
throw new McpException($"Request failed (remote): {error.Error.Message}", error.Error.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response is JsonRpcResponse success)
|
if (response is JsonRpcResponse success)
|
||||||
|
|
@ -409,13 +409,20 @@ internal sealed class McpSession : IDisposable
|
||||||
AddResponseTags(ref tags, activity, success.Result, method);
|
AddResponseTags(ref tags, activity, success.Result, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null");
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
_logger.RequestResponseReceived(EndpointName, request.Method);
|
{
|
||||||
|
LogRequestResponseReceivedSensitive(EndpointName, request.Method, success.Result?.ToJsonString() ?? "null");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogRequestResponseReceived(EndpointName, request.Method);
|
||||||
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unexpected response type
|
// Unexpected response type
|
||||||
_logger.RequestInvalidResponseType(EndpointName, request.Method);
|
LogSendingRequestInvalidResponseType(EndpointName, request.Method);
|
||||||
throw new McpException("Invalid response type");
|
throw new McpException("Invalid response type");
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (addTags)
|
catch (Exception ex) when (addTags)
|
||||||
|
|
@ -436,7 +443,6 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
if (!_transport.IsConnected)
|
if (!_transport.IsConnected)
|
||||||
{
|
{
|
||||||
_logger.ClientNotConnected(EndpointName);
|
|
||||||
throw new McpException("Transport is not connected");
|
throw new McpException("Transport is not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,9 +469,13 @@ internal sealed class McpSession : IDisposable
|
||||||
AddTags(ref tags, activity, message, method);
|
AddTags(ref tags, activity, message, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
{
|
{
|
||||||
_logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage));
|
LogSendingMessageSensitive(EndpointName, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LogSendingMessage(EndpointName);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _transport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
await _transport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
@ -683,4 +693,64 @@ internal sealed class McpSession : IDisposable
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} message processing canceled.")]
|
||||||
|
private partial void LogEndpointMessageProcessingCanceled(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} method '{Method}' request handler called.")]
|
||||||
|
private partial void LogRequestHandlerCalled(string endpointName, string method);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} method '{Method}' request handler completed.")]
|
||||||
|
private partial void LogRequestHandlerCompleted(string endpointName, string method);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} method '{Method}' request handler failed.")]
|
||||||
|
private partial void LogRequestHandlerException(string endpointName, string method, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} received request for unknown request ID '{RequestId}'.")]
|
||||||
|
private partial void LogNoRequestFoundForMessageWithId(string endpointName, RequestId requestId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} request failed for method '{Method}': {ErrorMessage} ({ErrorCode}).")]
|
||||||
|
private partial void LogSendingRequestFailed(string endpointName, string method, string errorMessage, int errorCode);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} received invalid response for method '{Method}'.")]
|
||||||
|
private partial void LogSendingRequestInvalidResponseType(string endpointName, string method);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} sending method '{Method}' request.")]
|
||||||
|
private partial void LogSendingRequest(string endpointName, string method);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} sending method '{Method}' request. Request: '{Request}'.")]
|
||||||
|
private partial void LogSendingRequestSensitive(string endpointName, string method, string request);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} canceled request '{RequestId}' per client notification. Reason: '{Reason}'.")]
|
||||||
|
private partial void LogRequestCanceled(string endpointName, RequestId requestId, string? reason);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} Request response received for method {method}")]
|
||||||
|
private partial void LogRequestResponseReceived(string endpointName, string method);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} Request response received for method {method}. Response: '{Response}'.")]
|
||||||
|
private partial void LogRequestResponseReceivedSensitive(string endpointName, string method, string response);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} read {MessageType} message from channel.")]
|
||||||
|
private partial void LogMessageRead(string endpointName, string messageType);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} message handler {MessageType} failed.")]
|
||||||
|
private partial void LogMessageHandlerException(string endpointName, string messageType, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} message handler {MessageType} failed. Message: '{Message}'.")]
|
||||||
|
private partial void LogMessageHandlerExceptionSensitive(string endpointName, string messageType, string message, Exception exception);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} received unexpected {MessageType} message type.")]
|
||||||
|
private partial void LogEndpointHandlerUnexpectedMessageType(string endpointName, string messageType);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} received request for method '{Method}', but not handler is available.")]
|
||||||
|
private partial void LogNoHandlerFoundForRequest(string endpointName, string method);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} waiting for response to request '{RequestId}' for method '{Method}'.")]
|
||||||
|
private partial void LogRequestSentAwaitingResponse(string endpointName, string method, RequestId requestId);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} sending message.")]
|
||||||
|
private partial void LogSendingMessage(string endpointName);
|
||||||
|
|
||||||
|
[LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} sending message. Message: '{Message}'.")]
|
||||||
|
private partial void LogSendingMessageSensitive(string endpointName, string message);
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue