SSE ClientTransport fixes and tests (#27)
SSE fixes Additional SSE integration tests with everything server
This commit is contained in:
parent
89c4913ed7
commit
30f3f192ca
3 changed files with 381 additions and 81 deletions
|
|
@ -480,4 +480,25 @@ internal static partial class Log
|
|||
this ILogger logger,
|
||||
string endpointName,
|
||||
Exception e);
|
||||
|
||||
[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);
|
||||
}
|
||||
|
|
@ -99,6 +99,13 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
"application/json"
|
||||
);
|
||||
|
||||
string messageId = "(no id)";
|
||||
RequestId? requestId = null;
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
var response = await _httpClient.PostAsync(
|
||||
_messageEndpoint,
|
||||
content,
|
||||
|
|
@ -107,31 +114,40 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Handle notifications, which don't have responses
|
||||
if (message is JsonRpcNotification)
|
||||
// Check if the message was an initialize request
|
||||
if (message is JsonRpcRequest request && request.Method == "initialize")
|
||||
{
|
||||
await HandleNotificationResponseAsync(message, responseContent, cancellationToken);
|
||||
// If the response is not a JSON-RPC response, it is an SSE message
|
||||
if (responseContent.ToLowerInvariant() == "accepted")
|
||||
{
|
||||
_logger.SSETransportPostAccepted(EndpointName, messageId);
|
||||
// The response will arrive as an SSE message
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize<JsonRpcResponse>(responseContent, _jsonOptions);
|
||||
if (initializeResponse == null)
|
||||
{
|
||||
throw new McpTransportException("Failed to initialize client");
|
||||
}
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(initializeResponse, cancellationToken);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var responseMessage = JsonSerializer.Deserialize<IJsonRpcMessage>(responseContent, _jsonOptions);
|
||||
if (responseMessage != null)
|
||||
// Otherwise, check if the response was accepted (the response will come as an SSE message)
|
||||
if (responseContent.ToLowerInvariant() == "accepted")
|
||||
{
|
||||
string messageId = "(no id)";
|
||||
if (responseMessage is IJsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(responseMessage, cancellationToken);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
_logger.SSETransportPostAccepted(EndpointName, messageId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.TransportMessageParseUnexpectedType(EndpointName, responseContent);
|
||||
_logger.SSETransportPostNotAccepted(EndpointName, messageId, responseContent);
|
||||
throw new McpTransportException("Failed to send message");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,28 +170,6 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async Task HandleNotificationResponseAsync(
|
||||
IJsonRpcMessage message,
|
||||
string responseContent,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Check for error response, but don't require one
|
||||
try
|
||||
{
|
||||
var errorResponse = JsonSerializer.Deserialize<JsonRpcError>(responseContent, _jsonOptions);
|
||||
if (errorResponse is not null)
|
||||
{
|
||||
await WriteMessageAsync(errorResponse, cancellationToken);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Ignore deserialization errors for notifications
|
||||
// This is expected as most notifications won't have responses
|
||||
}
|
||||
}
|
||||
|
||||
internal Uri? MessageEndpoint => _messageEndpoint;
|
||||
|
||||
internal SseClientTransportOptions Options => _options;
|
||||
|
|
@ -207,7 +201,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
|
||||
string? currentEvent = null;
|
||||
string? line;
|
||||
while ((line = await reader.ReadLineAsync()) != null)
|
||||
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
|
||||
{
|
||||
if (line.StartsWith("event: "))
|
||||
{
|
||||
|
|
@ -227,6 +221,16 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.TransportReadMessagesCancelled(EndpointName);
|
||||
// Normal shutdown
|
||||
}
|
||||
catch (IOException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.TransportReadMessagesCancelled(EndpointName);
|
||||
// Normal shutdown
|
||||
}
|
||||
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.TransportConnectionError(EndpointName, ex);
|
||||
|
|
@ -279,14 +283,31 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
{
|
||||
try
|
||||
{
|
||||
var endpointData = JsonSerializer.Deserialize<EndpointEventData>(data, _jsonOptions);
|
||||
if (endpointData?.Uri == null)
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
_logger.TransportEndpointEventInvalid(EndpointName, data);
|
||||
return;
|
||||
}
|
||||
|
||||
_messageEndpoint = new Uri(endpointData.Uri);
|
||||
// Check if data is absolute URI
|
||||
if (data.ToLowerInvariant().StartsWith("http://") || data.ToLowerInvariant().StartsWith("https://"))
|
||||
{
|
||||
// Since the endpoint is an absolute URI, we can use it directly
|
||||
_messageEndpoint = new Uri(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the endpoint is a relative URI, we need to combine it with the relative path of the SSE endpoint
|
||||
var hostUrl = _sseEndpoint.AbsoluteUri;
|
||||
if (hostUrl.EndsWith("/sse"))
|
||||
hostUrl = hostUrl[..^4];
|
||||
|
||||
var endpointUri = hostUrl + data;
|
||||
|
||||
_messageEndpoint = new Uri(endpointUri);
|
||||
}
|
||||
|
||||
// Set connected state
|
||||
SetConnected(true);
|
||||
_connectionEstablished.TrySetResult(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<PropertyGroup>
|
||||
<!-- license and package properties -->
|
||||
<PackageId>mcpdotnet</PackageId>
|
||||
<Version>1.0.1.1</Version>
|
||||
<Version>1.0.1.3</Version>
|
||||
<Authors>PederHP</Authors>
|
||||
<Description>.NET client library for the Model Context Protocol (MCP)</Description>
|
||||
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet</PackageProjectUrl>
|
||||
|
|
|
|||
Loading…
Reference in a new issue