Bring up to 0.1.13 conformance (#1254)

This commit is contained in:
Stephen Toub 2026-02-10 11:33:52 -05:00 • committed by GitHub
commit 27b5eb8031
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1249 additions and 78 deletions

View file

@ -197,6 +197,7 @@ internal sealed partial class McpClientImpl : McpClient
async ct =>
{
var result = await elicitationHandler(request, ct).ConfigureAwait(false);
result = ElicitResult.WithDefaults(request, result);
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.ElicitResult);
},
options.SendTaskStatusNotifications,
@ -205,6 +206,7 @@ internal sealed partial class McpClientImpl : McpClient
// Normal synchronous execution - serialize result to JsonElement
var elicitResult = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
elicitResult = ElicitResult.WithDefaults(request, elicitResult);
return JsonSerializer.SerializeToElement(elicitResult, McpJsonUtilities.JsonContext.Default.ElicitResult);
},
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
@ -214,7 +216,11 @@ internal sealed partial class McpClientImpl : McpClient
{
requestHandlers.Set(
RequestMethods.ElicitationCreate,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
async (request, _, cancellationToken) =>
{
var result = await elicitationHandler(request, cancellationToken).ConfigureAwait(false);
return ElicitResult.WithDefaults(request, result);
},
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);
}
@ -671,4 +677,5 @@ internal sealed partial class McpClientImpl : McpClient
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client resumed existing session.")]
private partial void LogClientSessionResumed(string endpointName);
}

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Net.ServerSentEvents;
using System.Text.Json;
@ -239,7 +240,19 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
if (shouldDelay)
{
var delay = state.RetryInterval ?? _options.DefaultReconnectionInterval;
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
// Subtract time already elapsed since the SSE stream ended to more accurately
// honor the retry interval. Without this, processing overhead (HTTP response
// disposal, condition checks, etc.) inflates the observed reconnection delay.
if (state.StreamEndedTimestamp != 0)
{
delay -= ElapsedSince(state.StreamEndedTimestamp);
}
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
}
shouldDelay = true;
@ -336,9 +349,11 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
}
catch (Exception ex) when (ex is IOException or HttpRequestException)
{
state.StreamEndedTimestamp = Stopwatch.GetTimestamp();
return new() { IsNetworkError = true };
}
state.StreamEndedTimestamp = Stopwatch.GetTimestamp();
return default;
}
@ -435,6 +450,8 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
{
public string? LastEventId { get; set; }
public TimeSpan? RetryInterval { get; set; }
/// <summary>Timestamp (via Stopwatch.GetTimestamp()) when the last SSE stream ended, used to discount processing overhead from the retry delay.</summary>
public long StreamEndedTimestamp { get; set; }
}
/// <summary>
@ -445,4 +462,13 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa
public JsonRpcMessageWithId? Response { get; init; }
public bool IsNetworkError { get; init; }
}
private static TimeSpan ElapsedSince(long stopwatchTimestamp)
{
#if NET
return Stopwatch.GetElapsedTime(stopwatchTimestamp);
#else
return TimeSpan.FromSeconds((double)(Stopwatch.GetTimestamp() - stopwatchTimestamp) / Stopwatch.Frequency);
#endif
}
}