SSE client transport: tolerate return of RPC responses in POST requests. (#445)
* SSE client transport: handle return of RPC responses in POST requests. * Remove accidental change. * Address feedback * Remove unused namespace.
This commit is contained in:
parent
ee9b04b7e5
commit
77f9e64e04
2 changed files with 30 additions and 14 deletions
|
|
@ -92,26 +92,18 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
||||||
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders);
|
StreamableHttpClientSessionTransport.CopyAdditionalHeaders(httpRequestMessage.Headers, _options.AdditionalHeaders);
|
||||||
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
|
var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
if (!response.IsSuccessStatusCode)
|
||||||
|
|
||||||
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(responseContent) || responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
LogAcceptedPost(Name, messageId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
{
|
{
|
||||||
LogRejectedPostSensitive(Name, messageId, responseContent);
|
LogRejectedPostSensitive(Name, messageId, await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LogRejectedPost(Name, messageId);
|
LogRejectedPost(Name, messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException("Failed to send message");
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,22 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
|
||||||
Assert.True(true);
|
Assert.True(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ConnectAndReceiveMessage_ServerReturningJsonInPostRequest()
|
||||||
|
{
|
||||||
|
await using var app = Builder.Build();
|
||||||
|
MapAbsoluteEndpointUriMcp(app, respondInJson: true);
|
||||||
|
|
||||||
|
await app.StartAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
await using var mcpClient = await ConnectMcpClientAsync();
|
||||||
|
|
||||||
|
// Send a test message through POST endpoint
|
||||||
|
await mcpClient.SendNotificationAsync("test/message", new Envelope { Message = "Hello, SSE!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(true);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ConnectAndReceiveNotification_InMemoryServer()
|
public async Task ConnectAndReceiveNotification_InMemoryServer()
|
||||||
{
|
{
|
||||||
|
|
@ -220,7 +236,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
|
||||||
Assert.Equal("Failed to add header '' with value '' from AdditionalHeaders.", ex.Message);
|
Assert.Equal("Failed to add header '' with value '' from AdditionalHeaders.", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MapAbsoluteEndpointUriMcp(IEndpointRouteBuilder endpoints)
|
private static void MapAbsoluteEndpointUriMcp(IEndpointRouteBuilder endpoints, bool respondInJson = false)
|
||||||
{
|
{
|
||||||
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
||||||
var optionsSnapshot = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
|
var optionsSnapshot = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
|
||||||
|
|
@ -267,7 +283,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
|
||||||
await Results.BadRequest("Session not started.").ExecuteAsync(context);
|
await Results.BadRequest("Session not started.").ExecuteAsync(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var message = (JsonRpcMessage?)await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), context.RequestAborted);
|
var message = await context.Request.ReadFromJsonAsync<JsonRpcMessage>(McpJsonUtilities.DefaultOptions, context.RequestAborted);
|
||||||
if (message is null)
|
if (message is null)
|
||||||
{
|
{
|
||||||
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
|
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
|
||||||
|
|
@ -276,7 +292,15 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
|
||||||
|
|
||||||
await session.OnMessageReceivedAsync(message, context.RequestAborted);
|
await session.OnMessageReceivedAsync(message, context.RequestAborted);
|
||||||
context.Response.StatusCode = StatusCodes.Status202Accepted;
|
context.Response.StatusCode = StatusCodes.Status202Accepted;
|
||||||
await context.Response.WriteAsync("Accepted");
|
|
||||||
|
if (respondInJson)
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsJsonAsync(message, McpJsonUtilities.DefaultOptions, cancellationToken: context.RequestAborted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await context.Response.WriteAsync("Accepted");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue