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
2 changed files with 1637 additions and 395 deletions
37
src/ModelContextProtocol/Server/DestinationBoundMcpServer.cs
Normal file
37
src/ModelContextProtocol/Server/DestinationBoundMcpServer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue