From 618342f27a434bc7e29372916295f9445ac8bef1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:15:52 +0000 Subject: [PATCH] Add logging for HTTP call completions with exceptions or non-success status codes - StreamableHttpClientSessionTransport: Add logging for POST failures and non-success, GET SSE request failures and non-success, DELETE request failures and non-success - SseClientSessionTransport: Add logging for GET SSE non-success status codes, upgrade LogRejectedPost to Warning level, remove unused LogAcceptedPost - ClientOAuthProvider: Add logging for auth server metadata non-success, token refresh failure, token exchange failure, protected resource metadata non-success, and dynamic client registration failure Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/d9f395f7-797a-47f1-a07f-1788d91811c4 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Authentication/ClientOAuthProvider.cs | 27 +++++++++ .../Client/SseClientSessionTransport.cs | 9 +-- .../StreamableHttpClientSessionTransport.cs | 58 +++++++++++++++++-- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index ecef8e15e..07eaf3987 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -341,6 +341,7 @@ private async Task GetAuthServerMetadataAsync(Uri a var response = await _httpClient.GetAsync(wellKnownEndpoint, cancellationToken).ConfigureAwait(false); if (!response.IsSuccessStatusCode) { + LogAuthServerMetadataNonSuccessStatusCode(wellKnownEndpoint, (int)response.StatusCode); continue; } @@ -443,6 +444,7 @@ private static IEnumerable GetWellKnownAuthorizationServerMetadataUris(Uri if (!httpResponse.IsSuccessStatusCode) { + LogOAuthTokenRefreshFailed((int)httpResponse.StatusCode); return null; } @@ -542,6 +544,10 @@ private async Task ExchangeCodeForTokenAsync( using var request = CreateTokenRequest(authServerMetadata.TokenEndpoint, formFields); using var httpResponse = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + if (!httpResponse.IsSuccessStatusCode) + { + LogOAuthTokenExchangeFailed((int)httpResponse.StatusCode); + } await httpResponse.EnsureSuccessStatusCodeWithResponseBodyAsync(cancellationToken).ConfigureAwait(false); var tokens = await HandleSuccessfulTokenResponseAsync(httpResponse, cancellationToken).ConfigureAwait(false); @@ -619,10 +625,15 @@ private async Task HandleSuccessfulTokenResponseAsync(HttpRespon using var httpResponse = await _httpClient.GetAsync(metadataUrl, cancellationToken).ConfigureAwait(false); if (requireSuccess) { + if (!httpResponse.IsSuccessStatusCode) + { + LogProtectedResourceMetadataNonSuccessStatusCode(metadataUrl, (int)httpResponse.StatusCode); + } await httpResponse.EnsureSuccessStatusCodeWithResponseBodyAsync(cancellationToken).ConfigureAwait(false); } else if (!httpResponse.IsSuccessStatusCode) { + LogProtectedResourceMetadataNonSuccessStatusCode(metadataUrl, (int)httpResponse.StatusCode); return null; } @@ -674,6 +685,7 @@ private async Task PerformDynamicClientRegistrationAsync( if (!httpResponse.IsSuccessStatusCode) { + LogDynamicClientRegistrationFailed((int)httpResponse.StatusCode); var errorContent = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); ThrowFailedToHandleUnauthorizedResponse($"Dynamic client registration failed with status {httpResponse.StatusCode}: {errorContent}"); } @@ -1003,4 +1015,19 @@ private static void ThrowFailedToHandleUnauthorizedResponse(string message) => [LoggerMessage(Level = LogLevel.Debug, Message = "Missing resource_metadata parameter from WWW-Authenticate header. Falling back to {MetadataUri}")] partial void LogMissingResourceMetadataParameter(Uri metadataUri); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Auth server metadata request to {Endpoint} received non-success status code {StatusCode}")] + partial void LogAuthServerMetadataNonSuccessStatusCode(Uri endpoint, int statusCode); + + [LoggerMessage(Level = LogLevel.Warning, Message = "OAuth token refresh received non-success status code {StatusCode}")] + partial void LogOAuthTokenRefreshFailed(int statusCode); + + [LoggerMessage(Level = LogLevel.Warning, Message = "OAuth token exchange received non-success status code {StatusCode}")] + partial void LogOAuthTokenExchangeFailed(int statusCode); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Protected resource metadata request to {MetadataUrl} received non-success status code {StatusCode}")] + partial void LogProtectedResourceMetadataNonSuccessStatusCode(Uri metadataUrl, int statusCode); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Dynamic client registration received non-success status code {StatusCode}")] + partial void LogDynamicClientRegistrationFailed(int statusCode); } diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index fb918989b..10bd1327e 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -160,6 +160,7 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) if (!response.IsSuccessStatusCode) { failureStatusCode = response.StatusCode; + LogHttpGetSseNonSuccessStatusCode(Name, (int)response.StatusCode); } await response.EnsureSuccessStatusCodeWithResponseBodyAsync(cancellationToken).ConfigureAwait(false); @@ -257,12 +258,12 @@ private void HandleEndpointEvent(string data) _connectionEstablished.TrySetResult(true); } - [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}'.")] + [LoggerMessage(Level = LogLevel.Warning, 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); + + [LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} HTTP GET SSE received non-success status code {StatusCode}.")] + private partial void LogHttpGetSseNonSuccessStatusCode(string endpointName, int statusCode); } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index f51e236b4..88dfa8fda 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -91,11 +91,22 @@ internal async Task SendHttpRequestAsync(JsonRpcMessage mes CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion); - var response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false); + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + LogHttpPostRequestFailed(Name, ex); + throw; + } // We'll let the caller decide whether to throw or fall back given an unsuccessful response. if (!response.IsSuccessStatusCode) { + LogHttpPostNonSuccessStatusCode(Name, (int)response.StatusCode); + // Per the MCP spec, a 404 response to a request containing an Mcp-Session-Id // indicates the session has ended. Signal completion so McpClient.Completion resolves. if (response.StatusCode == HttpStatusCode.NotFound && SessionId is not null) @@ -273,8 +284,9 @@ await SendGetSseRequestWithRetriesAsync( { response = await _httpClient.SendAsync(request, message: null, cancellationToken).ConfigureAwait(false); } - catch (HttpRequestException) + catch (HttpRequestException ex) { + LogHttpGetSseRequestFailed(Name, ex); attempt++; continue; } @@ -284,12 +296,15 @@ await SendGetSseRequestWithRetriesAsync( if (response.StatusCode >= HttpStatusCode.InternalServerError) { // Server error; retry. + LogHttpGetSseNonSuccessStatusCode(Name, (int)response.StatusCode); attempt++; continue; } if (!response.IsSuccessStatusCode) { + LogHttpGetSseNonSuccessStatusCode(Name, (int)response.StatusCode); + // Per the MCP spec, a 404 response to a request containing an Mcp-Session-Id // indicates the session has ended. Signal completion so McpClient.Completion resolves. if (response.StatusCode == HttpStatusCode.NotFound && SessionId is not null) @@ -406,8 +421,25 @@ private async Task SendDeleteRequest() using var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, _options.Endpoint); CopyAdditionalHeaders(deleteRequest.Headers, _options.AdditionalHeaders, SessionId, _negotiatedProtocolVersion); - // Do not validate we get a successful status code, because server support for the DELETE request is optional - (await _httpClient.SendAsync(deleteRequest, message: null, CancellationToken.None).ConfigureAwait(false)).Dispose(); + HttpResponseMessage response; + try + { + response = await _httpClient.SendAsync(deleteRequest, message: null, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + LogHttpDeleteRequestFailed(Name, ex); + return; + } + + using (response) + { + // Server support for the DELETE request is optional, so a 405 Method Not Allowed is expected. + if (!response.IsSuccessStatusCode) + { + LogHttpDeleteNonSuccessStatusCode(Name, (int)response.StatusCode); + } + } } private void LogJsonException(JsonException ex, string data) @@ -505,4 +537,22 @@ private void SetSessionExpired() SetDisconnected(_disconnectError); } + + [LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} HTTP POST request failed.")] + private partial void LogHttpPostRequestFailed(string endpointName, Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} HTTP POST received non-success status code {StatusCode}.")] + private partial void LogHttpPostNonSuccessStatusCode(string endpointName, int statusCode); + + [LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} HTTP GET SSE request failed.")] + private partial void LogHttpGetSseRequestFailed(string endpointName, Exception exception); + + [LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} HTTP GET SSE received non-success status code {StatusCode}.")] + private partial void LogHttpGetSseNonSuccessStatusCode(string endpointName, int statusCode); + + [LoggerMessage(Level = LogLevel.Warning, Message = "{EndpointName} HTTP DELETE request failed.")] + private partial void LogHttpDeleteRequestFailed(string endpointName, Exception exception); + + [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} HTTP DELETE received non-success status code {StatusCode}.")] + private partial void LogHttpDeleteNonSuccessStatusCode(string endpointName, int statusCode); }