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
2 changed files with 1637 additions and 395 deletions

View file

@ -0,0 +1,37 @@
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using System.Diagnostics;
namespace ModelContextProtocol.Server;
internal sealed class DestinationBoundMcpServer(McpServer server, ITransport? transport) : IMcpServer
{
public string EndpointName => server.EndpointName;
public ClientCapabilities? ClientCapabilities => server.ClientCapabilities;
public Implementation? ClientInfo => server.ClientInfo;
public McpServerOptions ServerOptions => server.ServerOptions;
public IServiceProvider? Services => server.Services;
public LoggingLevel? LoggingLevel => server.LoggingLevel;
public ValueTask DisposeAsync() => server.DisposeAsync();
public IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) => server.RegisterNotificationHandler(method, handler);
// This will throw because the server must already be running for this class to be constructed, but it should give us a good Exception message.
public Task RunAsync(CancellationToken cancellationToken) => server.RunAsync(cancellationToken);
public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
{
Debug.Assert(message.RelatedTransport is null);
message.RelatedTransport = transport;
return server.SendMessageAsync(message, cancellationToken);
}
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
{
Debug.Assert(request.RelatedTransport is null);
request.RelatedTransport = transport;
return server.SendRequestAsync(request, cancellationToken);
}
}

View file

@ -7,8 +7,7 @@ using ModelContextProtocol.Shared;
using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json;
using System.Runtime.CompilerServices;
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Server;
@ -158,7 +157,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
private void SetPingHandler()
{
RequestHandlers.Set(RequestMethods.Ping,
SetHandler(RequestMethods.Ping,
async (request, _) => new PingResult(),
McpJsonUtilities.JsonContext.Default.JsonNode,
McpJsonUtilities.JsonContext.Default.PingResult);
@ -167,7 +166,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
private void SetInitializeHandler(McpServerOptions options)
{
RequestHandlers.Set(RequestMethods.Initialize,
async (request, _) =>
async (request, _, _) =>
{
ClientCapabilities = request?.Capabilities ?? new();
ClientInfo = request?.ClientInfo;
@ -201,9 +200,9 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
$"but {nameof(CompletionsCapability.CompleteHandler)} was not specified.");
// This capability is not optional, so return an empty result if there is no handler.
RequestHandlers.Set(
SetHandler(
RequestMethods.CompletionComplete,
(request, cancellationToken) => InvokeHandlerAsync(completeHandler, request, cancellationToken),
completeHandler,
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
McpJsonUtilities.JsonContext.Default.CompleteResult);
}
@ -228,22 +227,22 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
listResourcesHandler ??= static async (_, _) => new ListResourcesResult();
RequestHandlers.Set(
SetHandler(
RequestMethods.ResourcesList,
(request, cancellationToken) => InvokeHandlerAsync(listResourcesHandler, request, cancellationToken),
listResourcesHandler,
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
RequestHandlers.Set(
SetHandler(
RequestMethods.ResourcesRead,
(request, cancellationToken) => InvokeHandlerAsync(readResourceHandler, request, cancellationToken),
readResourceHandler,
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
listResourceTemplatesHandler ??= static async (_, _) => new ListResourceTemplatesResult();
RequestHandlers.Set(
SetHandler(
RequestMethods.ResourcesTemplatesList,
(request, cancellationToken) => InvokeHandlerAsync(listResourceTemplatesHandler, request, cancellationToken),
listResourceTemplatesHandler,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
@ -261,15 +260,15 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
$"but {nameof(ResourcesCapability.SubscribeToResourcesHandler)} or {nameof(ResourcesCapability.UnsubscribeFromResourcesHandler)} was not specified.");
}
RequestHandlers.Set(
SetHandler(
RequestMethods.ResourcesSubscribe,
(request, cancellationToken) => InvokeHandlerAsync(subscribeHandler, request, cancellationToken),
subscribeHandler,
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
RequestHandlers.Set(
SetHandler(
RequestMethods.ResourcesUnsubscribe,
(request, cancellationToken) => InvokeHandlerAsync(unsubscribeHandler, request, cancellationToken),
unsubscribeHandler,
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
}
@ -359,15 +358,15 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
}
}
RequestHandlers.Set(
SetHandler(
RequestMethods.PromptsList,
(request, cancellationToken) => InvokeHandlerAsync(listPromptsHandler, request, cancellationToken),
listPromptsHandler,
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
RequestHandlers.Set(
SetHandler(
RequestMethods.PromptsGet,
(request, cancellationToken) => InvokeHandlerAsync(getPromptHandler, request, cancellationToken),
getPromptHandler,
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
McpJsonUtilities.JsonContext.Default.GetPromptResult);
}
@ -457,15 +456,15 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
}
}
RequestHandlers.Set(
SetHandler(
RequestMethods.ToolsList,
(request, cancellationToken) => InvokeHandlerAsync(listToolsHandler, request, cancellationToken),
listToolsHandler,
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
McpJsonUtilities.JsonContext.Default.ListToolsResult);
RequestHandlers.Set(
SetHandler(
RequestMethods.ToolsCall,
(request, cancellationToken) => InvokeHandlerAsync(callToolHandler, request, cancellationToken),
callToolHandler,
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
McpJsonUtilities.JsonContext.Default.CallToolResponse);
}
@ -478,7 +477,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
RequestHandlers.Set(
RequestMethods.LoggingSetLevel,
(request, cancellationToken) =>
(request, destinationTransport, cancellationToken) =>
{
// Store the provided level.
if (request is not null)
@ -494,7 +493,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
// If a handler was provided, now delegate to it.
if (setLoggingLevelHandler is not null)
{
return InvokeHandlerAsync(setLoggingLevelHandler, request, cancellationToken);
return InvokeHandlerAsync(setLoggingLevelHandler, request, destinationTransport, cancellationToken);
}
// Otherwise, consider it handled.
@ -507,11 +506,12 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
private ValueTask<TResult> InvokeHandlerAsync<TParams, TResult>(
Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler,
TParams? args,
CancellationToken cancellationToken)
ITransport? destinationTransport = null,
CancellationToken cancellationToken = default)
{
return _servicesScopePerRequest ?
InvokeScopedAsync(handler, args, cancellationToken) :
handler(new(this) { Params = args }, cancellationToken);
handler(new(new DestinationBoundMcpServer(this, destinationTransport)) { Params = args }, cancellationToken);
async ValueTask<TResult> InvokeScopedAsync(
Func<RequestContext<TParams>, CancellationToken, ValueTask<TResult>> handler,
@ -522,7 +522,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
try
{
return await handler(
new RequestContext<TParams>(this)
new RequestContext<TParams>(new DestinationBoundMcpServer(this, destinationTransport))
{
Services = scope?.ServiceProvider ?? Services,
Params = args
@ -539,6 +539,18 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
}
}
private void SetHandler<TRequest, TResponse>(
string method,
Func<RequestContext<TRequest>, CancellationToken, ValueTask<TResponse>> handler,
JsonTypeInfo<TRequest> requestTypeInfo,
JsonTypeInfo<TResponse> responseTypeInfo)
{
RequestHandlers.Set(method,
(request, destinationTransport, cancellationToken) =>
InvokeHandlerAsync(handler, request, destinationTransport, cancellationToken),
requestTypeInfo, responseTypeInfo);
}
/// <summary>Maps a <see cref="LogLevel"/> to a <see cref="LoggingLevel"/>.</summary>
internal static LoggingLevel ToLoggingLevel(LogLevel level) =>
level switch
@ -551,4 +563,4 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
LogLevel.Critical => Protocol.Types.LoggingLevel.Critical,
_ => Protocol.Types.LoggingLevel.Emergency,
};
}
}