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:
parent
7d77be68cf
commit
7dcba357d4
28 changed files with 1637 additions and 395 deletions
|
|
@ -5,6 +5,7 @@ using ModelContextProtocol.Protocol.Types;
|
|||
using ModelContextProtocol.Shared;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
|
|
@ -59,7 +60,7 @@ internal sealed partial class McpClient : McpEndpoint, IMcpClient
|
|||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, cancellationToken) => samplingHandler(
|
||||
(request, _, cancellationToken) => samplingHandler(
|
||||
request,
|
||||
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken),
|
||||
|
|
@ -76,7 +77,7 @@ internal sealed partial class McpClient : McpEndpoint, IMcpClient
|
|||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.RootsList,
|
||||
rootsHandler,
|
||||
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ internal static class Diagnostics
|
|||
};
|
||||
#endif
|
||||
|
||||
internal static ActivityContext ExtractActivityContext(this DistributedContextPropagator propagator, IJsonRpcMessage message)
|
||||
internal static ActivityContext ExtractActivityContext(this DistributedContextPropagator propagator, JsonRpcMessage message)
|
||||
{
|
||||
propagator.ExtractTraceIdAndState(message, ExtractContext, out var traceparent, out var tracestate);
|
||||
ActivityContext.TryParse(traceparent, tracestate, true, out var activityContext);
|
||||
|
|
@ -71,7 +71,7 @@ internal static class Diagnostics
|
|||
}
|
||||
}
|
||||
|
||||
internal static void InjectActivityContext(this DistributedContextPropagator propagator, Activity? activity, IJsonRpcMessage message)
|
||||
internal static void InjectActivityContext(this DistributedContextPropagator propagator, Activity? activity, JsonRpcMessage message)
|
||||
{
|
||||
// noop if activity is null
|
||||
propagator.Inject(activity, message, InjectContext);
|
||||
|
|
@ -100,7 +100,7 @@ internal static class Diagnostics
|
|||
}
|
||||
}
|
||||
|
||||
internal static bool ShouldInstrumentMessage(IJsonRpcMessage message) =>
|
||||
internal static bool ShouldInstrumentMessage(JsonRpcMessage message) =>
|
||||
ActivitySource.HasListeners() &&
|
||||
message switch
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public interface IMcpEndpoint : IAsyncDisposable
|
|||
/// Sends a JSON-RPC message to the connected endpoint.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// The JSON-RPC message to send. This can be any type that implements IJsonRpcMessage, such as
|
||||
/// The JSON-RPC message to send. This can be any type that implements JsonRpcMessage, such as
|
||||
/// JsonRpcRequest, JsonRpcResponse, JsonRpcNotification, or JsonRpcError.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
|
|
@ -63,7 +63,7 @@ public interface IMcpEndpoint : IAsyncDisposable
|
|||
/// The method will serialize the message and transmit it using the underlying transport mechanism.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Registers a handler to be invoked when a notification for the specified method is received.</summary>
|
||||
/// <param name="method">The notification method.</param>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ public static class McpEndpointExtensions
|
|||
string method,
|
||||
TParameters parameters,
|
||||
JsonSerializerOptions? serializerOptions = null,
|
||||
RequestId? requestId = null,
|
||||
RequestId requestId = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TResult : notnull
|
||||
{
|
||||
|
|
@ -72,7 +72,7 @@ public static class McpEndpointExtensions
|
|||
TParameters parameters,
|
||||
JsonTypeInfo<TParameters> parametersTypeInfo,
|
||||
JsonTypeInfo<TResult> resultTypeInfo,
|
||||
RequestId? requestId = null,
|
||||
RequestId requestId = default,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TResult : notnull
|
||||
{
|
||||
|
|
@ -83,15 +83,11 @@ public static class McpEndpointExtensions
|
|||
|
||||
JsonRpcRequest jsonRpcRequest = new()
|
||||
{
|
||||
Id = requestId,
|
||||
Method = method,
|
||||
Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo),
|
||||
};
|
||||
|
||||
if (requestId is { } id)
|
||||
{
|
||||
jsonRpcRequest.Id = id;
|
||||
}
|
||||
|
||||
JsonRpcResponse response = await endpoint.SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Represents any JSON-RPC message used in the Model Context Protocol (MCP).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface serves as the foundation for all message types in the JSON-RPC 2.0 protocol
|
||||
/// used by MCP, including requests, responses, notifications, and errors. JSON-RPC is a stateless,
|
||||
/// lightweight remote procedure call (RPC) protocol that uses JSON as its data format.
|
||||
/// </remarks>
|
||||
[JsonConverter(typeof(JsonRpcMessageConverter))]
|
||||
public interface IJsonRpcMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the JSON-RPC protocol version used.
|
||||
/// </summary>
|
||||
string JsonRpc { get; }
|
||||
}
|
||||
|
|
@ -16,16 +16,8 @@ namespace ModelContextProtocol.Protocol.Messages;
|
|||
/// and optional additional data to provide more context about the error.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public record JsonRpcError : IJsonRpcMessageWithId
|
||||
public class JsonRpcError : JsonRpcMessageWithId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("id")]
|
||||
public required RequestId Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed error information for the failed request, containing an error code,
|
||||
/// message, and optional additional data
|
||||
|
|
|
|||
35
src/ModelContextProtocol/Protocol/Messages/JsonRpcMessage.cs
Normal file
35
src/ModelContextProtocol/Protocol/Messages/JsonRpcMessage.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using ModelContextProtocol.Protocol.Transport;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Represents any JSON-RPC message used in the Model Context Protocol (MCP).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface serves as the foundation for all message types in the JSON-RPC 2.0 protocol
|
||||
/// used by MCP, including requests, responses, notifications, and errors. JSON-RPC is a stateless,
|
||||
/// lightweight remote procedure call (RPC) protocol that uses JSON as its data format.
|
||||
/// </remarks>
|
||||
[JsonConverter(typeof(JsonRpcMessageConverter))]
|
||||
public abstract class JsonRpcMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the JSON-RPC protocol version used.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport the <see cref="JsonRpcMessage"/> was received on or should be sent over.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to support the Streamable HTTP transport where the specification states that the server
|
||||
/// SHOULD include JSON-RPC responses in the HTTP response body for the POST request containing
|
||||
/// the corresponding JSON-RPC request. It may be <see langword="null"/> for other transports.
|
||||
/// </remarks>
|
||||
[JsonIgnore]
|
||||
public ITransport? RelatedTransport { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON-RPC message used in the Model Context Protocol (MCP) and that includes an ID.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In the JSON-RPC protocol, messages with an ID require a response from the receiver.
|
||||
/// This includes request messages (which expect a matching response) and response messages
|
||||
/// (which include the ID of the original request they're responding to).
|
||||
/// The ID is used to correlate requests with their responses, allowing asynchronous
|
||||
/// communication where multiple requests can be sent without waiting for responses.
|
||||
/// </remarks>
|
||||
public abstract class JsonRpcMessageWithId : JsonRpcMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the message identifier.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Each ID is expected to be unique within the context of a given session.
|
||||
/// </remarks>
|
||||
[JsonPropertyName("id")]
|
||||
public RequestId Id { get; init; }
|
||||
}
|
||||
|
|
@ -11,12 +11,8 @@ namespace ModelContextProtocol.Protocol.Messages;
|
|||
/// They are useful for one-way communication, such as log notifications and progress updates.
|
||||
/// Unlike requests, notifications do not include an ID field, since there will be no response to match with it.
|
||||
/// </remarks>
|
||||
public record JsonRpcNotification : IJsonRpcMessage
|
||||
public class JsonRpcNotification : JsonRpcMessage
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the notification method.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -14,16 +14,8 @@ namespace ModelContextProtocol.Protocol.Messages;
|
|||
/// and return either a <see cref="JsonRpcResponse"/> with the result, or a <see cref="JsonRpcError"/>
|
||||
/// if the method execution fails.
|
||||
/// </remarks>
|
||||
public record JsonRpcRequest : IJsonRpcMessageWithId
|
||||
public class JsonRpcRequest : JsonRpcMessageWithId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <inheritdoc/>
|
||||
[JsonPropertyName("id")]
|
||||
public RequestId Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the method to invoke.
|
||||
/// </summary>
|
||||
|
|
@ -35,4 +27,15 @@ public record JsonRpcRequest : IJsonRpcMessageWithId
|
|||
/// </summary>
|
||||
[JsonPropertyName("params")]
|
||||
public JsonNode? Params { get; init; }
|
||||
|
||||
internal JsonRpcRequest WithId(RequestId id)
|
||||
{
|
||||
return new JsonRpcRequest
|
||||
{
|
||||
JsonRpc = JsonRpc,
|
||||
Id = id,
|
||||
Method = Method,
|
||||
Params = Params
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,8 @@ namespace ModelContextProtocol.Protocol.Messages;
|
|||
/// This class represents a successful response with a result. For error responses, see <see cref="JsonRpcError"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public record JsonRpcResponse : IJsonRpcMessageWithId
|
||||
public class JsonRpcResponse : JsonRpcMessageWithId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public string JsonRpc { get; init; } = "2.0";
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonPropertyName("id")]
|
||||
public required RequestId Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the result of the method invocation.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -26,22 +26,6 @@ namespace ModelContextProtocol.Protocol.Transport;
|
|||
/// </remarks>
|
||||
public interface ITransport : IAsyncDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether the transport is currently connected and able to send/receive messages.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The <see cref="IsConnected"/> property indicates the current state of the transport connection.
|
||||
/// When <see langword="true"/>, the transport is ready to send and receive messages. When <see langword="false"/>,
|
||||
/// any attempt to send messages will typically result in exceptions being thrown.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The property transitions to <see langword="true"/> when the transport successfully establishes a connection,
|
||||
/// and transitions to <see langword="false"/> when the transport is disposed or encounters a connection error.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
bool IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a channel reader for receiving messages from the transport.
|
||||
/// </summary>
|
||||
|
|
@ -56,7 +40,7 @@ public interface ITransport : IAsyncDisposable
|
|||
/// any already transmitted messages are consumed.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
ChannelReader<IJsonRpcMessage> MessageReader { get; }
|
||||
ChannelReader<JsonRpcMessage> MessageReader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends a JSON-RPC message through the transport.
|
||||
|
|
@ -76,5 +60,5 @@ public interface ITransport : IAsyncDisposable
|
|||
/// rather than accessing this method directly.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,21 +68,21 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(
|
||||
IJsonRpcMessage message,
|
||||
JsonRpcMessage message,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_messageEndpoint == null)
|
||||
throw new InvalidOperationException("Transport not connected");
|
||||
|
||||
using var content = new StringContent(
|
||||
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage),
|
||||
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
|
||||
string messageId = "(no id)";
|
||||
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
|
@ -213,17 +213,20 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
}
|
||||
}
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Normal shutdown
|
||||
LogTransportReadMessagesCancelled(Name);
|
||||
_connectionEstablished.TrySetCanceled(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogTransportReadMessagesFailed(Name, ex);
|
||||
_connectionEstablished.TrySetException(ex);
|
||||
throw;
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Normal shutdown
|
||||
LogTransportReadMessagesCancelled(Name);
|
||||
_connectionEstablished.TrySetCanceled(cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogTransportReadMessagesFailed(Name, ex);
|
||||
_connectionEstablished.TrySetException(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -241,7 +244,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
|
||||
try
|
||||
{
|
||||
var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
|
||||
var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
|
||||
if (message == null)
|
||||
{
|
||||
LogTransportMessageParseUnexpectedTypeSensitive(Name, data);
|
||||
|
|
@ -249,7 +252,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
|
|||
}
|
||||
|
||||
string messageId = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
using System.Text;
|
||||
using System.Buffers;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Channels;
|
||||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using ModelContextProtocol.Utils;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
||||
|
|
@ -22,16 +18,22 @@ namespace ModelContextProtocol.Protocol.Transport;
|
|||
/// such as when streaming completion results or providing progress updates during long-running operations.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class SseResponseStreamTransport(Stream sseResponseStream, string messageEndpoint = "/message") : ITransport
|
||||
/// <param name="sseResponseStream">The response stream to write MCP JSON-RPC messages as SSE events to.</param>
|
||||
/// <param name="messageEndpoint">
|
||||
/// The relative or absolute URI the client should use to post MCP JSON-RPC messages for this session.
|
||||
/// These messages should be passed to <see cref="OnMessageReceivedAsync(JsonRpcMessage, CancellationToken)"/>.
|
||||
/// Defaults to "/message".
|
||||
/// </param>
|
||||
public sealed class SseResponseStreamTransport(Stream sseResponseStream, string? messageEndpoint = "/message") : ITransport
|
||||
{
|
||||
private readonly Channel<IJsonRpcMessage> _incomingChannel = CreateBoundedChannel<IJsonRpcMessage>();
|
||||
private readonly Channel<SseItem<IJsonRpcMessage?>> _outgoingSseChannel = CreateBoundedChannel<SseItem<IJsonRpcMessage?>>();
|
||||
private readonly SseWriter _sseWriter = new(messageEndpoint);
|
||||
private readonly Channel<JsonRpcMessage> _incomingChannel = Channel.CreateBounded<JsonRpcMessage>(new BoundedChannelOptions(1)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
private Task? _sseWriteTask;
|
||||
private Utf8JsonWriter? _jsonWriter;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected { get; private set; }
|
||||
private bool _isConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the transport and writes the JSON-RPC messages sent via <see cref="SendMessageAsync"/>
|
||||
|
|
@ -39,54 +41,27 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string
|
|||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the send loop that writes JSON-RPC messages to the SSE response stream.</returns>
|
||||
public Task RunAsync(CancellationToken cancellationToken)
|
||||
public async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type,
|
||||
// so we fib and special-case the "endpoint" event type in the formatter.
|
||||
if (!_outgoingSseChannel.Writer.TryWrite(new SseItem<IJsonRpcMessage?>(null, "endpoint")))
|
||||
{
|
||||
throw new InvalidOperationException($"You must call ${nameof(RunAsync)} before calling ${nameof(SendMessageAsync)}.");
|
||||
}
|
||||
|
||||
IsConnected = true;
|
||||
|
||||
var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
|
||||
return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
|
||||
}
|
||||
|
||||
private void WriteJsonRpcMessageToBuffer(SseItem<IJsonRpcMessage?> item, IBufferWriter<byte> writer)
|
||||
{
|
||||
if (item.EventType == "endpoint")
|
||||
{
|
||||
writer.Write(Encoding.UTF8.GetBytes(messageEndpoint));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage!);
|
||||
_isConnected = true;
|
||||
await _sseWriter.WriteAllAsync(sseResponseStream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ChannelReader<IJsonRpcMessage> MessageReader => _incomingChannel.Reader;
|
||||
public ChannelReader<JsonRpcMessage> MessageReader => _incomingChannel.Reader;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask DisposeAsync()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
IsConnected = false;
|
||||
_isConnected = false;
|
||||
_incomingChannel.Writer.TryComplete();
|
||||
_outgoingSseChannel.Writer.TryComplete();
|
||||
return new ValueTask(_sseWriteTask ?? Task.CompletedTask);
|
||||
await _sseWriter.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException($"Transport is not connected. Make sure to call {nameof(RunAsync)} first.");
|
||||
}
|
||||
|
||||
// Emit redundant "event: message" lines for better compatibility with other SDKs.
|
||||
await _outgoingSseChannel.Writer.WriteAsync(new SseItem<IJsonRpcMessage?>(message, SseParser.EventTypeDefault), cancellationToken).ConfigureAwait(false);
|
||||
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -111,34 +86,15 @@ public sealed class SseResponseStreamTransport(Stream sseResponseStream, string
|
|||
/// sequencing of operations in the transport lifecycle.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public async Task OnMessageReceivedAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
|
||||
public async Task OnMessageReceivedAsync(JsonRpcMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsConnected)
|
||||
Throw.IfNull(message);
|
||||
|
||||
if (!_isConnected)
|
||||
{
|
||||
throw new InvalidOperationException($"Transport is not connected. Make sure to call {nameof(RunAsync)} first.");
|
||||
}
|
||||
|
||||
await _incomingChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Channel<T> CreateBoundedChannel<T>(int capacity = 1) =>
|
||||
Channel.CreateBounded<T>(new BoundedChannelOptions(capacity)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter<byte> writer)
|
||||
{
|
||||
if (_jsonWriter is null)
|
||||
{
|
||||
_jsonWriter = new Utf8JsonWriter(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_jsonWriter.Reset(writer);
|
||||
}
|
||||
|
||||
return _jsonWriter;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
120
src/ModelContextProtocol/Protocol/Transport/SseWriter.cs
Normal file
120
src/ModelContextProtocol/Protocol/Transport/SseWriter.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Buffers;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
||||
internal sealed class SseWriter(string? messageEndpoint = null, BoundedChannelOptions? channelOptions = null) : IAsyncDisposable
|
||||
{
|
||||
private readonly Channel<SseItem<JsonRpcMessage?>> _messages = Channel.CreateBounded<SseItem<JsonRpcMessage?>>(channelOptions ?? new BoundedChannelOptions(1)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
private Utf8JsonWriter? _jsonWriter;
|
||||
private Task? _writeTask;
|
||||
private CancellationToken? _writeCancellationToken;
|
||||
|
||||
private readonly SemaphoreSlim _disposeLock = new(1, 1);
|
||||
private bool _disposed;
|
||||
|
||||
public Func<IAsyncEnumerable<SseItem<JsonRpcMessage?>>, CancellationToken, IAsyncEnumerable<SseItem<JsonRpcMessage?>>>? MessageFilter { get; set; }
|
||||
|
||||
public Task WriteAllAsync(Stream sseResponseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
// When messageEndpoint is set, the very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single
|
||||
// item of a different type, so we fib and special-case the "endpoint" event type in the formatter.
|
||||
if (messageEndpoint is not null && !_messages.Writer.TryWrite(new SseItem<JsonRpcMessage?>(null, "endpoint")))
|
||||
{
|
||||
throw new InvalidOperationException("You must call RunAsync before calling SendMessageAsync.");
|
||||
}
|
||||
|
||||
_writeCancellationToken = cancellationToken;
|
||||
|
||||
var messages = _messages.Reader.ReadAllAsync(cancellationToken);
|
||||
if (MessageFilter is not null)
|
||||
{
|
||||
messages = MessageFilter(messages, cancellationToken);
|
||||
}
|
||||
|
||||
_writeTask = SseFormatter.WriteAsync(messages, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
|
||||
return _writeTask;
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Throw.IfNull(message);
|
||||
|
||||
using var _ = await _disposeLock.LockAsync().ConfigureAwait(false);
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
// Don't throw an ODE, because this is disposed internally when the transport disconnects due to an abort
|
||||
// or sending all the responses for the a give given Streamable HTTP POST request, so the user might not be at fault.
|
||||
// There's precedence for no-oping here similar to writing to the response body of an aborted request in ASP.NET Core.
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit redundant "event: message" lines for better compatibility with other SDKs.
|
||||
await _messages.Writer.WriteAsync(new SseItem<JsonRpcMessage?>(message, SseParser.EventTypeDefault), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
using var _ = await _disposeLock.LockAsync().ConfigureAwait(false);
|
||||
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_messages.Writer.Complete();
|
||||
try
|
||||
{
|
||||
if (_writeTask is not null)
|
||||
{
|
||||
await _writeTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_writeCancellationToken?.IsCancellationRequested == true)
|
||||
{
|
||||
// Ignore exceptions caused by intentional cancellation during shutdown.
|
||||
}
|
||||
finally
|
||||
{
|
||||
_jsonWriter?.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteJsonRpcMessageToBuffer(SseItem<JsonRpcMessage?> item, IBufferWriter<byte> writer)
|
||||
{
|
||||
if (item.EventType == "endpoint" && messageEndpoint is not null)
|
||||
{
|
||||
writer.Write(Encoding.UTF8.GetBytes(messageEndpoint));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.JsonContext.Default.JsonRpcMessage!);
|
||||
}
|
||||
|
||||
private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter<byte> writer)
|
||||
{
|
||||
if (_jsonWriter is null)
|
||||
{
|
||||
_jsonWriter = new Utf8JsonWriter(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_jsonWriter.Reset(writer);
|
||||
}
|
||||
|
||||
return _jsonWriter;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
|||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the underlying process has exited or cannot be accessed.
|
||||
/// </exception>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Exception? processException = null;
|
||||
bool hasExited = false;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ internal class StreamClientSessionTransport : TransportBase
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
|
|
@ -65,12 +65,12 @@ internal class StreamClientSessionTransport : TransportBase
|
|||
}
|
||||
|
||||
string id = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
id = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)));
|
||||
var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)));
|
||||
|
||||
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
|
|
@ -143,11 +143,11 @@ internal class StreamClientSessionTransport : TransportBase
|
|||
{
|
||||
try
|
||||
{
|
||||
var message = (IJsonRpcMessage?)JsonSerializer.Deserialize(line.AsSpan().Trim(), McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)));
|
||||
var message = (JsonRpcMessage?)JsonSerializer.Deserialize(line.AsSpan().Trim(), McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)));
|
||||
if (message != null)
|
||||
{
|
||||
string messageId = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public class StreamServerTransport : TransportBase
|
|||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
|
|
@ -66,14 +66,14 @@ public class StreamServerTransport : TransportBase
|
|||
using var _ = await _sendLock.LockAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string id = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
id = messageWithId.Id.ToString();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)), cancellationToken).ConfigureAwait(false);
|
||||
await JsonSerializer.SerializeAsync(_outputStream, message, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage)), cancellationToken).ConfigureAwait(false);
|
||||
await _outputStream.WriteAsync(s_newlineBytes, cancellationToken).ConfigureAwait(false);
|
||||
await _outputStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
|
@ -109,10 +109,10 @@ public class StreamServerTransport : TransportBase
|
|||
|
||||
try
|
||||
{
|
||||
if (JsonSerializer.Deserialize(line, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage))) is IJsonRpcMessage message)
|
||||
if (JsonSerializer.Deserialize(line, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonRpcMessage))) is JsonRpcMessage message)
|
||||
{
|
||||
string messageId = "(no id)";
|
||||
if (message is IJsonRpcMessageWithId messageWithId)
|
||||
if (message is JsonRpcMessageWithId messageWithId)
|
||||
{
|
||||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Buffers;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.ServerSentEvents;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Handles processing the request/response body pairs for the Streamable HTTP transport.
|
||||
/// This is typically used via <see cref="JsonRpcMessage.RelatedTransport"/>.
|
||||
/// </summary>
|
||||
internal sealed class StreamableHttpPostTransport(ChannelWriter<JsonRpcMessage>? incomingChannel, IDuplexPipe httpBodies) : ITransport
|
||||
{
|
||||
private readonly SseWriter _sseWriter = new();
|
||||
private readonly HashSet<RequestId> _pendingRequests = [];
|
||||
|
||||
// REVIEW: Should we introduce a send-only interface for RelatedTransport?
|
||||
public ChannelReader<JsonRpcMessage> MessageReader => throw new NotSupportedException("JsonRpcMessage.RelatedTransport should only be used for sending messages.");
|
||||
|
||||
/// <returns>
|
||||
/// True, if data was written to the respond body.
|
||||
/// False, if nothing was written because the request body did not contain any <see cref="JsonRpcRequest"/> messages to respond to.
|
||||
/// The HTTP application should typically respond with an empty "202 Accepted" response in this scenario.
|
||||
/// </returns>
|
||||
public async ValueTask<bool> RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// The incomingChannel is null to handle the potential client GET request to handle unsolicited JsonRpcMessages.
|
||||
if (incomingChannel is not null)
|
||||
{
|
||||
await OnPostBodyReceivedAsync(httpBodies.Input, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_pendingRequests.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_sseWriter.MessageFilter = StopOnFinalResponseFilter;
|
||||
await _sseWriter.WriteAllAsync(httpBodies.Output.AsStream(), cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _sseWriter.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async IAsyncEnumerable<SseItem<JsonRpcMessage?>> StopOnFinalResponseFilter(IAsyncEnumerable<SseItem<JsonRpcMessage?>> messages, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await foreach (var message in messages.WithCancellation(cancellationToken))
|
||||
{
|
||||
yield return message;
|
||||
|
||||
if (message.Data is JsonRpcResponse response)
|
||||
{
|
||||
if (_pendingRequests.Remove(response.Id) && _pendingRequests.Count == 0)
|
||||
{
|
||||
// Complete the SSE response stream now that all pending requests have been processed.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask OnPostBodyReceivedAsync(PipeReader streamableHttpRequestBody, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await IsJsonArrayAsync(streamableHttpRequestBody, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var message = await JsonSerializer.DeserializeAsync(streamableHttpRequestBody.AsStream(), McpJsonUtilities.JsonContext.Default.JsonRpcMessage, cancellationToken).ConfigureAwait(false);
|
||||
await OnMessageReceivedAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Batched JSON-RPC message
|
||||
var messages = JsonSerializer.DeserializeAsyncEnumerable(streamableHttpRequestBody.AsStream(), McpJsonUtilities.JsonContext.Default.JsonRpcMessage, cancellationToken).ConfigureAwait(false);
|
||||
await foreach (var message in messages.WithCancellation(cancellationToken))
|
||||
{
|
||||
await OnMessageReceivedAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask OnMessageReceivedAsync(JsonRpcMessage? message, CancellationToken cancellationToken)
|
||||
{
|
||||
if (message is null)
|
||||
{
|
||||
throw new InvalidOperationException("Received invalid null message.");
|
||||
}
|
||||
|
||||
if (message is JsonRpcRequest request)
|
||||
{
|
||||
_pendingRequests.Add(request.Id);
|
||||
}
|
||||
|
||||
message.RelatedTransport = this;
|
||||
|
||||
// Really an assertion. This doesn't get called when incomingChannel is null for GET requests.
|
||||
Throw.IfNull(incomingChannel);
|
||||
await incomingChannel.WriteAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask<bool> IsJsonArrayAsync(PipeReader requestBody, CancellationToken cancellationToken)
|
||||
{
|
||||
// REVIEW: Should we bother trimming whitespace before checking for '['?
|
||||
var firstCharacterResult = await requestBody.ReadAtLeastAsync(1, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (firstCharacterResult.Buffer.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> firstCharBuffer = stackalloc byte[1];
|
||||
firstCharacterResult.Buffer.Slice(0, 1).CopyTo(firstCharBuffer);
|
||||
return firstCharBuffer[0] == (byte)'[';
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Never consume data when checking for '['. System.Text.Json still needs to consume it.
|
||||
requestBody.AdvanceTo(firstCharacterResult.Buffer.Start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace ModelContextProtocol.Protocol.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an <see cref="ITransport"/> implementation using Server-Sent Events (SSE) for server-to-client communication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This transport provides one-way communication from server to client using the SSE protocol over HTTP,
|
||||
/// while receiving client messages through a separate mechanism. It writes messages as
|
||||
/// SSE events to a response stream, typically associated with an HTTP response.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This transport is used in scenarios where the server needs to push messages to the client in real-time,
|
||||
/// such as when streaming completion results or providing progress updates during long-running operations.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class StreamableHttpServerTransport : ITransport
|
||||
{
|
||||
// For JsonRpcMessages without a RelatedTransport, we don't want to block just because the client didn't make a GET request to handle unsolicited messages.
|
||||
private readonly SseWriter _sseWriter = new(channelOptions: new BoundedChannelOptions(1)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.DropOldest,
|
||||
});
|
||||
private readonly Channel<JsonRpcMessage> _incomingChannel = Channel.CreateBounded<JsonRpcMessage>(new BoundedChannelOptions(1)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
private readonly CancellationTokenSource _disposeCts = new();
|
||||
|
||||
private int _getRequestStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Handles an optional SSE GET request a client using the Streamable HTTP transport might make by
|
||||
/// writing any unsolicited JSON-RPC messages sent via <see cref="SendMessageAsync"/>
|
||||
/// to the SSE response stream until cancellation is requested or the transport is disposed.
|
||||
/// </summary>
|
||||
/// <param name="sseResponseStream">The response stream to write MCP JSON-RPC messages as SSE events to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A task representing the send loop that writes JSON-RPC messages to the SSE response stream.</returns>
|
||||
public async Task HandleGetRequest(Stream sseResponseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _getRequestStarted, 1) == 1)
|
||||
{
|
||||
throw new InvalidOperationException("Session resumption is not yet supported. Please start a new session.");
|
||||
}
|
||||
|
||||
using var getCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, cancellationToken);
|
||||
await _sseWriter.WriteAllAsync(sseResponseStream, getCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a Streamable HTTP POST request processing both the request body and response body ensuring that
|
||||
/// <see cref="JsonRpcResponse"/> and other correlated messages are sent back to the client directly in response
|
||||
/// to the <see cref="JsonRpcRequest"/> that initiated the message.
|
||||
/// </summary>
|
||||
/// <param name="httpBodies">The duplex pipe facilitates the reading and writing of HTTP request and response data.</param>
|
||||
/// <param name="cancellationToken">This token allows for the operation to be canceled if needed.</param>
|
||||
/// <returns>
|
||||
/// True, if data was written to the respond body.
|
||||
/// False, if nothing was written because the request body did not contain any <see cref="JsonRpcRequest"/> messages to respond to.
|
||||
/// The HTTP application should typically respond with an empty "202 Accepted" response in this scenario.
|
||||
/// </returns>
|
||||
public async Task<bool> HandlePostRequest(IDuplexPipe httpBodies, CancellationToken cancellationToken)
|
||||
{
|
||||
using var postCts = CancellationTokenSource.CreateLinkedTokenSource(_disposeCts.Token, cancellationToken);
|
||||
await using var postTransport = new StreamableHttpPostTransport(_incomingChannel.Writer, httpBodies);
|
||||
return await postTransport.RunAsync(postCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ChannelReader<JsonRpcMessage> MessageReader => _incomingChannel.Reader;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _sseWriter.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_disposeCts.Cancel();
|
||||
try
|
||||
{
|
||||
await _sseWriter.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposeCts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue