Add server-side Streamable HTTP transport support (#330)

- Changes IJsonRpcMessage to an abstract base class so RelatedTransport will always be available
- Streamable HTTP supports multiple concurrent HTTP request with their own indpendent SSE response streams
- RelatedTransport indicates the source or destination of the JsonRpcMessage

- Changes the default RequestId to a JSON number for better compatibility with MCP servers in the wild
This commit is contained in:
Stephen Halter 2025-04-18 17:07:23 -07:00 • committed by GitHub
commit 7dcba357d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1637 additions and 395 deletions

View file

@ -106,11 +106,11 @@ public class McpClientFactoryTests
private class NopTransport : ITransport, IClientTransport
{
private readonly Channel<IJsonRpcMessage> _channel = Channel.CreateUnbounded<IJsonRpcMessage>();
private readonly Channel<JsonRpcMessage> _channel = Channel.CreateUnbounded<JsonRpcMessage>();
public bool IsConnected => true;
public ChannelReader<IJsonRpcMessage> MessageReader => _channel.Reader;
public ChannelReader<JsonRpcMessage> MessageReader => _channel.Reader;
public Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default) => Task.FromResult<ITransport>(this);
@ -118,7 +118,7 @@ public class McpClientFactoryTests
public string Name => "Test Nop Transport";
public virtual Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
public virtual Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
switch (message)
{
@ -148,7 +148,7 @@ public class McpClientFactoryTests
{
public const string ExpectedMessage = "Something failed";
public override Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
throw new InvalidOperationException(ExpectedMessage);
}

View file

@ -33,7 +33,7 @@ public class NotificationHandlerTests : ClientServerTestBase
}
}
Assert.Equal(Iterations, counter);
Assert.Equal(Iterations, counter);
}
[Fact]

View file

@ -619,7 +619,7 @@ public class McpServerTests : LoggedTest
public McpServerOptions ServerOptions => throw new NotImplementedException();
public IServiceProvider? Services => throw new NotImplementedException();
public LoggingLevel? LoggingLevel => throw new NotImplementedException();
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) =>
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) =>
throw new NotImplementedException();
public Task RunAsync(CancellationToken cancellationToken = default) =>
throw new NotImplementedException();

View file

@ -216,6 +216,7 @@ public class SseClientTransportTests : LoggedTest
await session.DisposeAsync();
Assert.False(session.IsConnected);
var transportBase = Assert.IsAssignableFrom<TransportBase>(session);
Assert.False(transportBase.IsConnected);
}
}

View file

@ -9,19 +9,19 @@ namespace ModelContextProtocol.Tests.Utils;
public class TestServerTransport : ITransport
{
private readonly Channel<IJsonRpcMessage> _messageChannel;
private readonly Channel<JsonRpcMessage> _messageChannel;
public bool IsConnected { get; set; }
public ChannelReader<IJsonRpcMessage> MessageReader => _messageChannel;
public ChannelReader<JsonRpcMessage> MessageReader => _messageChannel;
public List<IJsonRpcMessage> SentMessages { get; } = [];
public List<JsonRpcMessage> SentMessages { get; } = [];
public Action<IJsonRpcMessage>? OnMessageSent { get; set; }
public Action<JsonRpcMessage>? OnMessageSent { get; set; }
public TestServerTransport()
{
_messageChannel = Channel.CreateUnbounded<IJsonRpcMessage>(new UnboundedChannelOptions
_messageChannel = Channel.CreateUnbounded<JsonRpcMessage>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = true,
@ -36,7 +36,7 @@ public class TestServerTransport : ITransport
return default;
}
public async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
SentMessages.Add(message);
if (message is JsonRpcRequest request)
@ -77,7 +77,7 @@ public class TestServerTransport : ITransport
}, cancellationToken);
}
private async Task WriteMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
private async Task WriteMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
await _messageChannel.Writer.WriteAsync(message, cancellationToken);
}