SSE ClientTransport fixes and tests (#27)
SSE fixes Additional SSE integration tests with everything server
This commit is contained in:
parent
89c4913ed7
commit
30f3f192ca
8 changed files with 381 additions and 81 deletions
|
|
@ -9,10 +9,10 @@
|
|||
<PropertyGroup>
|
||||
<!-- license and package properties -->
|
||||
<PackageId>McpDotNet.Extensions.AI</PackageId>
|
||||
<Version>1.0.1.1</Version>
|
||||
<Version>1.0.1.3</Version>
|
||||
<Authors>PederHP</Authors>
|
||||
<Description>Microsoft.Extensions.AI integration for the Model Context Protocol (MCP). Enables seamless use of MCP tools as AI functions in any IChatClient implementation.</Description>
|
||||
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet/src/McpDotNet.Extensions.AI</PackageProjectUrl>
|
||||
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet/tree/main/src/McpDotNet.Extensions.AI</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/PederHP/mcpdotnet</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>mcp;ai;microsoft-extensions-ai;chatclient;llm</PackageTags>
|
||||
|
|
@ -26,6 +26,10 @@
|
|||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="/" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="McpDotNet.Extensions.AI.Tests" />
|
||||
</ItemGroup>
|
||||
|
|
@ -33,7 +37,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.3.0-preview.1.25114.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI" Version="9.3.0-preview.1.25114.11" />
|
||||
<PackageReference Include="mcpdotnet" Version="1.0.1.1" />
|
||||
<PackageReference Include="mcpdotnet" Version="1.0.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class McpSessionScope : IAsyncDisposable
|
|||
/// <summary>
|
||||
/// The list of tools exposed by the servers in this scope.
|
||||
/// </summary>
|
||||
public IList<AITool> Tools { get; private set; }
|
||||
public IList<AITool> Tools { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The server instructions provided by the servers in this scope.
|
||||
|
|
@ -84,7 +84,7 @@ public class McpSessionScope : IAsyncDisposable
|
|||
return client;
|
||||
}
|
||||
|
||||
/// </inheritdoc>
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
63
tests/mcpdotnet.Tests/EverythingSseServerFixture.cs
Normal file
63
tests/mcpdotnet.Tests/EverythingSseServerFixture.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace McpDotNet.Tests;
|
||||
|
||||
public class EverythingSseServerFixture : IAsyncDisposable
|
||||
{
|
||||
private Process _process;
|
||||
|
||||
public async Task StartAsync()
|
||||
{
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "docker",
|
||||
Arguments = "run -p 3001:3001 --rm tzolov/mcp-everything-server:v1",
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
|
||||
_process = Process.Start(processStartInfo);
|
||||
|
||||
// Wait for the server to start
|
||||
await Task.Delay(10000);
|
||||
}
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Find the container ID
|
||||
var psInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "docker",
|
||||
Arguments = "ps -q --filter ancestor=tzolov/mcp-everything-server:v1",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
using var psProcess = Process.Start(psInfo);
|
||||
string containerId = await psProcess.StandardOutput.ReadToEndAsync();
|
||||
containerId = containerId.Trim();
|
||||
|
||||
if (!string.IsNullOrEmpty(containerId))
|
||||
{
|
||||
// Stop the container
|
||||
var stopInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "docker",
|
||||
Arguments = $"stop {containerId}",
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
using var stopProcess = Process.Start(stopInfo);
|
||||
await stopProcess.WaitForExitAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception but don't throw
|
||||
Console.Error.WriteLine($"Error stopping Docker container: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Transport;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using McpDotNet.Tests.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
|
@ -49,18 +50,169 @@ public class SseIntegrationTests
|
|||
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Send a test message through POST endpoint
|
||||
var testMessage = new JsonRpcRequest()
|
||||
await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" });
|
||||
|
||||
// Assert
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAndReceiveMessage_EverythingServerWithSse()
|
||||
{
|
||||
using var loggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
|
||||
builder.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Debug));
|
||||
|
||||
await using var fixture = new EverythingSseServerFixture();
|
||||
await fixture.StartAsync();
|
||||
|
||||
var defaultOptions = new McpClientOptions
|
||||
{
|
||||
Id = RequestId.FromNumber(1),
|
||||
Method = "test/message",
|
||||
Params = new { message = "Hello, SSE!" }
|
||||
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
|
||||
};
|
||||
using var httpClient = new HttpClient();
|
||||
await httpClient.PostAsync(
|
||||
"http://localhost:5000/message",
|
||||
new StringContent(JsonSerializer.Serialize(testMessage))
|
||||
|
||||
var defaultConfig = new McpServerConfig
|
||||
{
|
||||
Id = "everything",
|
||||
Name = "Everything",
|
||||
TransportType = "sse",
|
||||
TransportOptions = new Dictionary<string, string>(),
|
||||
Location = "http://localhost:3001/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
);
|
||||
|
||||
// Create client and run tests
|
||||
var client = await factory.GetClientAsync("everything");
|
||||
var tools = await client.ListToolsAsync();
|
||||
|
||||
// assert
|
||||
Assert.NotNull(tools);
|
||||
Assert.NotEmpty(tools.Tools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sampling_Sse_EverythingServer()
|
||||
{
|
||||
// arrange
|
||||
using var loggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
|
||||
builder.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Debug));
|
||||
|
||||
await using var fixture = new EverythingSseServerFixture();
|
||||
await fixture.StartAsync();
|
||||
|
||||
var defaultOptions = new McpClientOptions
|
||||
{
|
||||
ClientInfo = new()
|
||||
{
|
||||
Name = "IntegrationTestClient",
|
||||
Version = "1.0.0"
|
||||
},
|
||||
Capabilities = new()
|
||||
{
|
||||
Sampling = new()
|
||||
}
|
||||
};
|
||||
|
||||
var defaultConfig = new McpServerConfig
|
||||
{
|
||||
Id = "everything",
|
||||
Name = "Everything",
|
||||
TransportType = "sse",
|
||||
TransportOptions = new Dictionary<string, string>(),
|
||||
Location = "http://localhost:3001/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
);
|
||||
var client = await factory.GetClientAsync("everything");
|
||||
|
||||
// Set up the sampling handler
|
||||
int samplingHandlerCalls = 0;
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
client.SamplingHandler = async (_, _) =>
|
||||
{
|
||||
samplingHandlerCalls++;
|
||||
return new CreateMessageResult
|
||||
{
|
||||
Model = "test-model",
|
||||
Role = "assistant",
|
||||
Content = new Content
|
||||
{
|
||||
Type = "text",
|
||||
Text = "Test response"
|
||||
}
|
||||
};
|
||||
};
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
// Call the server's sampleLLM tool which should trigger our sampling handler
|
||||
var result = await client.CallToolAsync(
|
||||
"sampleLLM",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["prompt"] = "Test prompt",
|
||||
["maxTokens"] = 100
|
||||
}
|
||||
);
|
||||
|
||||
// assert
|
||||
Assert.NotNull(result);
|
||||
var textContent = Assert.Single(result.Content);
|
||||
Assert.Equal("text", textContent.Type);
|
||||
Assert.False(string.IsNullOrEmpty(textContent.Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAndReceiveMessage_InMemoryServer_WithFullEndpointEventUri()
|
||||
{
|
||||
// Arrange
|
||||
using var loggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder =>
|
||||
builder.AddConsole()
|
||||
.SetMinimumLevel(LogLevel.Debug));
|
||||
|
||||
await using TestSseServer server = new(logger: loggerFactory.CreateLogger<TestSseServer>());
|
||||
server.UseFullUrlForEndpointEvent = true;
|
||||
await server.StartAsync();
|
||||
|
||||
|
||||
var defaultOptions = new McpClientOptions
|
||||
{
|
||||
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
|
||||
};
|
||||
|
||||
var defaultConfig = new McpServerConfig
|
||||
{
|
||||
Id = "test_server",
|
||||
Name = "In-memory Test Server",
|
||||
TransportType = "sse",
|
||||
TransportOptions = new Dictionary<string, string>(),
|
||||
Location = "http://localhost:5000/sse"
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
[defaultConfig],
|
||||
defaultOptions,
|
||||
loggerFactory
|
||||
);
|
||||
|
||||
// Act
|
||||
var client = await factory.GetClientAsync("test_server");
|
||||
|
||||
// Wait for SSE connection to be established
|
||||
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
// Send a test message through POST endpoint
|
||||
await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" });
|
||||
|
||||
// Assert
|
||||
Assert.True(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ public sealed class TestSseServer : IAsyncDisposable
|
|||
_messagePath = "/message";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is to be able to use the full URL for the endpoint event.
|
||||
/// </summary>
|
||||
public bool UseFullUrlForEndpointEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Full URL for the SSE endpoint, e.g. "http://localhost:5000/sse"
|
||||
/// </summary>
|
||||
|
|
@ -120,14 +125,15 @@ public sealed class TestSseServer : IAsyncDisposable
|
|||
_sseClients.Add(writer);
|
||||
|
||||
// Immediately send the "endpoint" event with the POST URL
|
||||
var endpointData = new
|
||||
{
|
||||
uri = MessageEndpoint
|
||||
};
|
||||
var json = JsonSerializer.Serialize(endpointData);
|
||||
|
||||
await writer.WriteLineAsync("event: endpoint");
|
||||
await writer.WriteLineAsync($"data: {json}");
|
||||
if (UseFullUrlForEndpointEvent)
|
||||
{
|
||||
await writer.WriteLineAsync($"data: {MessageEndpoint}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await writer.WriteLineAsync($"data: {_messagePath}");
|
||||
}
|
||||
await writer.WriteLineAsync(); // blank line to end an SSE message
|
||||
await writer.FlushAsync();
|
||||
|
||||
|
|
@ -183,33 +189,50 @@ public sealed class TestSseServer : IAsyncDisposable
|
|||
using var reader = new StreamReader(request.InputStream);
|
||||
string content = await reader.ReadToEndAsync();
|
||||
|
||||
var jsonRpcNotification = JsonSerializer.Deserialize<JsonRpcNotification>(content);
|
||||
if (jsonRpcNotification != null && jsonRpcNotification.Method != "initialize")
|
||||
{
|
||||
// Test server so just ignore notifications
|
||||
|
||||
// Set status code to 202 Accepted
|
||||
response.StatusCode = 202;
|
||||
|
||||
// Write "accepted" as the response body
|
||||
using var writer = new StreamWriter(response.OutputStream);
|
||||
await writer.WriteAsync("accepted");
|
||||
await writer.FlushAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonRpcRequest = JsonSerializer.Deserialize<JsonRpcRequest>(content);
|
||||
|
||||
if (jsonRpcRequest != null && !jsonRpcRequest.Method.StartsWith("notification"))
|
||||
if (jsonRpcRequest != null)
|
||||
{
|
||||
if (jsonRpcRequest.Method == "initialize")
|
||||
{
|
||||
await HandleInitializationRequest(response, jsonRpcRequest);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set status code to 202 Accepted
|
||||
response.StatusCode = 202;
|
||||
|
||||
// Method not found - proper JSON-RPC error response
|
||||
await SendJsonRpcErrorAsync(response, jsonRpcRequest.Id, -32601, $"Method '{jsonRpcRequest.Method}' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var notification = JsonSerializer.Deserialize<JsonRpcNotification>(content);
|
||||
if (notification != null)
|
||||
{
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "application/json";
|
||||
await using var writer = new StreamWriter(response.OutputStream);
|
||||
await writer.WriteAsync(JsonSerializer.Serialize(new { jsonrpc = "2.0" }));
|
||||
return;
|
||||
}
|
||||
// Write "accepted" as the response body
|
||||
using var writer = new StreamWriter(response.OutputStream);
|
||||
await writer.WriteAsync("accepted");
|
||||
await writer.FlushAsync();
|
||||
|
||||
// Invalid JSON-RPC message - proper JSON-RPC error response
|
||||
await SendJsonRpcErrorAsync(response, null, -32700, "Parse error");
|
||||
// Then process the message and send the response via SSE
|
||||
if (jsonRpcRequest != null)
|
||||
{
|
||||
// Process the request and generate a response
|
||||
var responseMessage = await ProcessRequestAsync(jsonRpcRequest);
|
||||
|
||||
// Send the response via the SSE connection instead of HTTP response
|
||||
await SendSseMessageAsync(responseMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -222,6 +245,22 @@ public sealed class TestSseServer : IAsyncDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<JsonRpcResponse> ProcessRequestAsync(JsonRpcRequest jsonRpcRequest)
|
||||
{
|
||||
// This is a test server so we just echo back the request
|
||||
return new JsonRpcResponse
|
||||
{
|
||||
Id = jsonRpcRequest.Id,
|
||||
Result = jsonRpcRequest.Params
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SendSseMessageAsync(JsonRpcResponse jsonRpcResponse)
|
||||
{
|
||||
// This is a test server so we just send to all connected clients
|
||||
await BroadcastMessageAsync(JsonSerializer.Serialize(jsonRpcResponse));
|
||||
}
|
||||
|
||||
private async Task SendJsonRpcErrorAsync(HttpListenerResponse response, RequestId? id, int code, string message, string? data = null)
|
||||
{
|
||||
var errorResponse = new JsonRpcError
|
||||
|
|
|
|||
Loading…
Reference in a new issue