Move notification handler registrations to capabilities (#207)
* Move notification handler registrations to capabilities Currently request handlers are set on the capability objects, but notification handlers are set after construction via an AddNotificationHandler method on the IMcpEndpoint interface. This moves handler specification to be at construction as well. This makes it more consistent with request handlers, simplifies the IMcpEndpoint interface to just be about message sending, and avoids a concurrency bug that could occur if someone tried to add a handler while the endpoint was processing notifications. * Address more feedback and further cleanup
This commit is contained in:
parent
8fcdf95785
commit
d21d933876
35 changed files with 292 additions and 288 deletions
|
|
@ -1,7 +1,6 @@
|
||||||
using ModelContextProtocol;
|
using ModelContextProtocol;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace QuickstartWeatherServer.Tools;
|
namespace QuickstartWeatherServer.Tools;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ namespace ModelContextProtocol.Client;
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
internal sealed class McpClient : McpEndpoint, IMcpClient
|
internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
{
|
{
|
||||||
|
private static Implementation DefaultImplementation { get; } = new()
|
||||||
|
{
|
||||||
|
Name = DefaultAssemblyName.Name ?? nameof(McpClient),
|
||||||
|
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||||
|
};
|
||||||
|
|
||||||
private readonly IClientTransport _clientTransport;
|
private readonly IClientTransport _clientTransport;
|
||||||
private readonly McpClientOptions _options;
|
private readonly McpClientOptions _options;
|
||||||
|
|
||||||
|
|
@ -29,43 +35,53 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
||||||
/// <param name="serverConfig">The server configuration.</param>
|
/// <param name="serverConfig">The server configuration.</param>
|
||||||
/// <param name="loggerFactory">The logger factory.</param>
|
/// <param name="loggerFactory">The logger factory.</param>
|
||||||
public McpClient(IClientTransport clientTransport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
public McpClient(IClientTransport clientTransport, McpClientOptions? options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
||||||
: base(loggerFactory)
|
: base(loggerFactory)
|
||||||
{
|
{
|
||||||
|
options ??= new();
|
||||||
|
|
||||||
_clientTransport = clientTransport;
|
_clientTransport = clientTransport;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
|
||||||
EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
||||||
|
|
||||||
if (options.Capabilities?.Sampling is { } samplingCapability)
|
if (options.Capabilities is { } capabilities)
|
||||||
{
|
{
|
||||||
if (samplingCapability.SamplingHandler is not { } samplingHandler)
|
if (capabilities.NotificationHandlers is { } notificationHandlers)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
|
NotificationHandlers.AddRange(notificationHandlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetRequestHandler(
|
if (capabilities.Sampling is { } samplingCapability)
|
||||||
RequestMethods.SamplingCreateMessage,
|
|
||||||
(request, cancellationToken) => samplingHandler(
|
|
||||||
request,
|
|
||||||
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
|
||||||
cancellationToken),
|
|
||||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
|
||||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.Capabilities?.Roots is { } rootsCapability)
|
|
||||||
{
|
|
||||||
if (rootsCapability.RootsHandler is not { } rootsHandler)
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
|
if (samplingCapability.SamplingHandler is not { } samplingHandler)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestHandlers.Set(
|
||||||
|
RequestMethods.SamplingCreateMessage,
|
||||||
|
(request, cancellationToken) => samplingHandler(
|
||||||
|
request,
|
||||||
|
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||||
|
cancellationToken),
|
||||||
|
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||||
|
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetRequestHandler(
|
if (capabilities.Roots is { } rootsCapability)
|
||||||
RequestMethods.RootsList,
|
{
|
||||||
rootsHandler,
|
if (rootsCapability.RootsHandler is not { } rootsHandler)
|
||||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
{
|
||||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestHandlers.Set(
|
||||||
|
RequestMethods.RootsList,
|
||||||
|
rootsHandler,
|
||||||
|
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||||
|
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,20 +112,20 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Send initialize request
|
// Send initialize request
|
||||||
var initializeResponse = await this.SendRequestAsync(
|
var initializeResponse = await this.SendRequestAsync(
|
||||||
RequestMethods.Initialize,
|
RequestMethods.Initialize,
|
||||||
new InitializeRequestParams
|
new InitializeRequestParams
|
||||||
{
|
{
|
||||||
ProtocolVersion = _options.ProtocolVersion,
|
ProtocolVersion = _options.ProtocolVersion,
|
||||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||||
ClientInfo = _options.ClientInfo
|
ClientInfo = _options.ClientInfo ?? DefaultImplementation,
|
||||||
},
|
},
|
||||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
||||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// Store server information
|
// Store server information
|
||||||
_logger.ServerCapabilitiesReceived(EndpointName,
|
_logger.ServerCapabilitiesReceived(EndpointName,
|
||||||
|
|
|
||||||
|
|
@ -5,30 +5,12 @@ using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Client;
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
/// <summary>Provides factory methods for creating MCP clients.</summary>
|
/// <summary>Provides factory methods for creating MCP clients.</summary>
|
||||||
public static class McpClientFactory
|
public static class McpClientFactory
|
||||||
{
|
{
|
||||||
/// <summary>Default client options to use when none are supplied.</summary>
|
|
||||||
private static readonly McpClientOptions s_defaultClientOptions = CreateDefaultClientOptions();
|
|
||||||
|
|
||||||
/// <summary>Creates default client options to use when no options are supplied.</summary>
|
|
||||||
private static McpClientOptions CreateDefaultClientOptions()
|
|
||||||
{
|
|
||||||
var asmName = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName();
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
ClientInfo = new()
|
|
||||||
{
|
|
||||||
Name = asmName.Name ?? "McpClient",
|
|
||||||
Version = asmName.Version?.ToString() ?? "1.0.0",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
||||||
/// <param name="serverConfig">Configuration for the target server to which the client should connect.</param>
|
/// <param name="serverConfig">Configuration for the target server to which the client should connect.</param>
|
||||||
/// <param name="clientOptions">
|
/// <param name="clientOptions">
|
||||||
|
|
@ -52,7 +34,6 @@ public static class McpClientFactory
|
||||||
{
|
{
|
||||||
Throw.IfNull(serverConfig);
|
Throw.IfNull(serverConfig);
|
||||||
|
|
||||||
clientOptions ??= s_defaultClientOptions;
|
|
||||||
createTransportFunc ??= CreateTransport;
|
createTransportFunc ??= CreateTransport;
|
||||||
|
|
||||||
string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ public class McpClientOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information about this client implementation.
|
/// Information about this client implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Implementation ClientInfo { get; set; }
|
public Implementation? ClientInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client capabilities to advertise to the server.
|
/// Client capabilities to advertise to the server.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
using ModelContextProtocol.Utils;
|
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Reflection;
|
using ModelContextProtocol.Server;
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
|
|
||||||
|
|
@ -25,18 +24,6 @@ internal sealed class McpServerOptionsSetup(
|
||||||
{
|
{
|
||||||
Throw.IfNull(options);
|
Throw.IfNull(options);
|
||||||
|
|
||||||
// Configure the option's server information based on the current process,
|
|
||||||
// if it otherwise lacks server information.
|
|
||||||
if (options.ServerInfo is not { } serverInfo)
|
|
||||||
{
|
|
||||||
var assemblyName = Assembly.GetEntryAssembly()?.GetName();
|
|
||||||
options.ServerInfo = new()
|
|
||||||
{
|
|
||||||
Name = assemblyName?.Name ?? "McpServer",
|
|
||||||
Version = assemblyName?.Version?.ToString() ?? "1.0.0",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all of the provided tools into a tools collection. If the options already has
|
// Collect all of the provided tools into a tools collection. If the options already has
|
||||||
// a collection, add to it, otherwise create a new one. We want to maintain the identity
|
// a collection, add to it, otherwise create a new one. We want to maintain the identity
|
||||||
// of an existing collection in case someone has provided their own derived type, wants
|
// of an existing collection in case someone has provided their own derived type, wants
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,4 @@ public interface IMcpEndpoint : IAsyncDisposable
|
||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
|
||||||
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a handler for server notifications of a specific method.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="method">The notification method to handle.</param>
|
|
||||||
/// <param name="handler">The async handler function to process notifications.</param>
|
|
||||||
/// <remarks>
|
|
||||||
/// <para>
|
|
||||||
/// Each method may have multiple handlers. Adding a handler for a method that already has one
|
|
||||||
/// will not replace the existing handler.
|
|
||||||
/// </para>
|
|
||||||
/// <para>
|
|
||||||
/// <see cref="NotificationMethods"> provides constants for common notification methods.</see>
|
|
||||||
/// </para>
|
|
||||||
/// </remarks>
|
|
||||||
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public class McpTransportException : Exception
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The message that describes the error.</param>
|
/// <param name="message">The message that describes the error.</param>
|
||||||
/// <param name="innerException">The exception that is the cause of the current exception.</param>
|
/// <param name="innerException">The exception that is the cause of the current exception.</param>
|
||||||
public McpTransportException(string message, Exception innerException)
|
public McpTransportException(string message, Exception? innerException)
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,22 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
public override async Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_process.HasExited)
|
Exception? processException = null;
|
||||||
|
bool hasExited = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hasExited = _process.HasExited;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
processException = e;
|
||||||
|
hasExited = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasExited)
|
||||||
{
|
{
|
||||||
Logger.TransportNotConnected(EndpointName);
|
Logger.TransportNotConnected(EndpointName);
|
||||||
throw new McpTransportException("Transport is not connected");
|
throw new McpTransportException("Transport is not connected", processException);
|
||||||
}
|
}
|
||||||
|
|
||||||
await base.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
await base.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
@ -33,7 +45,7 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
protected override ValueTask CleanupAsync(CancellationToken cancellationToken)
|
protected override ValueTask CleanupAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
StdioClientTransport.DisposeProcess(_process, processStarted: true, Logger, _options.ShutdownTimeout, EndpointName);
|
StdioClientTransport.DisposeProcess(_process, processRunning: true, Logger, _options.ShutdownTimeout, EndpointName);
|
||||||
|
|
||||||
return base.CleanupAsync(cancellationToken);
|
return base.CleanupAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,13 +129,25 @@ public sealed class StdioClientTransport : IClientTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void DisposeProcess(
|
internal static void DisposeProcess(
|
||||||
Process? process, bool processStarted, ILogger logger, TimeSpan shutdownTimeout, string endpointName)
|
Process? process, bool processRunning, ILogger logger, TimeSpan shutdownTimeout, string endpointName)
|
||||||
{
|
{
|
||||||
if (process is not null)
|
if (process is not null)
|
||||||
{
|
{
|
||||||
|
if (processRunning)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
processRunning = !process.HasExited;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
processRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (processStarted && !process.HasExited)
|
if (processRunning)
|
||||||
{
|
{
|
||||||
// Wait for the process to exit.
|
// Wait for the process to exit.
|
||||||
// Kill the while process tree because the process may spawn child processes
|
// Kill the while process tree because the process may spawn child processes
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,7 @@ public sealed class StdioServerTransport : StreamServerTransport
|
||||||
private static string GetServerName(McpServerOptions serverOptions)
|
private static string GetServerName(McpServerOptions serverOptions)
|
||||||
{
|
{
|
||||||
Throw.IfNull(serverOptions);
|
Throw.IfNull(serverOptions);
|
||||||
Throw.IfNull(serverOptions.ServerInfo);
|
|
||||||
Throw.IfNull(serverOptions.ServerInfo.Name);
|
|
||||||
|
|
||||||
return serverOptions.ServerInfo.Name;
|
return serverOptions.ServerInfo?.Name ?? McpServer.DefaultImplementation.Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
@ -26,6 +27,14 @@ public class ClientCapabilities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("sampling")]
|
[JsonPropertyName("sampling")]
|
||||||
public SamplingCapability? Sampling { get; set; }
|
public SamplingCapability? Sampling { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets notification handlers to register with the client.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When constructed, the client will enumerate these handlers, which may contain multiple handlers per key.
|
||||||
|
/// The client will not re-enumerate the sequence.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, Task>>>? NotificationHandlers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A request from the server to get a list of root URIs from the client.
|
/// A request from the server to get a list of root URIs from the client.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Text.Json.Serialization;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Protocol.Types;
|
namespace ModelContextProtocol.Protocol.Types;
|
||||||
|
|
||||||
|
|
@ -37,4 +38,12 @@ public class ServerCapabilities
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("tools")]
|
[JsonPropertyName("tools")]
|
||||||
public ToolsCapability? Tools { get; set; }
|
public ToolsCapability? Tools { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Gets or sets notification handlers to register with the server.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When constructed, the server will enumerate these handlers, which may contain multiple handlers per key.
|
||||||
|
/// The server will not re-enumerate the sequence.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, Task>>>? NotificationHandlers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ namespace ModelContextProtocol.Server;
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
internal sealed class McpServer : McpEndpoint, IMcpServer
|
internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
{
|
{
|
||||||
|
internal static Implementation DefaultImplementation { get; } = new()
|
||||||
|
{
|
||||||
|
Name = DefaultAssemblyName.Name ?? nameof(McpServer),
|
||||||
|
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||||
|
};
|
||||||
|
|
||||||
private readonly EventHandler? _toolsChangedDelegate;
|
private readonly EventHandler? _toolsChangedDelegate;
|
||||||
private readonly EventHandler? _promptsChangedDelegate;
|
private readonly EventHandler? _promptsChangedDelegate;
|
||||||
|
|
||||||
|
|
@ -32,9 +38,11 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
Throw.IfNull(transport);
|
Throw.IfNull(transport);
|
||||||
Throw.IfNull(options);
|
Throw.IfNull(options);
|
||||||
|
|
||||||
|
options ??= new();
|
||||||
|
|
||||||
ServerOptions = options;
|
ServerOptions = options;
|
||||||
Services = serviceProvider;
|
Services = serviceProvider;
|
||||||
_endpointName = $"Server ({options.ServerInfo.Name} {options.ServerInfo.Version})";
|
_endpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})";
|
||||||
|
|
||||||
_toolsChangedDelegate = delegate
|
_toolsChangedDelegate = delegate
|
||||||
{
|
{
|
||||||
|
|
@ -51,7 +59,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
AddNotificationHandler(NotificationMethods.InitializedNotification, _ =>
|
NotificationHandlers.Add(NotificationMethods.InitializedNotification, _ =>
|
||||||
{
|
{
|
||||||
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
|
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
|
||||||
{
|
{
|
||||||
|
|
@ -66,6 +74,11 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.Capabilities?.NotificationHandlers is { } notificationHandlers)
|
||||||
|
{
|
||||||
|
NotificationHandlers.AddRange(notificationHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
SetToolsHandler(options);
|
SetToolsHandler(options);
|
||||||
SetInitializeHandler(options);
|
SetInitializeHandler(options);
|
||||||
SetCompletionHandler(options);
|
SetCompletionHandler(options);
|
||||||
|
|
@ -131,7 +144,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
|
|
||||||
private void SetPingHandler()
|
private void SetPingHandler()
|
||||||
{
|
{
|
||||||
SetRequestHandler(RequestMethods.Ping,
|
RequestHandlers.Set(RequestMethods.Ping,
|
||||||
(request, _) => Task.FromResult(new PingResult()),
|
(request, _) => Task.FromResult(new PingResult()),
|
||||||
McpJsonUtilities.JsonContext.Default.JsonNode,
|
McpJsonUtilities.JsonContext.Default.JsonNode,
|
||||||
McpJsonUtilities.JsonContext.Default.PingResult);
|
McpJsonUtilities.JsonContext.Default.PingResult);
|
||||||
|
|
@ -139,7 +152,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
|
|
||||||
private void SetInitializeHandler(McpServerOptions options)
|
private void SetInitializeHandler(McpServerOptions options)
|
||||||
{
|
{
|
||||||
SetRequestHandler(RequestMethods.Initialize,
|
RequestHandlers.Set(RequestMethods.Initialize,
|
||||||
(request, _) =>
|
(request, _) =>
|
||||||
{
|
{
|
||||||
ClientCapabilities = request?.Capabilities ?? new();
|
ClientCapabilities = request?.Capabilities ?? new();
|
||||||
|
|
@ -153,7 +166,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
{
|
{
|
||||||
ProtocolVersion = options.ProtocolVersion,
|
ProtocolVersion = options.ProtocolVersion,
|
||||||
Instructions = options.ServerInstructions,
|
Instructions = options.ServerInstructions,
|
||||||
ServerInfo = options.ServerInfo,
|
ServerInfo = options.ServerInfo ?? DefaultImplementation,
|
||||||
Capabilities = ServerCapabilities ?? new(),
|
Capabilities = ServerCapabilities ?? new(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -164,7 +177,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
private void SetCompletionHandler(McpServerOptions options)
|
private void SetCompletionHandler(McpServerOptions options)
|
||||||
{
|
{
|
||||||
// This capability is not optional, so return an empty result if there is no handler.
|
// This capability is not optional, so return an empty result if there is no handler.
|
||||||
SetRequestHandler(RequestMethods.CompletionComplete,
|
RequestHandlers.Set(RequestMethods.CompletionComplete,
|
||||||
options.GetCompletionHandler is { } handler ?
|
options.GetCompletionHandler is { } handler ?
|
||||||
(request, ct) => handler(new(this, request), ct) :
|
(request, ct) => handler(new(this, request), ct) :
|
||||||
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }),
|
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }),
|
||||||
|
|
@ -190,20 +203,20 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
|
|
||||||
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
|
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ResourcesList,
|
RequestMethods.ResourcesList,
|
||||||
(request, ct) => listResourcesHandler(new(this, request), ct),
|
(request, ct) => listResourcesHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
|
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ResourcesRead,
|
RequestMethods.ResourcesRead,
|
||||||
(request, ct) => readResourceHandler(new(this, request), ct),
|
(request, ct) => readResourceHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
|
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
|
||||||
|
|
||||||
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
|
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ResourcesTemplatesList,
|
RequestMethods.ResourcesTemplatesList,
|
||||||
(request, ct) => listResourceTemplatesHandler(new(this, request), ct),
|
(request, ct) => listResourceTemplatesHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
|
||||||
|
|
@ -221,13 +234,13 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
throw new McpException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
|
throw new McpException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
|
||||||
}
|
}
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ResourcesSubscribe,
|
RequestMethods.ResourcesSubscribe,
|
||||||
(request, ct) => subscribeHandler(new(this, request), ct),
|
(request, ct) => subscribeHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
McpJsonUtilities.JsonContext.Default.EmptyResult);
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ResourcesUnsubscribe,
|
RequestMethods.ResourcesUnsubscribe,
|
||||||
(request, ct) => unsubscribeHandler(new(this, request), ct),
|
(request, ct) => unsubscribeHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
|
||||||
|
|
@ -314,13 +327,13 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.PromptsList,
|
RequestMethods.PromptsList,
|
||||||
(request, ct) => listPromptsHandler(new(this, request), ct),
|
(request, ct) => listPromptsHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
|
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.PromptsGet,
|
RequestMethods.PromptsGet,
|
||||||
(request, ct) => getPromptHandler(new(this, request), ct),
|
(request, ct) => getPromptHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
|
||||||
|
|
@ -407,13 +420,13 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ToolsList,
|
RequestMethods.ToolsList,
|
||||||
(request, ct) => listToolsHandler(new(this, request), ct),
|
(request, ct) => listToolsHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
|
||||||
McpJsonUtilities.JsonContext.Default.ListToolsResult);
|
McpJsonUtilities.JsonContext.Default.ListToolsResult);
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.ToolsCall,
|
RequestMethods.ToolsCall,
|
||||||
(request, ct) => callToolHandler(new(this, request), ct),
|
(request, ct) => callToolHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
|
||||||
|
|
@ -432,7 +445,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
throw new McpException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
|
throw new McpException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
|
||||||
}
|
}
|
||||||
|
|
||||||
SetRequestHandler(
|
RequestHandlers.Set(
|
||||||
RequestMethods.LoggingSetLevel,
|
RequestMethods.LoggingSetLevel,
|
||||||
(request, ct) => setLoggingLevelHandler(new(this, request), ct),
|
(request, ct) => setLoggingLevelHandler(new(this, request), ct),
|
||||||
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using ModelContextProtocol.Client;
|
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Protocol.Types;
|
using ModelContextProtocol.Protocol.Types;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ public class McpServerOptions
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information about this server implementation.
|
/// Information about this server implementation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Implementation ServerInfo { get; set; }
|
public Implementation? ServerInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server capabilities to advertise to the server.
|
/// Server capabilities to advertise to the server.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Server;
|
using ModelContextProtocol.Server;
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Shared;
|
namespace ModelContextProtocol.Shared;
|
||||||
|
|
||||||
|
|
@ -19,8 +19,8 @@ namespace ModelContextProtocol.Shared;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class McpEndpoint : IAsyncDisposable
|
internal abstract class McpEndpoint : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly RequestHandlers _requestHandlers = [];
|
/// <summary>Cached naming information used for name/version when none is specified.</summary>
|
||||||
private readonly NotificationHandlers _notificationHandlers = [];
|
internal static AssemblyName DefaultAssemblyName { get; } = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName();
|
||||||
|
|
||||||
private McpSession? _session;
|
private McpSession? _session;
|
||||||
private CancellationTokenSource? _sessionCts;
|
private CancellationTokenSource? _sessionCts;
|
||||||
|
|
@ -39,16 +39,9 @@ internal abstract class McpEndpoint : IAsyncDisposable
|
||||||
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SetRequestHandler<TRequest, TResponse>(
|
protected RequestHandlers RequestHandlers { get; } = [];
|
||||||
string method,
|
|
||||||
Func<TRequest?, CancellationToken, Task<TResponse>> handler,
|
|
||||||
JsonTypeInfo<TRequest> requestTypeInfo,
|
|
||||||
JsonTypeInfo<TResponse> responseTypeInfo)
|
|
||||||
|
|
||||||
=> _requestHandlers.Set(method, handler, requestTypeInfo, responseTypeInfo);
|
protected NotificationHandlers NotificationHandlers { get; } = [];
|
||||||
|
|
||||||
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler)
|
|
||||||
=> _notificationHandlers.Add(method, handler);
|
|
||||||
|
|
||||||
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
|
||||||
=> GetSessionOrThrow().SendRequestAsync(request, cancellationToken);
|
=> GetSessionOrThrow().SendRequestAsync(request, cancellationToken);
|
||||||
|
|
@ -70,7 +63,7 @@ internal abstract class McpEndpoint : IAsyncDisposable
|
||||||
protected void StartSession(ITransport sessionTransport)
|
protected void StartSession(ITransport sessionTransport)
|
||||||
{
|
{
|
||||||
_sessionCts = new CancellationTokenSource();
|
_sessionCts = new CancellationTokenSource();
|
||||||
_session = new McpSession(this is IMcpServer, sessionTransport, EndpointName, _requestHandlers, _notificationHandlers, _logger);
|
_session = new McpSession(this is IMcpServer, sessionTransport, EndpointName, RequestHandlers, NotificationHandlers, _logger);
|
||||||
MessageProcessingTask = _session.ProcessMessagesAsync(_sessionCts.Token);
|
MessageProcessingTask = _session.ProcessMessagesAsync(_sessionCts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ModelContextProtocol.Logging;
|
using ModelContextProtocol.Logging;
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using ModelContextProtocol.Protocol.Transport;
|
using ModelContextProtocol.Protocol.Transport;
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
using ModelContextProtocol.Utils;
|
using ModelContextProtocol.Utils;
|
||||||
using ModelContextProtocol.Utils.Json;
|
using ModelContextProtocol.Utils.Json;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,28 @@
|
||||||
using ModelContextProtocol.Protocol.Messages;
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace ModelContextProtocol.Shared;
|
namespace ModelContextProtocol.Shared;
|
||||||
|
|
||||||
internal sealed class NotificationHandlers : ConcurrentDictionary<string, List<Func<JsonRpcNotification, Task>>>
|
internal sealed class NotificationHandlers : Dictionary<string, List<Func<JsonRpcNotification, Task>>>
|
||||||
{
|
{
|
||||||
|
/// <summary>Adds a notification handler as part of configuring the endpoint.</summary>
|
||||||
|
/// <remarks>This method is not thread-safe and should only be used serially as part of configuring the instance.</remarks>
|
||||||
public void Add(string method, Func<JsonRpcNotification, Task> handler)
|
public void Add(string method, Func<JsonRpcNotification, Task> handler)
|
||||||
{
|
{
|
||||||
var handlers = GetOrAdd(method, _ => []);
|
if (!TryGetValue(method, out var handlers))
|
||||||
lock (handlers)
|
|
||||||
{
|
{
|
||||||
handlers.Add(handler);
|
this[method] = handlers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.Add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Adds notification handlers as part of configuring the endpoint.</summary>
|
||||||
|
/// <remarks>This method is not thread-safe and should only be used serially as part of configuring the instance.</remarks>
|
||||||
|
public void AddRange(IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, Task>>> handlers)
|
||||||
|
{
|
||||||
|
foreach (var handler in handlers)
|
||||||
|
{
|
||||||
|
Add(handler.Key, handler.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue