diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index a8455d871..b88cc07c6 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -9,6 +9,7 @@ using ModelContextProtocol.Server; using ModelContextProtocol.Utils.Json; using System.Collections.Concurrent; +using System.Diagnostics; using System.Security.Cryptography; namespace ModelContextProtocol.AspNetCore; @@ -61,18 +62,19 @@ public async Task HandleSseRequestAsync(HttpContext context) var httpMcpSession = new HttpMcpSession(transport, context.User); if (!_sessions.TryAdd(sessionId, httpMcpSession)) { - throw new Exception($"Unreachable given good entropy! 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); + Debug.Fail("Unreachable given good entropy!"); + throw new InvalidOperationException($"Session with ID '{sessionId}' has already been created."); } 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); try diff --git a/src/ModelContextProtocol/Client/McpClient.cs b/src/ModelContextProtocol/Client/McpClient.cs index d80f8fc38..a8b5abd13 100644 --- a/src/ModelContextProtocol/Client/McpClient.cs +++ b/src/ModelContextProtocol/Client/McpClient.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Types; @@ -10,7 +9,7 @@ namespace ModelContextProtocol.Client; /// -internal sealed class McpClient : McpEndpoint, IMcpClient +internal sealed partial class McpClient : McpEndpoint, IMcpClient { private static Implementation DefaultImplementation { get; } = new() { @@ -133,9 +132,12 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) cancellationToken: initializationCts.Token).ConfigureAwait(false); // Store server information - _logger.ServerCapabilitiesReceived(EndpointName, - capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities), - serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation)); + if (_logger.IsEnabled(LogLevel.Information)) + { + LogServerCapabilitiesReceived(EndpointName, + capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities), + serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation)); + } _serverCapabilities = initializeResponse.Capabilities; _serverInfo = initializeResponse.ServerInfo; @@ -144,7 +146,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) // Validate protocol version 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}"); } @@ -155,13 +157,13 @@ await SendMessageAsync( } catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested) { - _logger.ClientInitializationTimeout(EndpointName); + LogClientInitializationTimeout(EndpointName); throw new McpException("Initialization timed out", oce); } } catch (Exception e) { - _logger.ClientInitializationError(EndpointName, e); + LogClientInitializationError(EndpointName, e); await DisposeAsync().ConfigureAwait(false); throw; } @@ -188,4 +190,16 @@ public override async ValueTask DisposeUnsynchronizedAsync() } } } + + [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); } \ No newline at end of file diff --git a/src/ModelContextProtocol/Client/McpClientFactory.cs b/src/ModelContextProtocol/Client/McpClientFactory.cs index d5df4615d..55c1343e3 100644 --- a/src/ModelContextProtocol/Client/McpClientFactory.cs +++ b/src/ModelContextProtocol/Client/McpClientFactory.cs @@ -1,8 +1,6 @@ -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Utils; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; namespace ModelContextProtocol.Client; @@ -14,7 +12,7 @@ namespace ModelContextProtocol.Client; /// that connect to MCP servers. It handles the creation and connection /// of appropriate implementations through the supplied transport. /// -public static class McpClientFactory +public static partial class McpClientFactory { /// Creates an , connecting it to the specified server. /// The transport instance used to communicate with the server. @@ -35,21 +33,24 @@ public static async Task CreateAsync( { Throw.IfNull(clientTransport); - string endpointName = clientTransport.Name; - var logger = loggerFactory?.CreateLogger(typeof(McpClientFactory)) ?? NullLogger.Instance; - logger.CreatingClient(endpointName); - McpClient client = new(clientTransport, clientOptions, loggerFactory); try { await client.ConnectAsync(cancellationToken).ConfigureAwait(false); - logger.ClientCreated(endpointName); - return client; + if (loggerFactory?.CreateLogger(typeof(McpClientFactory)) is ILogger logger) + { + logger.LogClientCreated(client.EndpointName); + } } catch { await client.DisposeAsync().ConfigureAwait(false); throw; } + + return client; } + + [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client created and connected.")] + private static partial void LogClientCreated(this ILogger logger, string endpointName); } \ No newline at end of file diff --git a/src/ModelContextProtocol/Logging/Log.cs b/src/ModelContextProtocol/Logging/Log.cs deleted file mode 100644 index 47e584174..000000000 --- a/src/ModelContextProtocol/Logging/Log.cs +++ /dev/null @@ -1,350 +0,0 @@ -using ModelContextProtocol.Protocol.Messages; -using Microsoft.Extensions.Logging; - -namespace ModelContextProtocol.Logging; - -/// -/// Logging methods for the ModelContextProtocol library. -/// -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); - - /// - /// Logs the byte representation of a message in UTF-8 encoding. - /// - /// The logger to use. - /// The name of the endpoint. - /// The byte representation as a hex string. - [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); - - /// - /// Logs the byte representation of a message for diagnostic purposes. - /// This is useful for diagnosing encoding issues with non-ASCII characters. - /// - /// The logger to use. - /// The name of the endpoint. - /// The message to log bytes for. - 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); - } - } -} \ No newline at end of file diff --git a/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs b/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs index 168e25818..921867a20 100644 --- a/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Utils; using ModelContextProtocol.Utils.Json; +using System.Diagnostics; using System.Net.Http.Headers; using System.Net.ServerSentEvents; using System.Text; @@ -14,9 +14,8 @@ namespace ModelContextProtocol.Protocol.Transport; /// /// The ServerSideEvents client transport implementation /// -internal sealed class SseClientSessionTransport : TransportBase +internal sealed partial class SseClientSessionTransport : TransportBase { - private readonly string _endpointName; private readonly HttpClient _httpClient; private readonly SseClientTransportOptions _options; private readonly Uri _sseEndpoint; @@ -35,7 +34,7 @@ internal sealed class SseClientSessionTransport : TransportBase /// Logger factory for creating loggers. /// The endpoint name used for logging purposes. public SseClientSessionTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory, string endpointName) - : base(loggerFactory) + : base(endpointName, loggerFactory) { Throw.IfNull(transportOptions); Throw.IfNull(httpClient); @@ -45,36 +44,23 @@ public SseClientSessionTransport(SseClientTransportOptions transportOptions, Htt _httpClient = httpClient; _connectionCts = new CancellationTokenSource(); _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _connectionEstablished = new TaskCompletionSource(); - _endpointName = endpointName; + _connectionEstablished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } /// public async Task ConnectAsync(CancellationToken cancellationToken = default) { + Debug.Assert(!IsConnected); try { - if (IsConnected) - { - _logger.TransportAlreadyConnected(_endpointName); - throw new McpTransportException("Transport is already connected"); - } - // Start message receiving loop _receiveTask = ReceiveMessagesAsync(_connectionCts.Token); - _logger.TransportReadingMessages(_endpointName); - await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false); } - catch (McpTransportException) + catch (Exception ex) when (ex is not McpTransportException) // propagate transport exceptions { - // Rethrow transport exceptions - throw; - } - catch (Exception ex) - { - _logger.TransportConnectFailed(_endpointName, ex); + LogTransportConnectFailed(Name, ex); await CloseAsync().ConfigureAwait(false); throw new McpTransportException("Failed to connect transport", ex); } @@ -118,7 +104,7 @@ public override async Task SendMessageAsync( // If the response is not a JSON-RPC response, it is an SSE message if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase)) { - _logger.SSETransportPostAccepted(_endpointName, messageId); + LogAcceptedPost(Name, messageId); // The response will arrive as an SSE message } else @@ -126,21 +112,30 @@ public override async Task SendMessageAsync( JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ?? throw new McpTransportException("Failed to initialize client"); - _logger.TransportReceivedMessageParsed(_endpointName, messageId); + LogTransportReceivedMessage(Name, messageId); await WriteMessageAsync(initializeResponse, cancellationToken).ConfigureAwait(false); - _logger.TransportMessageWritten(_endpointName, messageId); + LogTransportMessageWritten(Name, messageId); } + return; } // Otherwise, check if the response was accepted (the response will come as an SSE message) if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase)) { - _logger.SSETransportPostAccepted(_endpointName, messageId); + LogAcceptedPost(Name, messageId); } 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"); } } @@ -151,12 +146,17 @@ private async Task CloseAsync() { 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 { @@ -216,13 +216,13 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) catch when (cancellationToken.IsCancellationRequested) { // Normal shutdown + LogTransportReadMessagesCancelled(Name); _connectionEstablished.TrySetCanceled(cancellationToken); - _logger.TransportReadMessagesCancelled(_endpointName); } - catch (Exception ex) when (!cancellationToken.IsCancellationRequested) + catch (Exception ex) { + LogTransportReadMessagesFailed(Name, ex); _connectionEstablished.TrySetException(ex); - _logger.TransportConnectionError(_endpointName, ex); throw; } finally @@ -235,7 +235,7 @@ private async Task ProcessSseMessage(string data, CancellationToken cancellation { if (!IsConnected) { - _logger.TransportMessageReceivedBeforeConnected(_endpointName, data); + LogTransportMessageReceivedBeforeConnected(Name); return; } @@ -244,7 +244,7 @@ private async Task ProcessSseMessage(string data, CancellationToken cancellation var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage); if (message == null) { - _logger.TransportMessageParseUnexpectedType(_endpointName, data); + LogTransportMessageParseUnexpectedTypeSensitive(Name, data); return; } @@ -254,13 +254,20 @@ private async Task ProcessSseMessage(string data, CancellationToken cancellation messageId = messageWithId.Id.ToString(); } - _logger.TransportReceivedMessageParsed(_endpointName, messageId); + LogTransportReceivedMessage(Name, messageId); await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false); - _logger.TransportMessageWritten(_endpointName, messageId); + LogTransportMessageWritten(Name, messageId); } 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 @@ private void HandleEndpointEvent(string data) { if (string.IsNullOrEmpty(data)) { - _logger.TransportEndpointEventInvalid(_endpointName, data); + LogTransportEndpointEventInvalid(Name); return; } @@ -283,7 +290,15 @@ private void HandleEndpointEvent(string data) } 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); } } @@ -301,4 +316,13 @@ private void CopyAdditionalHeaders(HttpRequestHeaders headers) } } } + + [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); } \ No newline at end of file diff --git a/src/ModelContextProtocol/Protocol/Transport/StdioClientSessionTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StdioClientSessionTransport.cs index 69549b1c7..0f4b80dd4 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StdioClientSessionTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StdioClientSessionTransport.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.Logging; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; +using System; using System.Diagnostics; namespace ModelContextProtocol.Protocol.Transport; @@ -49,7 +49,6 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio if (hasExited) { - Logger.TransportNotConnected(EndpointName); throw new McpTransportException("Transport is not connected", processException); } @@ -59,7 +58,14 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio /// 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); } diff --git a/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs index 2eda947ca..fff3786a6 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Logging; using ModelContextProtocol.Utils; using System.Diagnostics; 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. /// /// -public sealed class StdioClientTransport : IClientTransport +public sealed partial class StdioClientTransport : IClientTransport { private readonly StdioClientTransportOptions _options; private readonly ILoggerFactory? _loggerFactory; @@ -68,7 +67,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = ILogger logger = (ILogger?)_loggerFactory?.CreateLogger() ?? NullLogger.Instance; try { - logger.TransportConnecting(endpointName); + LogTransportConnecting(logger, endpointName); UTF8Encoding noBomUTF8 = new(encoderShouldEmitUTF8Identifier: false); @@ -114,14 +113,22 @@ public async Task ConnectAsync(CancellationToken cancellationToken = } } - logger.CreateProcessForTransport(endpointName, _options.Command, - startInfo.Arguments, string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)), - startInfo.WorkingDirectory, _options.ShutdownTimeout.ToString()); + if (logger.IsEnabled(LogLevel.Trace)) + { + 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 }; // 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 can use ProcessStartInfo.StandardOutputEncoding/StandardInputEncoding, but @@ -146,11 +153,11 @@ public async Task ConnectAsync(CancellationToken cancellationToken = if (!processStarted) { - logger.TransportProcessStartFailed(endpointName); + LogTransportProcessStartFailed(logger, endpointName); throw new McpTransportException("Failed to start MCP server process"); } - logger.TransportProcessStarted(endpointName, process.Id); + LogTransportProcessStarted(logger, endpointName, process.Id); process.BeginErrorReadLine(); @@ -158,14 +165,23 @@ public async Task ConnectAsync(CancellationToken cancellationToken = } catch (Exception ex) { - logger.TransportConnectFailed(endpointName, ex); - DisposeProcess(process, processStarted, logger, _options.ShutdownTimeout, endpointName); + LogTransportConnectFailed(logger, endpointName, ex); + + try + { + DisposeProcess(process, processStarted, _options.ShutdownTimeout, endpointName); + } + catch (Exception ex2) + { + LogTransportShutdownFailed(logger, endpointName, ex2); + } + throw new McpTransportException("Failed to connect transport", ex); } } 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) { @@ -188,18 +204,37 @@ internal static void DisposeProcess( // Wait for the process to exit. // Kill the while process tree because the process may spawn child processes // and Node.js does not kill its children when it exits properly. - logger.TransportWaitingForShutdown(endpointName); process.KillTree(shutdownTimeout); } } - catch (Exception ex) - { - logger.TransportShutdownFailed(endpointName, ex); - } finally { 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); } diff --git a/src/ModelContextProtocol/Protocol/Transport/StreamClientSessionTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StreamClientSessionTransport.cs index 9cb8401ff..6c6ebcd29 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StreamClientSessionTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StreamClientSessionTransport.cs @@ -1,6 +1,4 @@ using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Utils; using ModelContextProtocol.Utils.Json; @@ -40,17 +38,14 @@ internal class StreamClientSessionTransport : TransportBase /// public StreamClientSessionTransport( TextWriter serverInput, TextReader serverOutput, string endpointName, ILoggerFactory? loggerFactory) - : base(loggerFactory) + : base(endpointName, loggerFactory) { - Logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; _serverOutput = serverOutput; _serverInput = serverInput; - EndpointName = endpointName; // 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. // It is then able to reliably null it out on completion. - Logger.TransportReadingMessages(endpointName); var readTask = new Task( thisRef => ((StreamClientSessionTransport)thisRef!).ReadMessagesAsync(_shutdownCts.Token), this, @@ -61,25 +56,6 @@ public StreamClientSessionTransport( SetConnected(true); } - /// - /// 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. - /// - /// - /// Derived classes can use this logger to emit additional diagnostic information - /// specific to their implementation. - /// - protected ILogger Logger { get; private set; } - - /// - /// Gets the name that identifies this transport endpoint in logs. - /// - /// - /// This name is provided during construction and is used in log messages to identify - /// the source of transport-related events. - /// - protected string EndpointName { get; } - /// /// /// @@ -102,7 +78,6 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio { if (!IsConnected) { - Logger.TransportNotConnected(EndpointName); throw new McpTransportException("Transport is not connected"); } @@ -117,18 +92,13 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false); try { - Logger.TransportSendingMessage(EndpointName, id, json); - Logger.TransportMessageBytesUtf8(EndpointName, json); - // Write the message followed by a newline using our UTF-8 writer await _serverInput.WriteLineAsync(json).ConfigureAwait(false); await _serverInput.FlushAsync(cancellationToken).ConfigureAwait(false); - - Logger.TransportSentMessage(EndpointName, id); } catch (Exception ex) { - Logger.TransportSendFailed(EndpointName, id, ex); + LogTransportSendFailed(Name, id, ex); throw new McpTransportException("Failed to send message", ex); } } @@ -151,14 +121,13 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken) { try { - Logger.TransportEnteringReadMessagesLoop(EndpointName); + LogTransportEnteringReadMessagesLoop(Name); while (!cancellationToken.IsCancellationRequested) { - Logger.TransportWaitingForMessage(EndpointName); if (await _serverOutput.ReadLineAsync(cancellationToken).ConfigureAwait(false) is not string line) { - Logger.TransportEndOfStream(EndpointName); + LogTransportEndOfStream(Name); break; } @@ -167,20 +136,18 @@ private async Task ReadMessagesAsync(CancellationToken cancellationToken) continue; } - Logger.TransportReceivedMessage(EndpointName, line); - Logger.TransportMessageBytesUtf8(EndpointName, line); + LogTransportReceivedMessageSensitive(Name, line); await ProcessMessageAsync(line, cancellationToken).ConfigureAwait(false); } - Logger.TransportExitingReadMessagesLoop(EndpointName); } catch (OperationCanceledException) { - Logger.TransportReadMessagesCancelled(EndpointName); + LogTransportReadMessagesCancelled(Name); } catch (Exception ex) { - Logger.TransportReadMessagesFailed(EndpointName, ex); + LogTransportReadMessagesFailed(Name, ex); } finally { @@ -202,24 +169,31 @@ private async Task ProcessMessageAsync(string line, CancellationToken cancellati messageId = messageWithId.Id.ToString(); } - Logger.TransportReceivedMessageParsed(EndpointName, messageId); + LogTransportReceivedMessage(Name, messageId); await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false); - Logger.TransportMessageWritten(EndpointName, messageId); + LogTransportMessageWritten(Name, messageId); } else { - Logger.TransportMessageParseUnexpectedType(EndpointName, line); + LogTransportMessageParseUnexpectedTypeSensitive(Name, line); } } 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) { - Logger.TransportCleaningUp(EndpointName); + LogTransportShuttingDown(Name); if (Interlocked.Exchange(ref _shutdownCts, null) is { } shutdownCts) { @@ -231,25 +205,18 @@ protected virtual async ValueTask CleanupAsync(CancellationToken cancellationTok { try { - Logger.TransportWaitingForReadTask(EndpointName); await readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); - Logger.TransportReadTaskCleanedUp(EndpointName); - } - catch (TimeoutException) - { - Logger.TransportCleanupReadTaskTimeout(EndpointName); } catch (OperationCanceledException) { - Logger.TransportCleanupReadTaskCancelled(EndpointName); } catch (Exception ex) { - Logger.TransportCleanupReadTaskFailed(EndpointName, ex); + LogTransportCleanupReadTaskFailed(Name, ex); } } SetConnected(false); - Logger.TransportCleanedUp(EndpointName); + LogTransportShutDown(Name); } } diff --git a/src/ModelContextProtocol/Protocol/Transport/StreamServerTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StreamServerTransport.cs index 10a786e86..516eb4041 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StreamServerTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StreamServerTransport.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Utils; using ModelContextProtocol.Utils.Json; @@ -25,7 +24,6 @@ public class StreamServerTransport : TransportBase private readonly TextReader _inputReader; private readonly Stream _outputStream; - private readonly string _endpointName; private readonly SemaphoreSlim _sendLock = new(1, 1); private readonly CancellationTokenSource _shutdownCts = new(); @@ -43,7 +41,7 @@ public class StreamServerTransport : TransportBase /// is . /// is . 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(outputStream); @@ -55,8 +53,6 @@ public StreamServerTransport(Stream inputStream, Stream outputStream, string? se SetConnected(true); _readLoopCompleted = Task.Run(ReadMessagesAsync, _shutdownCts.Token); - - _endpointName = serverName is not null ? $"Server (stream) ({serverName})" : "Server (stream)"; } /// @@ -64,7 +60,6 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio { if (!IsConnected) { - _logger.TransportNotConnected(_endpointName); throw new McpTransportException("Transport is not connected"); } @@ -78,17 +73,13 @@ public override async Task SendMessageAsync(IJsonRpcMessage message, Cancellatio try { - _logger.TransportSendingMessage(_endpointName, id); - await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)), cancellationToken).ConfigureAwait(false); await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false); await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false); - - _logger.TransportSentMessage(_endpointName, id); } catch (Exception ex) { - _logger.TransportSendFailed(_endpointName, id, ex); + LogTransportSendFailed(Name, id, ex); throw new McpTransportException("Failed to send message", ex); } } @@ -98,26 +89,23 @@ private async Task ReadMessagesAsync() CancellationToken shutdownToken = _shutdownCts.Token; try { - _logger.TransportEnteringReadMessagesLoop(_endpointName); + LogTransportEnteringReadMessagesLoop(Name); while (!shutdownToken.IsCancellationRequested) { - _logger.TransportWaitingForMessage(_endpointName); - var line = await _inputReader.ReadLineAsync(shutdownToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(line)) { if (line is null) { - _logger.TransportEndOfStream(_endpointName); + LogTransportEndOfStream(Name); break; } continue; } - _logger.TransportReceivedMessage(_endpointName, line); - _logger.TransportMessageBytesUtf8(_endpointName, line); + LogTransportReceivedMessageSensitive(Name, line); try { @@ -128,32 +116,38 @@ private async Task ReadMessagesAsync() { messageId = messageWithId.Id.ToString(); } - _logger.TransportReceivedMessageParsed(_endpointName, messageId); + LogTransportReceivedMessage(Name, messageId); await WriteMessageAsync(message, shutdownToken).ConfigureAwait(false); - _logger.TransportMessageWritten(_endpointName, messageId); + LogTransportMessageWritten(Name, messageId); } else { - _logger.TransportMessageParseUnexpectedType(_endpointName, line); + LogTransportMessageParseUnexpectedTypeSensitive(Name, line); } } 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 } } - - _logger.TransportExitingReadMessagesLoop(_endpointName); } catch (OperationCanceledException) { - _logger.TransportReadMessagesCancelled(_endpointName); + LogTransportReadMessagesCancelled(Name); } catch (Exception ex) { - _logger.TransportReadMessagesFailed(_endpointName, ex); + LogTransportReadMessagesFailed(Name, ex); } finally { @@ -171,7 +165,7 @@ public override async ValueTask DisposeAsync() try { - _logger.TransportCleaningUp(_endpointName); + LogTransportShuttingDown(Name); // Signal to the stdin reading loop to stop. await _shutdownCts.CancelAsync().ConfigureAwait(false); @@ -185,27 +179,20 @@ public override async ValueTask DisposeAsync() // Make sure the work has quiesced. try { - _logger.TransportWaitingForReadTask(_endpointName); await _readLoopCompleted.ConfigureAwait(false); - _logger.TransportReadTaskCleanedUp(_endpointName); - } - catch (TimeoutException) - { - _logger.TransportCleanupReadTaskTimeout(_endpointName); } catch (OperationCanceledException) { - _logger.TransportCleanupReadTaskCancelled(_endpointName); } catch (Exception ex) { - _logger.TransportCleanupReadTaskFailed(_endpointName, ex); + LogTransportCleanupReadTaskFailed(Name, ex); } } finally { SetConnected(false); - _logger.TransportCleanedUp(_endpointName); + LogTransportShutDown(Name); } GC.SuppressFinalize(this); diff --git a/src/ModelContextProtocol/Protocol/Transport/TransportBase.cs b/src/ModelContextProtocol/Protocol/Transport/TransportBase.cs index 43062e367..9e253072f 100644 --- a/src/ModelContextProtocol/Protocol/Transport/TransportBase.cs +++ b/src/ModelContextProtocol/Protocol/Transport/TransportBase.cs @@ -1,6 +1,5 @@ using System.Threading.Channels; using Microsoft.Extensions.Logging; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using Microsoft.Extensions.Logging.Abstractions; @@ -20,7 +19,7 @@ namespace ModelContextProtocol.Protocol.Transport; /// to handle the specific transport mechanism being used. /// /// -public abstract class TransportBase : ITransport +public abstract partial class TransportBase : ITransport { private readonly Channel _messageChannel; private readonly ILogger _logger; @@ -29,17 +28,30 @@ public abstract class TransportBase : ITransport /// /// Initializes a new instance of the class. /// - 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 _messageChannel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true, }); - _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; } + /// Gets the logger used by this transport. + private protected ILogger Logger => _logger; + + /// + /// Gets the name that identifies this transport endpoint in logs. + /// + /// + /// This name is used in log messages to identify the source of transport-related events. + /// + protected string Name { get; } + /// public bool IsConnected => _isConnected == 1; @@ -64,9 +76,7 @@ protected async Task WriteMessageAsync(IJsonRpcMessage message, CancellationToke throw new McpTransportException("Transport is not connected"); } - _logger.TransportWritingMessageToChannel(message); await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false); - _logger.TransportMessageWrittenToChannel(); } /// @@ -86,4 +96,64 @@ protected void SetConnected(bool isConnected) _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); } \ No newline at end of file diff --git a/src/ModelContextProtocol/Shared/McpEndpoint.cs b/src/ModelContextProtocol/Shared/McpEndpoint.cs index bbe6fbd4a..45566cefe 100644 --- a/src/ModelContextProtocol/Shared/McpEndpoint.cs +++ b/src/ModelContextProtocol/Shared/McpEndpoint.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; 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. /// Any multi-client or multi-server functionality should be implemented at a higher level of abstraction. /// -internal abstract class McpEndpoint : IAsyncDisposable +internal abstract partial class McpEndpoint : IAsyncDisposable { /// Cached naming information used for name/version when none is specified. internal static AssemblyName DefaultAssemblyName { get; } = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName(); @@ -95,7 +94,7 @@ public async ValueTask DisposeAsync() /// public virtual async ValueTask DisposeUnsynchronizedAsync() { - _logger.CleaningUpEndpoint(EndpointName); + LogEndpointShuttingDown(EndpointName); try { @@ -122,9 +121,15 @@ public virtual async ValueTask DisposeUnsynchronizedAsync() _sessionCts?.Dispose(); } - _logger.EndpointCleanedUp(EndpointName); + LogEndpointShutDown(EndpointName); } protected McpSession GetSessionOrThrow() => _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); } \ No newline at end of file diff --git a/src/ModelContextProtocol/Shared/McpSession.cs b/src/ModelContextProtocol/Shared/McpSession.cs index afbcecabd..a15618a2f 100644 --- a/src/ModelContextProtocol/Shared/McpSession.cs +++ b/src/ModelContextProtocol/Shared/McpSession.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Logging; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Utils; @@ -19,7 +18,7 @@ namespace ModelContextProtocol.Shared; /// /// Class for managing an MCP JSON-RPC session. This covers both MCP clients and servers. /// -internal sealed class McpSession : IDisposable +internal sealed partial class McpSession : IDisposable { private static readonly Histogram s_clientSessionDuration = Diagnostics.CreateDurationHistogram( "mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true); @@ -101,7 +100,7 @@ public async Task ProcessMessagesAsync(CancellationToken cancellationToken) { await foreach (var message in _transport.MessageReader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { - _logger.TransportMessageRead(EndpointName, message.GetType().Name); + LogMessageRead(EndpointName, message.GetType().Name); _ = ProcessMessageAsync(); async Task ProcessMessageAsync() @@ -142,7 +141,7 @@ ex is OperationCanceledException && if (!isUserCancellation && message is JsonRpcRequest request) { - _logger.RequestHandlerError(EndpointName, request.Method, ex); + LogRequestHandlerException(EndpointName, request.Method, ex); await _transport.SendMessageAsync(new JsonRpcError { Id = request.Id, @@ -156,8 +155,14 @@ await _transport.SendMessageAsync(new JsonRpcError } else if (ex is not OperationCanceledException) { - var payload = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage); - _logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex); + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogMessageHandlerExceptionSensitive(EndpointName, message.GetType().Name, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage), ex); + } + else + { + LogMessageHandlerException(EndpointName, message.GetType().Name, ex); + } } } finally @@ -174,7 +179,7 @@ await _transport.SendMessageAsync(new JsonRpcError catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // Normal shutdown - _logger.EndpointMessageProcessingCancelled(EndpointName); + LogEndpointMessageProcessingCanceled(EndpointName); } finally { @@ -226,7 +231,7 @@ private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken break; default: - _logger.EndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name); + LogEndpointHandlerUnexpectedMessageType(EndpointName, message.GetType().Name); break; } } @@ -252,7 +257,7 @@ private async Task HandleNotification(JsonRpcNotification notification, Cancella _handlingRequests.TryGetValue(cn.RequestId, out var cts)) { await cts.CancelAsync().ConfigureAwait(false); - _logger.RequestCanceled(cn.RequestId, cn.Reason); + LogRequestCanceled(EndpointName, cn.RequestId, cn.Reason); } } catch @@ -267,18 +272,13 @@ private async Task HandleNotification(JsonRpcNotification notification, Cancella 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); } else { - _logger.NoRequestFoundForMessageWithId(EndpointName, messageWithId.Id.ToString()); + LogNoRequestFoundForMessageWithId(EndpointName, messageWithId.Id); } } @@ -286,13 +286,14 @@ private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId { 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); } - _logger.RequestHandlerCalled(EndpointName, request.Method); + LogRequestHandlerCalled(EndpointName, request.Method); JsonNode? result = await handler(request, cancellationToken).ConfigureAwait(false); - _logger.RequestHandlerCompleted(EndpointName, request.Method); + LogRequestHandlerCompleted(EndpointName, request.Method); + await _transport.SendMessageAsync(new JsonRpcResponse { Id = request.Id, @@ -341,7 +342,6 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc { if (!_transport.IsConnected) { - _logger.EndpointNotConnected(EndpointName); throw new McpException("Transport is not connected"); } @@ -375,21 +375,21 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc AddTags(ref tags, activity, request, method); } - // Expensive logging, use the logging framework to check if the logger is enabled - if (_logger.IsEnabled(LogLevel.Debug)) + if (_logger.IsEnabled(LogLevel.Trace)) { - _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); - _logger.RequestSentAwaitingResponse(EndpointName, request.Method, request.Id.ToString()); // 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 // case the server could ignore it. + LogRequestSentAwaitingResponse(EndpointName, request.Method, request.Id); IJsonRpcMessage? response; using (var registration = RegisterCancellation(cancellationToken, request.Id)) { @@ -398,8 +398,8 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc if (response is JsonRpcError error) { - _logger.RequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code); - throw new McpException($"Request failed (server side): {error.Error.Message}", error.Error.Code); + LogSendingRequestFailed(EndpointName, request.Method, error.Error.Message, error.Error.Code); + throw new McpException($"Request failed (remote): {error.Error.Message}", error.Error.Code); } if (response is JsonRpcResponse success) @@ -409,13 +409,20 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc AddResponseTags(ref tags, activity, success.Result, method); } - _logger.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null"); - _logger.RequestResponseReceived(EndpointName, request.Method); + if (_logger.IsEnabled(LogLevel.Trace)) + { + LogRequestResponseReceivedSensitive(EndpointName, request.Method, success.Result?.ToJsonString() ?? "null"); + } + else + { + LogRequestResponseReceived(EndpointName, request.Method); + } + return success; } // Unexpected response type - _logger.RequestInvalidResponseType(EndpointName, request.Method); + LogSendingRequestInvalidResponseType(EndpointName, request.Method); throw new McpException("Invalid response type"); } catch (Exception ex) when (addTags) @@ -436,7 +443,6 @@ public async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken ca if (!_transport.IsConnected) { - _logger.ClientNotConnected(EndpointName); throw new McpException("Transport is not connected"); } @@ -463,9 +469,13 @@ public async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken ca 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); @@ -683,4 +693,64 @@ private static TimeSpan GetElapsed(long startingTimestamp) => 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); +} \ No newline at end of file