diff --git a/README.md b/README.md index 163d57f8a..cb3b28f10 100644 --- a/README.md +++ b/README.md @@ -176,52 +176,51 @@ McpServerOptions options = new() ServerInfo = new Implementation { Name = "MyServer", Version = "1.0.0" }, Capabilities = new ServerCapabilities { - Tools = new ToolsCapability - { - ListToolsHandler = (request, cancellationToken) => - ValueTask.FromResult(new ListToolsResult - { - Tools = - [ - new Tool - { - Name = "echo", - Description = "Echoes the input back to the client.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "The input to echo back" - } - }, - "required": ["message"] - } - """), - } - ] - }), - - CallToolHandler = (request, cancellationToken) => + Tools = new ToolsCapability(), + }, +}; + +options.Handlers.ListToolsHandler = (request, cancellationToken) => + ValueTask.FromResult(new ListToolsResult + { + Tools = + [ + new Tool { - if (request.Params?.Name == "echo") - { - if (request.Params.Arguments?.TryGetValue("message", out var message) is not true) + Name = "echo", + Description = "Echoes the input back to the client.", + InputSchema = JsonSerializer.Deserialize(""" { - throw new McpException("Missing required argument 'message'"); + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The input to echo back" + } + }, + "required": ["message"] } + """), + } + ] + }); - return ValueTask.FromResult(new CallToolResult - { - Content = [new TextContentBlock { Text = $"Echo: {message}", Type = "text" }] - }); - } - - throw new McpException($"Unknown tool: '{request.Params?.Name}'"); - }, +options.Handlers.CallToolHandler = (request, cancellationToken) => +{ + if (request.Params?.Name == "echo") + { + if (request.Params.Arguments?.TryGetValue("message", out var message) is not true) + { + throw new McpException("Missing required argument 'message'"); } - }, + + return ValueTask.FromResult(new CallToolResult + { + Content = [new TextContentBlock { Text = $"Echo: {message}", Type = "text" }] + }); + } + + throw new McpException($"Unknown tool: '{request.Params?.Name}'"); }; await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("MyServer"), options); diff --git a/docs/concepts/elicitation/samples/client/Program.cs b/docs/concepts/elicitation/samples/client/Program.cs index 6d5178796..08bf66333 100644 --- a/docs/concepts/elicitation/samples/client/Program.cs +++ b/docs/concepts/elicitation/samples/client/Program.cs @@ -17,15 +17,9 @@ { Name = "ElicitationClient", Version = "1.0.0" - }, - Capabilities = new() - { - Elicitation = new() - { - ElicitationHandler = HandleElicitationAsync - } } }; +options.Handlers.ElicitationHandler = HandleElicitationAsync; await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options); // diff --git a/samples/AspNetCoreMcpPerSessionTools/Program.cs b/samples/AspNetCoreMcpPerSessionTools/Program.cs index 3a52b93ed..3484978ec 100644 --- a/samples/AspNetCoreMcpPerSessionTools/Program.cs +++ b/samples/AspNetCoreMcpPerSessionTools/Program.cs @@ -24,7 +24,7 @@ { mcpOptions.Capabilities = new(); mcpOptions.Capabilities.Tools = new(); - var toolCollection = mcpOptions.Capabilities.Tools.ToolCollection = new(); + var toolCollection = mcpOptions.ToolCollection = new(); foreach (var tool in tools) { diff --git a/samples/ChatWithTools/Program.cs b/samples/ChatWithTools/Program.cs index ba597ae8a..30cdf0534 100644 --- a/samples/ChatWithTools/Program.cs +++ b/samples/ChatWithTools/Program.cs @@ -32,6 +32,9 @@ .UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true) .Build(); +var clientOptions = new McpClientOptions(); +clientOptions.Handlers.SamplingHandler = samplingClient.CreateSamplingHandler(); + var mcpClient = await McpClientFactory.CreateAsync( new StdioClientTransport(new() { @@ -39,10 +42,7 @@ Arguments = ["-y", "--verbose", "@modelcontextprotocol/server-everything"], Name = "Everything", }), - clientOptions: new() - { - Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } }, - }, + clientOptions: clientOptions, loggerFactory: loggerFactory); // Get all available tools diff --git a/samples/InMemoryTransport/Program.cs b/samples/InMemoryTransport/Program.cs index 67e2d320c..81f64afcc 100644 --- a/samples/InMemoryTransport/Program.cs +++ b/samples/InMemoryTransport/Program.cs @@ -10,13 +10,7 @@ new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()), new McpServerOptions() { - Capabilities = new() - { - Tools = new() - { - ToolCollection = [McpServerTool.Create((string arg) => $"Echo: {arg}", new() { Name = "Echo" })] - } - } + ToolCollection = [McpServerTool.Create((string arg) => $"Echo: {arg}", new() { Name = "Echo" })] }); _ = server.RunAsync(); diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index dd8c7fe09..5a25dd5f6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -39,57 +39,48 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL EndpointName = clientTransport.Name; - if (options.Capabilities is { } capabilities) + if (options.Handlers.NotificationHandlers is { } notificationHandlers) { - if (capabilities.NotificationHandlers is { } notificationHandlers) - { - NotificationHandlers.RegisterRange(notificationHandlers); - } - - if (capabilities.Sampling is { } samplingCapability) - { - 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?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, - cancellationToken), - McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, - McpJsonUtilities.JsonContext.Default.CreateMessageResult); - } - - if (capabilities.Roots is { } rootsCapability) - { - if (rootsCapability.RootsHandler is not { } rootsHandler) - { - throw new InvalidOperationException("Roots capability was set but it did not provide a handler."); - } + NotificationHandlers.RegisterRange(notificationHandlers); + } - RequestHandlers.Set( - RequestMethods.RootsList, - (request, _, cancellationToken) => rootsHandler(request, cancellationToken), - McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, - McpJsonUtilities.JsonContext.Default.ListRootsResult); - } + if (options.Handlers.SamplingHandler is { } samplingHandler) + { + RequestHandlers.Set( + RequestMethods.SamplingCreateMessage, + (request, _, cancellationToken) => samplingHandler( + request, + request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, + cancellationToken), + McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, + McpJsonUtilities.JsonContext.Default.CreateMessageResult); + + _options.Capabilities ??= new(); + _options.Capabilities.Sampling ??= new(); + } - if (capabilities.Elicitation is { } elicitationCapability) - { - if (elicitationCapability.ElicitationHandler is not { } elicitationHandler) - { - throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler."); - } + if (options.Handlers.RootsHandler is { } rootsHandler) + { + RequestHandlers.Set( + RequestMethods.RootsList, + (request, _, cancellationToken) => rootsHandler(request, cancellationToken), + McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, + McpJsonUtilities.JsonContext.Default.ListRootsResult); + + _options.Capabilities ??= new(); + _options.Capabilities.Roots ??= new(); + } - RequestHandlers.Set( - RequestMethods.ElicitationCreate, - (request, _, cancellationToken) => elicitationHandler(request, cancellationToken), - McpJsonUtilities.JsonContext.Default.ElicitRequestParams, - McpJsonUtilities.JsonContext.Default.ElicitResult); - } + if (options.Handlers.ElicitationHandler is { } elicitationHandler) + { + RequestHandlers.Set( + RequestMethods.ElicitationCreate, + (request, _, cancellationToken) => elicitationHandler(request, cancellationToken), + McpJsonUtilities.JsonContext.Default.ElicitRequestParams, + McpJsonUtilities.JsonContext.Default.ElicitResult); + + _options.Capabilities ??= new(); + _options.Capabilities.Elicitation ??= new(); } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 60a9c3a64..3f89bc04a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -965,11 +965,11 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat } /// - /// Creates a sampling handler for use with that will + /// Creates a sampling handler for use with that will /// satisfy sampling requests using the specified . /// /// The with which to satisfy sampling requests. - /// The created handler delegate that can be assigned to . + /// The created handler delegate that can be assigned to . /// /// /// This method creates a function that converts MCP message requests into chat client calls, enabling diff --git a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs new file mode 100644 index 000000000..b03d20746 --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.AI; +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Client; + +/// +/// Provides a container for handlers used in the creation of an MCP client. +/// +/// +/// +/// This class provides a centralized collection of delegates that implement various capabilities of the Model Context Protocol. +/// +/// +/// Each handler in this class corresponds to a specific client endpoint in the Model Context Protocol and +/// is responsible for processing a particular type of message. The handlers are used to customize +/// the behavior of the MCP server by providing implementations for the various protocol operations. +/// +/// +/// When a server sends a message to the client, the appropriate handler is invoked to process it +/// according to the protocol specification. Which handler is selected +/// is done based on an ordinal, case-sensitive string comparison. +/// +/// +public class McpClientHandlers +{ + + /// Gets or sets notification handlers to register with the client. + /// + /// + /// When constructed, the client will enumerate these handlers once, which may contain multiple handlers per notification method key. + /// The client will not re-enumerate the sequence after initialization. + /// + /// + /// Notification handlers allow the client to respond to server-sent notifications for specific methods. + /// Each key in the collection is a notification method name, and each value is a callback that will be invoked + /// when a notification with that method is received. + /// + /// + /// Handlers provided via will be registered with the client for the lifetime of the client. + /// For transient handlers, may be used to register a handler that can + /// then be unregistered by disposing of the returned from the method. + /// + /// + public IEnumerable>>? NotificationHandlers { get; set; } + + /// + /// Gets or sets the handler for requests. + /// + /// + /// This handler is invoked when a client sends a request to retrieve available roots. + /// The handler receives request parameters and should return a containing the collection of available roots. + /// + public Func>? RootsHandler { get; set; } + + /// + /// Gets or sets the handler for processing requests. + /// + /// + /// + /// This handler function is called when an MCP server requests the client to provide additional + /// information during interactions. The client must set this property for the elicitation capability to work. + /// + /// + /// The handler receives message parameters and a cancellation token. + /// It should return a containing the response to the elicitation request. + /// + /// + public Func>? ElicitationHandler { get; set; } + + /// + /// Gets or sets the handler for processing requests. + /// + /// + /// + /// This handler function is called when an MCP server requests the client to generate content + /// using an AI model. The client must set this property for the sampling capability to work. + /// + /// + /// The handler receives message parameters, a progress reporter for updates, and a + /// cancellation token. It should return a containing the + /// generated content. + /// + /// + /// You can create a handler using the extension + /// method with any implementation of . + /// + /// + public Func, CancellationToken, ValueTask>? SamplingHandler { get; set; } +} diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs index 76099d0d9..3e6ea2b76 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs @@ -63,4 +63,9 @@ public sealed class McpClientOptions /// The default value is 60 seconds. /// public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// Gets or sets the container of handlers used by the client for processing protocol messages. + /// + public McpClientHandlers Handlers { get; } = new(); } diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index ebe698135..41da12d01 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -45,7 +45,7 @@ public sealed class ClientCapabilities /// /// /// The server can use to request the list of - /// available roots from the client, which will trigger the client's . + /// available roots from the client, which will trigger the client's . /// /// [JsonPropertyName("roots")] @@ -64,24 +64,4 @@ public sealed class ClientCapabilities /// [JsonPropertyName("elicitation")] public ElicitationCapability? Elicitation { get; set; } - - /// Gets or sets notification handlers to register with the client. - /// - /// - /// When constructed, the client will enumerate these handlers once, which may contain multiple handlers per notification method key. - /// The client will not re-enumerate the sequence after initialization. - /// - /// - /// Notification handlers allow the client to respond to server-sent notifications for specific methods. - /// Each key in the collection is a notification method name, and each value is a callback that will be invoked - /// when a notification with that method is received. - /// - /// - /// Handlers provided via will be registered with the client for the lifetime of the client. - /// For transient handlers, may be used to register a handler that can - /// then be unregistered by disposing of the returned from the method. - /// - /// - [JsonIgnore] - public IEnumerable>>? NotificationHandlers { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs b/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs index 86363351a..e8d5932c9 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs @@ -1,6 +1,3 @@ -using ModelContextProtocol.Server; -using System.Text.Json.Serialization; - namespace ModelContextProtocol.Protocol; /// @@ -23,15 +20,4 @@ namespace ModelContextProtocol.Protocol; public sealed class CompletionsCapability { // Currently empty in the spec, but may be extended in the future. - - /// - /// Gets or sets the handler for completion requests. - /// - /// - /// This handler provides auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol. - /// The handler receives a reference type (e.g., "ref/prompt" or "ref/resource") and the current argument value, - /// and should return appropriate completion suggestions. - /// - [JsonIgnore] - public McpRequestHandler? CompleteHandler { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs b/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs index d88247d2d..39d76ad05 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace ModelContextProtocol.Protocol; /// @@ -11,26 +9,10 @@ namespace ModelContextProtocol.Protocol; /// /// /// When this capability is enabled, an MCP server can request the client to provide additional information -/// during interactions. The client must set a to process these requests. +/// during interactions. The client must set a to process these requests. /// /// public sealed class ElicitationCapability { // Currently empty in the spec, but may be extended in the future. - - /// - /// Gets or sets the handler for processing requests. - /// - /// - /// - /// This handler function is called when an MCP server requests the client to provide additional - /// information during interactions. The client must set this property for the elicitation capability to work. - /// - /// - /// The handler receives message parameters and a cancellation token. - /// It should return a containing the response to the elicitation request. - /// - /// - [JsonIgnore] - public Func>? ElicitationHandler { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs b/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs index 07803c1ac..3cc771b0c 100644 --- a/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs @@ -1,6 +1,3 @@ -using ModelContextProtocol.Server; -using System.Text.Json.Serialization; - namespace ModelContextProtocol.Protocol; /// @@ -13,10 +10,4 @@ namespace ModelContextProtocol.Protocol; public sealed class LoggingCapability { // Currently empty in the spec, but may be extended in the future - - /// - /// Gets or sets the handler for set logging level requests from clients. - /// - [JsonIgnore] - public McpRequestHandler? SetLoggingLevelHandler { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs b/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs index fdfa3d43c..50e5a6f56 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Protocol; /// /// /// The prompts capability allows a server to expose a collection of predefined prompt templates that clients -/// can discover and use. These prompts can be static (defined in the ) or +/// can discover and use. These prompts can be static (defined in the ) or /// dynamically generated through handlers. /// /// @@ -30,53 +30,4 @@ public sealed class PromptsCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// This handler is invoked when a client requests a list of available prompts from the server - /// via a request. Results from this handler are returned - /// along with any prompts defined in . - /// - [JsonIgnore] - public McpRequestHandler? ListPromptsHandler { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// - /// This handler is invoked when a client requests details for a specific prompt by name and provides arguments - /// for the prompt if needed. The handler receives the request context containing the prompt name and any arguments, - /// and should return a with the prompt messages and other details. - /// - /// - /// This handler will be invoked if the requested prompt name is not found in the , - /// allowing for dynamic prompt generation or retrieval from external sources. - /// - /// - [JsonIgnore] - public McpRequestHandler? GetPromptHandler { get; set; } - - /// - /// Gets or sets a collection of prompts that will be served by the server. - /// - /// - /// - /// The contains the predefined prompts that clients can request from the server. - /// This collection works in conjunction with and - /// when those are provided: - /// - /// - /// - For requests: The server returns all prompts from this collection - /// plus any additional prompts provided by the if it's set. - /// - /// - /// - For requests: The server first checks this collection for the requested prompt. - /// If not found, it will invoke the as a fallback if one is set. - /// - /// - [JsonIgnore] - public McpServerPrimitiveCollection? PromptCollection { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs b/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs index b5336b207..cf1648de2 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs @@ -28,80 +28,4 @@ public sealed class ResourcesCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// This handler is called when clients request available resource templates that can be used - /// to create resources within the Model Context Protocol server. - /// Resource templates define the structure and URI patterns for resources accessible in the system, - /// allowing clients to discover available resource types and their access patterns. - /// - [JsonIgnore] - public McpRequestHandler? ListResourceTemplatesHandler { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// This handler responds to client requests for available resources and returns information about resources accessible through the server. - /// The implementation should return a with the matching resources. - /// - [JsonIgnore] - public McpRequestHandler? ListResourcesHandler { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// This handler is responsible for retrieving the content of a specific resource identified by its URI in the Model Context Protocol. - /// When a client sends a resources/read request, this handler is invoked with the resource URI. - /// The handler should implement logic to locate and retrieve the requested resource, then return - /// its contents in a ReadResourceResult object. - /// - [JsonIgnore] - public McpRequestHandler? ReadResourceHandler { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// When a client sends a request, this handler is invoked with the resource URI - /// to be subscribed to. The implementation should register the client's interest in receiving updates - /// for the specified resource. - /// Subscriptions allow clients to receive real-time notifications when resources change, without - /// requiring polling. - /// - [JsonIgnore] - public McpRequestHandler? SubscribeToResourcesHandler { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// When a client sends a request, this handler is invoked with the resource URI - /// to be unsubscribed from. The implementation should remove the client's registration for receiving updates - /// about the specified resource. - /// - [JsonIgnore] - public McpRequestHandler? UnsubscribeFromResourcesHandler { get; set; } - - /// - /// Gets or sets a collection of resources served by the server. - /// - /// - /// - /// Resources specified via augment the , - /// and handlers, if provided. Resources with template expressions in their URI templates are considered resource templates - /// and are listed via ListResourceTemplate, whereas resources without template parameters are considered static resources and are listed with ListResources. - /// - /// - /// ReadResource requests will first check the for the exact resource being requested. If no match is found, they'll proceed to - /// try to match the resource against each resource template in . If no match is still found, the request will fall back to - /// any handler registered for . - /// - /// - [JsonIgnore] - public McpServerResourceCollection? ResourceCollection { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs b/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs index 60d20b94f..9f0b0b812 100644 --- a/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs @@ -31,14 +31,4 @@ public sealed class RootsCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// This handler is invoked when a client sends a request to retrieve available roots. - /// The handler receives request parameters and should return a containing the collection of available roots. - /// - [JsonIgnore] - public Func>? RootsHandler { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs index 6e0f1190a..ad82c7957 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs @@ -1,7 +1,3 @@ -using Microsoft.Extensions.AI; -using ModelContextProtocol.Client; -using System.Text.Json.Serialization; - namespace ModelContextProtocol.Protocol; /// @@ -13,31 +9,10 @@ namespace ModelContextProtocol.Protocol; /// /// /// When this capability is enabled, an MCP server can request the client to generate content -/// using an AI model. The client must set a to process these requests. +/// using an AI model. The client must set a to process these requests. /// /// public sealed class SamplingCapability { // Currently empty in the spec, but may be extended in the future - - /// - /// Gets or sets the handler for processing requests. - /// - /// - /// - /// This handler function is called when an MCP server requests the client to generate content - /// using an AI model. The client must set this property for the sampling capability to work. - /// - /// - /// The handler receives message parameters, a progress reporter for updates, and a - /// cancellation token. It should return a containing the - /// generated content. - /// - /// - /// You can create a handler using the extension - /// method with any implementation of . - /// - /// - [JsonIgnore] - public Func, CancellationToken, ValueTask>? SamplingHandler { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index 6a4b2e62a..ed2f1f2af 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -63,24 +63,4 @@ public sealed class ServerCapabilities /// [JsonPropertyName("completions")] public CompletionsCapability? Completions { get; set; } - - /// Gets or sets notification handlers to register with the server. - /// - /// - /// When constructed, the server will enumerate these handlers once, which may contain multiple handlers per notification method key. - /// The server will not re-enumerate the sequence after initialization. - /// - /// - /// Notification handlers allow the server to respond to client-sent notifications for specific methods. - /// Each key in the collection is a notification method name, and each value is a callback that will be invoked - /// when a notification with that method is received. - /// - /// - /// Handlers provided via will be registered with the server for the lifetime of the server. - /// For transient handlers, may be used to register a handler that can - /// then be unregistered by disposing of the returned from the method. - /// - /// - [JsonIgnore] - public IEnumerable>>? NotificationHandlers { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs index 0ea955314..dbbb9462f 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs @@ -1,4 +1,3 @@ -using ModelContextProtocol.Server; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -21,43 +20,4 @@ public sealed class ToolsCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// The handler should return a list of available tools when requested by a client. - /// It supports pagination through the cursor mechanism, where the client can make - /// repeated calls with the cursor returned by the previous call to retrieve more tools. - /// When used in conjunction with , both the tools from this handler - /// and the tools from the collection will be combined to form the complete list of available tools. - /// - [JsonIgnore] - public McpRequestHandler? ListToolsHandler { get; set; } - - /// - /// Gets or sets the handler for requests. - /// - /// - /// This handler is invoked when a client makes a call to a tool that isn't found in the . - /// The handler should implement logic to execute the requested tool and return appropriate results. - /// It receives a containing information about the tool - /// being called and its arguments, and should return a with the execution results. - /// - [JsonIgnore] - public McpRequestHandler? CallToolHandler { get; set; } - - /// - /// Gets or sets a collection of tools served by the server. - /// - /// - /// Tools will specified via augment the and - /// , if provided. ListTools requests will output information about every tool - /// in and then also any tools output by , if it's - /// non-. CallTool requests will first check for the tool - /// being requested, and if the tool is not found in the , any specified - /// will be invoked as a fallback. - /// - [JsonIgnore] - public McpServerPrimitiveCollection? ToolCollection { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index 6e15e2465..c71fae3c3 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -68,7 +68,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? ConfigurePing(); // Register any notification handlers that were provided. - if (options.Capabilities?.NotificationHandlers is { } notificationHandlers) + if (options.Handlers.NotificationHandlers is { } notificationHandlers) { NotificationHandlers.RegisterRange(notificationHandlers); } @@ -76,9 +76,9 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? // Now that everything has been configured, subscribe to any necessary notifications. if (transport is not StreamableHttpServerTransport streamableHttpTransport || streamableHttpTransport.Stateless is false) { - Register(ServerOptions.Capabilities?.Tools?.ToolCollection, NotificationMethods.ToolListChangedNotification); - Register(ServerOptions.Capabilities?.Prompts?.PromptCollection, NotificationMethods.PromptListChangedNotification); - Register(ServerOptions.Capabilities?.Resources?.ResourceCollection, NotificationMethods.ResourceListChangedNotification); + Register(ServerOptions.ToolCollection, NotificationMethods.ToolListChangedNotification); + Register(ServerOptions.PromptCollection, NotificationMethods.PromptListChangedNotification); + Register(ServerOptions.ResourceCollection, NotificationMethods.ResourceListChangedNotification); void Register(McpServerPrimitiveCollection? collection, string notificationMethod) where TPrimitive : IMcpServerPrimitive @@ -190,22 +190,21 @@ private void ConfigureInitialize(McpServerOptions options) private void ConfigureCompletion(McpServerOptions options) { - if (options.Capabilities?.Completions is not { } completionsCapability) + var completeHandler = options.Handlers.CompleteHandler; + + if (completeHandler is null && options.Capabilities?.Completions is null) { return; } - var completeHandler = completionsCapability.CompleteHandler ?? (static async (_, __) => new CompleteResult()); + completeHandler ??= (static async (_, __) => new CompleteResult()); completeHandler = BuildFilterPipeline(completeHandler, options.Filters.CompleteFilters); - ServerCapabilities.Completions = new() - { - CompleteHandler = completeHandler - }; + ServerCapabilities.Completions = new(); SetHandler( RequestMethods.CompletionComplete, - ServerCapabilities.Completions.CompleteHandler, + completeHandler, McpJsonUtilities.JsonContext.Default.CompleteRequestParams, McpJsonUtilities.JsonContext.Default.CompleteResult); } @@ -217,21 +216,30 @@ private void ConfigureExperimental(McpServerOptions options) private void ConfigureResources(McpServerOptions options) { - if (options.Capabilities?.Resources is not { } resourcesCapability) + var listResourcesHandler = options.Handlers.ListResourcesHandler; + var listResourceTemplatesHandler = options.Handlers.ListResourceTemplatesHandler; + var readResourceHandler = options.Handlers.ReadResourceHandler; + var subscribeHandler = options.Handlers.SubscribeToResourcesHandler; + var unsubscribeHandler = options.Handlers.UnsubscribeFromResourcesHandler; + var resources = options.ResourceCollection; + var resourcesCapability = options.Capabilities?.Resources; + + if (listResourcesHandler is null && listResourceTemplatesHandler is null && readResourceHandler is null && + subscribeHandler is null && unsubscribeHandler is null && resources is null && + resourcesCapability is null) { return; } ServerCapabilities.Resources = new(); - var listResourcesHandler = resourcesCapability.ListResourcesHandler ?? (static async (_, __) => new ListResourcesResult()); - var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler ?? (static async (_, __) => new ListResourceTemplatesResult()); - var readResourceHandler = resourcesCapability.ReadResourceHandler ?? (static async (request, _) => throw new McpException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.InvalidParams)); - var subscribeHandler = resourcesCapability.SubscribeToResourcesHandler ?? (static async (_, __) => new EmptyResult()); - var unsubscribeHandler = resourcesCapability.UnsubscribeFromResourcesHandler ?? (static async (_, __) => new EmptyResult()); - var resources = resourcesCapability.ResourceCollection; - var listChanged = resourcesCapability.ListChanged; - var subscribe = resourcesCapability.Subscribe; + listResourcesHandler ??= (static async (_, __) => new ListResourcesResult()); + listResourceTemplatesHandler ??= (static async (_, __) => new ListResourceTemplatesResult()); + readResourceHandler ??= (static async (request, _) => throw new McpException($"Unknown resource URI: '{request.Params?.Uri}'", McpErrorCode.InvalidParams)); + subscribeHandler ??= (static async (_, __) => new EmptyResult()); + unsubscribeHandler ??= (static async (_, __) => new EmptyResult()); + var listChanged = resourcesCapability?.ListChanged; + var subscribe = resourcesCapability?.Subscribe; // Handle resources provided via DI. if (resources is { IsEmpty: false }) @@ -336,12 +344,6 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure subscribeHandler = BuildFilterPipeline(subscribeHandler, options.Filters.SubscribeToResourcesFilters); unsubscribeHandler = BuildFilterPipeline(unsubscribeHandler, options.Filters.UnsubscribeFromResourcesFilters); - ServerCapabilities.Resources.ListResourcesHandler = listResourcesHandler; - ServerCapabilities.Resources.ListResourceTemplatesHandler = listResourceTemplatesHandler; - ServerCapabilities.Resources.ReadResourceHandler = readResourceHandler; - ServerCapabilities.Resources.ResourceCollection = resources; - ServerCapabilities.Resources.SubscribeToResourcesHandler = subscribeHandler; - ServerCapabilities.Resources.UnsubscribeFromResourcesHandler = unsubscribeHandler; ServerCapabilities.Resources.ListChanged = listChanged; ServerCapabilities.Resources.Subscribe = subscribe; @@ -378,17 +380,22 @@ await originalListResourceTemplatesHandler(request, cancellationToken).Configure private void ConfigurePrompts(McpServerOptions options) { - if (options.Capabilities?.Prompts is not { } promptsCapability) + var listPromptsHandler = options.Handlers.ListPromptsHandler; + var getPromptHandler = options.Handlers.GetPromptHandler; + var prompts = options.PromptCollection; + var promptsCapability = options.Capabilities?.Prompts; + + if (listPromptsHandler is null && getPromptHandler is null && prompts is null && + promptsCapability is null) { return; } ServerCapabilities.Prompts = new(); - var listPromptsHandler = promptsCapability.ListPromptsHandler ?? (static async (_, __) => new ListPromptsResult()); - var getPromptHandler = promptsCapability.GetPromptHandler ?? (static async (request, _) => throw new McpException($"Unknown prompt: '{request.Params?.Name}'", McpErrorCode.InvalidParams)); - var prompts = promptsCapability.PromptCollection; - var listChanged = promptsCapability.ListChanged; + listPromptsHandler ??= (static async (_, __) => new ListPromptsResult()); + getPromptHandler ??= (static async (request, _) => throw new McpException($"Unknown prompt: '{request.Params?.Name}'", McpErrorCode.InvalidParams)); + var listChanged = promptsCapability?.ListChanged; // Handle tools provided via DI by augmenting the handlers to incorporate them. if (prompts is { IsEmpty: false }) @@ -439,9 +446,6 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals return handler(request, cancellationToken); }); - ServerCapabilities.Prompts.ListPromptsHandler = listPromptsHandler; - ServerCapabilities.Prompts.GetPromptHandler = getPromptHandler; - ServerCapabilities.Prompts.PromptCollection = prompts; ServerCapabilities.Prompts.ListChanged = listChanged; SetHandler( @@ -459,17 +463,22 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals private void ConfigureTools(McpServerOptions options) { - if (options.Capabilities?.Tools is not { } toolsCapability) + var listToolsHandler = options.Handlers.ListToolsHandler; + var callToolHandler = options.Handlers.CallToolHandler; + var tools = options.ToolCollection; + var toolsCapability = options.Capabilities?.Tools; + + if (listToolsHandler is null && callToolHandler is null && tools is null && + toolsCapability is null) { return; } ServerCapabilities.Tools = new(); - var listToolsHandler = toolsCapability.ListToolsHandler ?? (static async (_, __) => new ListToolsResult()); - var callToolHandler = toolsCapability.CallToolHandler ?? (static async (request, _) => throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams)); - var tools = toolsCapability.ToolCollection; - var listChanged = toolsCapability.ListChanged; + listToolsHandler ??= (static async (_, __) => new ListToolsResult()); + callToolHandler ??= (static async (request, _) => throw new McpException($"Unknown tool: '{request.Params?.Name}'", McpErrorCode.InvalidParams)); + var listChanged = toolsCapability?.ListChanged; // Handle tools provided via DI by augmenting the handlers to incorporate them. if (tools is { IsEmpty: false }) @@ -551,9 +560,6 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) } }); - ServerCapabilities.Tools.ListToolsHandler = listToolsHandler; - ServerCapabilities.Tools.CallToolHandler = callToolHandler; - ServerCapabilities.Tools.ToolCollection = tools; ServerCapabilities.Tools.ListChanged = listChanged; SetHandler( @@ -572,7 +578,7 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false) private void ConfigureLogging(McpServerOptions options) { // We don't require that the handler be provided, as we always store the provided log level to the server. - var setLoggingLevelHandler = options.Capabilities?.Logging?.SetLoggingLevelHandler; + var setLoggingLevelHandler = options.Handlers.SetLoggingLevelHandler; // Apply filters to the handler if (setLoggingLevelHandler is not null) @@ -581,7 +587,6 @@ private void ConfigureLogging(McpServerOptions options) } ServerCapabilities.Logging = new(); - ServerCapabilities.Logging.SetLoggingLevelHandler = setLoggingLevelHandler; RequestHandlers.Set( RequestMethods.LoggingSetLevel, diff --git a/src/ModelContextProtocol/McpServerHandlers.cs b/src/ModelContextProtocol.Core/Server/McpServerHandlers.cs similarity index 68% rename from src/ModelContextProtocol/McpServerHandlers.cs rename to src/ModelContextProtocol.Core/Server/McpServerHandlers.cs index 34504e928..0d8deba13 100644 --- a/src/ModelContextProtocol/McpServerHandlers.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerHandlers.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol; namespace ModelContextProtocol.Server; @@ -10,17 +9,12 @@ namespace ModelContextProtocol.Server; /// /// This class provides a centralized collection of delegates that implement various capabilities of the Model Context Protocol. /// Each handler in this class corresponds to a specific endpoint in the Model Context Protocol and -/// is responsible for processing a particular type of request. The handlers are used to customize +/// is responsible for processing a particular type of message. The handlers are used to customize /// the behavior of the MCP server by providing implementations for the various protocol operations. /// /// -/// Handlers can be configured individually using the extension methods in -/// such as and -/// . -/// -/// -/// When a client sends a request to the server, the appropriate handler is invoked to process the -/// request and produce a response according to the protocol specification. Which handler is selected +/// When a client sends a message to the server, the appropriate handler is invoked to process it +/// according to the protocol specification. Which handler is selected /// is done based on an ordinal, case-sensitive string comparison. /// /// @@ -162,65 +156,22 @@ public sealed class McpServerHandlers /// public McpRequestHandler? SetLoggingLevelHandler { get; set; } - /// - /// Overwrite any handlers in McpServerOptions with non-null handlers from this instance. - /// - /// - /// - internal void OverwriteWithSetHandlers(McpServerOptions options) - { - PromptsCapability? promptsCapability = options.Capabilities?.Prompts; - if (ListPromptsHandler is not null || GetPromptHandler is not null) - { - promptsCapability ??= new(); - promptsCapability.ListPromptsHandler = ListPromptsHandler ?? promptsCapability.ListPromptsHandler; - promptsCapability.GetPromptHandler = GetPromptHandler ?? promptsCapability.GetPromptHandler; - } - - ResourcesCapability? resourcesCapability = options.Capabilities?.Resources; - if (ListResourcesHandler is not null || - ReadResourceHandler is not null) - { - resourcesCapability ??= new(); - resourcesCapability.ListResourceTemplatesHandler = ListResourceTemplatesHandler ?? resourcesCapability.ListResourceTemplatesHandler; - resourcesCapability.ListResourcesHandler = ListResourcesHandler ?? resourcesCapability.ListResourcesHandler; - resourcesCapability.ReadResourceHandler = ReadResourceHandler ?? resourcesCapability.ReadResourceHandler; - - if (SubscribeToResourcesHandler is not null || UnsubscribeFromResourcesHandler is not null) - { - resourcesCapability.SubscribeToResourcesHandler = SubscribeToResourcesHandler ?? resourcesCapability.SubscribeToResourcesHandler; - resourcesCapability.UnsubscribeFromResourcesHandler = UnsubscribeFromResourcesHandler ?? resourcesCapability.UnsubscribeFromResourcesHandler; - resourcesCapability.Subscribe = true; - } - } - - ToolsCapability? toolsCapability = options.Capabilities?.Tools; - if (ListToolsHandler is not null || CallToolHandler is not null) - { - toolsCapability ??= new(); - toolsCapability.ListToolsHandler = ListToolsHandler ?? toolsCapability.ListToolsHandler; - toolsCapability.CallToolHandler = CallToolHandler ?? toolsCapability.CallToolHandler; - } - - LoggingCapability? loggingCapability = options.Capabilities?.Logging; - if (SetLoggingLevelHandler is not null) - { - loggingCapability ??= new(); - loggingCapability.SetLoggingLevelHandler = SetLoggingLevelHandler; - } - - CompletionsCapability? completionsCapability = options.Capabilities?.Completions; - if (CompleteHandler is not null) - { - completionsCapability ??= new(); - completionsCapability.CompleteHandler = CompleteHandler; - } - - options.Capabilities ??= new(); - options.Capabilities.Prompts = promptsCapability; - options.Capabilities.Resources = resourcesCapability; - options.Capabilities.Tools = toolsCapability; - options.Capabilities.Logging = loggingCapability; - options.Capabilities.Completions = completionsCapability; - } + /// Gets or sets notification handlers to register with the server. + /// + /// + /// When constructed, the server will enumerate these handlers once, which may contain multiple handlers per notification method key. + /// The server will not re-enumerate the sequence after initialization. + /// + /// + /// Notification handlers allow the server to respond to client-sent notifications for specific methods. + /// Each key in the collection is a notification method name, and each value is a callback that will be invoked + /// when a notification with that method is received. + /// + /// + /// Handlers provided via will be registered with the server for the lifetime of the server. + /// For transient handlers, may be used to register a handler that can + /// then be unregistered by disposing of the returned from the method. + /// + /// + public IEnumerable>>? NotificationHandlers { get; set; } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs index 1c981b77f..c02fc408e 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerOptions.cs @@ -89,4 +89,59 @@ public sealed class McpServerOptions /// added will be the outermost (first to execute). /// public McpServerFilters Filters { get; } = new(); + + /// + /// Gets or sets the container of handlers used by the server for processing protocol messages. + /// + public McpServerHandlers Handlers { get; } = new(); + + /// + /// Gets or sets a collection of tools served by the server. + /// + /// + /// Tools specified via augment the and + /// , if provided. ListTools requests will output information about every tool + /// in and then also any tools output by , if it's + /// non-. CallTool requests will first check for the tool + /// being requested, and if the tool is not found in the , any specified + /// will be invoked as a fallback. + /// + public McpServerPrimitiveCollection? ToolCollection { get; set; } + + /// + /// Gets or sets a collection of resources served by the server. + /// + /// + /// + /// Resources specified via augment the , + /// and handlers, if provided. Resources with template expressions in their URI templates are considered resource templates + /// and are listed via ListResourceTemplate, whereas resources without template parameters are considered static resources and are listed with ListResources. + /// + /// + /// ReadResource requests will first check the for the exact resource being requested. If no match is found, they'll proceed to + /// try to match the resource against each resource template in . If no match is still found, the request will fall back to + /// any handler registered for . + /// + /// + public McpServerResourceCollection? ResourceCollection { get; set; } + + /// + /// Gets or sets a collection of prompts that will be served by the server. + /// + /// + /// + /// The contains the predefined prompts that clients can request from the server. + /// This collection works in conjunction with and + /// when those are provided: + /// + /// + /// - For requests: The server returns all prompts from this collection + /// plus any additional prompts provided by the if it's set. + /// + /// + /// - For requests: The server first checks this collection for the requested prompt. + /// If not found, it will invoke the as a fallback if one is set. + /// + /// + public McpServerPrimitiveCollection? PromptCollection { get; set; } } diff --git a/src/ModelContextProtocol/McpServerOptionsSetup.cs b/src/ModelContextProtocol/McpServerOptionsSetup.cs index 7fe4f61cb..030c3012a 100644 --- a/src/ModelContextProtocol/McpServerOptionsSetup.cs +++ b/src/ModelContextProtocol/McpServerOptionsSetup.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; namespace ModelContextProtocol; @@ -29,7 +30,7 @@ public void Configure(McpServerOptions options) // 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 // change notifications, etc. - McpServerPrimitiveCollection toolCollection = options.Capabilities?.Tools?.ToolCollection ?? []; + McpServerPrimitiveCollection toolCollection = options.ToolCollection ?? []; foreach (var tool in serverTools) { toolCollection.TryAdd(tool); @@ -37,16 +38,14 @@ public void Configure(McpServerOptions options) if (!toolCollection.IsEmpty) { - options.Capabilities ??= new(); - options.Capabilities.Tools ??= new(); - options.Capabilities.Tools.ToolCollection = toolCollection; + options.ToolCollection = toolCollection; } // Collect all of the provided prompts into a prompts collection. If the options already has // 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 // change notifications, etc. - McpServerPrimitiveCollection promptCollection = options.Capabilities?.Prompts?.PromptCollection ?? []; + McpServerPrimitiveCollection promptCollection = options.PromptCollection ?? []; foreach (var prompt in serverPrompts) { promptCollection.TryAdd(prompt); @@ -54,16 +53,14 @@ public void Configure(McpServerOptions options) if (!promptCollection.IsEmpty) { - options.Capabilities ??= new(); - options.Capabilities.Prompts ??= new(); - options.Capabilities.Prompts.PromptCollection = promptCollection; + options.PromptCollection = promptCollection; } // Collect all of the provided resources into a resources collection. If the options already has // 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 // change notifications, etc. - McpServerResourceCollection resourceCollection = options.Capabilities?.Resources?.ResourceCollection ?? []; + McpServerResourceCollection resourceCollection = options.ResourceCollection ?? []; foreach (var resource in serverResources) { resourceCollection.TryAdd(resource); @@ -71,12 +68,71 @@ public void Configure(McpServerOptions options) if (!resourceCollection.IsEmpty) { - options.Capabilities ??= new(); - options.Capabilities.Resources ??= new(); - options.Capabilities.Resources.ResourceCollection = resourceCollection; + options.ResourceCollection = resourceCollection; } // Apply custom server handlers. - serverHandlers.Value.OverwriteWithSetHandlers(options); + OverwriteWithSetHandlers(serverHandlers.Value, options); + } + + /// + /// Overwrite any handlers in McpServerOptions with non-null handlers from this instance. + /// + private static void OverwriteWithSetHandlers(McpServerHandlers handlers, McpServerOptions options) + { + McpServerHandlers optionsHandlers = options.Handlers; + + PromptsCapability? promptsCapability = options.Capabilities?.Prompts; + if (handlers.ListPromptsHandler is not null || handlers.GetPromptHandler is not null) + { + promptsCapability ??= new(); + optionsHandlers.ListPromptsHandler = handlers.ListPromptsHandler ?? optionsHandlers.ListPromptsHandler; + optionsHandlers.GetPromptHandler = handlers.GetPromptHandler ?? optionsHandlers.GetPromptHandler; + } + + ResourcesCapability? resourcesCapability = options.Capabilities?.Resources; + if (handlers.ListResourceTemplatesHandler is not null || handlers.ListResourcesHandler is not null || handlers.ReadResourceHandler is not null) + { + resourcesCapability ??= new(); + optionsHandlers.ListResourceTemplatesHandler = handlers.ListResourceTemplatesHandler ?? optionsHandlers.ListResourceTemplatesHandler; + optionsHandlers.ListResourcesHandler = handlers.ListResourcesHandler ?? optionsHandlers.ListResourcesHandler; + optionsHandlers.ReadResourceHandler = handlers.ReadResourceHandler ?? optionsHandlers.ReadResourceHandler; + + if (handlers.SubscribeToResourcesHandler is not null || handlers.UnsubscribeFromResourcesHandler is not null) + { + optionsHandlers.SubscribeToResourcesHandler = handlers.SubscribeToResourcesHandler ?? optionsHandlers.SubscribeToResourcesHandler; + optionsHandlers.UnsubscribeFromResourcesHandler = handlers.UnsubscribeFromResourcesHandler ?? optionsHandlers.UnsubscribeFromResourcesHandler; + resourcesCapability.Subscribe = true; + } + } + + ToolsCapability? toolsCapability = options.Capabilities?.Tools; + if (handlers.ListToolsHandler is not null || handlers.CallToolHandler is not null) + { + toolsCapability ??= new(); + optionsHandlers.ListToolsHandler = handlers.ListToolsHandler ?? optionsHandlers.ListToolsHandler; + optionsHandlers.CallToolHandler = handlers.CallToolHandler ?? optionsHandlers.CallToolHandler; + } + + LoggingCapability? loggingCapability = options.Capabilities?.Logging; + if (handlers.SetLoggingLevelHandler is not null) + { + loggingCapability ??= new(); + optionsHandlers.SetLoggingLevelHandler = handlers.SetLoggingLevelHandler; + } + + CompletionsCapability? completionsCapability = options.Capabilities?.Completions; + if (handlers.CompleteHandler is not null) + { + completionsCapability ??= new(); + optionsHandlers.CompleteHandler = handlers.CompleteHandler; + } + + options.Capabilities ??= new(); + options.Capabilities.Prompts = promptsCapability; + options.Capabilities.Resources = resourcesCapability; + options.Capabilities.Tools = toolsCapability; + options.Capabilities.Logging = loggingCapability; + options.Capabilities.Completions = completionsCapability; } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 9b3c91b94..a97bed3ae 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -250,9 +250,7 @@ public async Task Sampling_Sse_TestServer() int samplingHandlerCalls = 0; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously McpClientOptions options = new(); - options.Capabilities = new(); - options.Capabilities.Sampling ??= new(); - options.Capabilities.Sampling.SamplingHandler = async (_, _, _) => + options.Handlers.SamplingHandler = async (_, _, _) => { samplingHandlerCalls++; return new CreateMessageResult diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 0d867c8f0..ab8257d7d 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -161,29 +161,21 @@ public async Task Sampling_DoesNotCloseStream_Prematurely() await app.StartAsync(TestContext.Current.CancellationToken); var sampleCount = 0; - var clientOptions = new McpClientOptions + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.SamplingHandler = async (parameters, _, _) => { - Capabilities = new() + Assert.NotNull(parameters?.Messages); + var message = Assert.Single(parameters.Messages); + Assert.Equal(Role.User, message.Role); + Assert.Equal("Test prompt for sampling", Assert.IsType(message.Content).Text); + + sampleCount++; + return new CreateMessageResult { - Sampling = new() - { - SamplingHandler = async (parameters, _, _) => - { - Assert.NotNull(parameters?.Messages); - var message = Assert.Single(parameters.Messages); - Assert.Equal(Role.User, message.Role); - Assert.Equal("Test prompt for sampling", Assert.IsType(message.Content).Text); - - sampleCount++; - return new CreateMessageResult - { - Model = "test-model", - Role = Role.Assistant, - Content = new TextContentBlock { Text = "Sampling response from client" }, - }; - }, - }, - }, + Model = "test-model", + Role = Role.Assistant, + Content = new TextContentBlock { Text = "Sampling response from client" }, + }; }; await using var mcpClient = await ConnectAsync(clientOptions: clientOptions); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index b50a43edc..639c38cab 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -102,9 +102,7 @@ public async Task SamplingRequest_Fails_WithInvalidOperationException() await StartAsync(); var mcpClientOptions = new McpClientOptions(); - mcpClientOptions.Capabilities = new(); - mcpClientOptions.Capabilities.Sampling ??= new(); - mcpClientOptions.Capabilities.Sampling.SamplingHandler = (_, _, _) => + mcpClientOptions.Handlers.SamplingHandler = (_, _, _) => { throw new UnreachableException(); }; @@ -122,9 +120,7 @@ public async Task RootsRequest_Fails_WithInvalidOperationException() await StartAsync(); var mcpClientOptions = new McpClientOptions(); - mcpClientOptions.Capabilities = new(); - mcpClientOptions.Capabilities.Roots ??= new(); - mcpClientOptions.Capabilities.Roots.RootsHandler = (_, _) => + mcpClientOptions.Handlers.RootsHandler = (_, _) => { throw new UnreachableException(); }; @@ -142,9 +138,7 @@ public async Task ElicitRequest_Fails_WithInvalidOperationException() await StartAsync(); var mcpClientOptions = new McpClientOptions(); - mcpClientOptions.Capabilities = new(); - mcpClientOptions.Capabilities.Elicitation ??= new(); - mcpClientOptions.Capabilities.Elicitation.ElicitationHandler = (_, _) => + mcpClientOptions.Handlers.ElicitationHandler = (_, _) => { throw new UnreachableException(); }; diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index ddd3701fd..bd236512a 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -1,10 +1,10 @@ -using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Serilog; -using System.Collections.Concurrent; -using System.Text; -using System.Text.Json; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -38,17 +38,16 @@ private static async Task Main(string[] args) McpServerOptions options = new() { - Capabilities = new ServerCapabilities - { - Tools = ConfigureTools(), - Resources = ConfigureResources(), - Prompts = ConfigurePrompts(), - Logging = ConfigureLogging(), - Completions = ConfigureCompletions(), - }, + Capabilities = new ServerCapabilities(), ServerInstructions = "This is a test server with only stub functionality", }; + ConfigureTools(options); + ConfigureResources(options); + ConfigurePrompts(options); + ConfigureLogging(options); + ConfigureCompletions(options); + using var loggerFactory = CreateLoggerFactory(); await using var stdioTransport = new StdioServerTransport("TestServer", loggerFactory); await using IMcpServer server = McpServerFactory.Create(stdioTransport, options, loggerFactory); @@ -105,222 +104,215 @@ await server.SendMessageAsync(new JsonRpcNotification } } - private static ToolsCapability ConfigureTools() + private static void ConfigureTools(McpServerOptions options) { - return new() + options.Handlers.ListToolsHandler = async (request, cancellationToken) => { - ListToolsHandler = async (request, cancellationToken) => + return new ListToolsResult { - return new ListToolsResult - { - Tools = - [ - new Tool - { - Name = "echo", - Description = "Echoes the input back to the client.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "The input to echo back." - } - }, - "required": ["message"] - } - """), - }, - new Tool - { - Name = "echoSessionId", - Description = "Echoes the session id back to the client.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object" - } - """, McpJsonUtilities.DefaultOptions), - }, - new Tool - { - Name = "sampleLLM", - Description = "Samples from an LLM using MCP's sampling feature.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "The prompt to send to the LLM" - }, - "maxTokens": { - "type": "number", - "description": "Maximum number of tokens to generate" - } + Tools = + [ + new Tool + { + Name = "echo", + Description = "Echoes the input back to the client.", + InputSchema = JsonSerializer.Deserialize(""" + { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The input to echo back." + } + }, + "required": ["message"] + } + """), + }, + new Tool + { + Name = "echoSessionId", + Description = "Echoes the session id back to the client.", + InputSchema = JsonSerializer.Deserialize(""" + { + "type": "object" + } + """, McpJsonUtilities.DefaultOptions), + }, + new Tool + { + Name = "sampleLLM", + Description = "Samples from an LLM using MCP's sampling feature.", + InputSchema = JsonSerializer.Deserialize(""" + { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The prompt to send to the LLM" }, - "required": ["prompt", "maxTokens"] - } - """), - } - ] - }; - }, - - CallToolHandler = async (request, cancellationToken) => + "maxTokens": { + "type": "number", + "description": "Maximum number of tokens to generate" + } + }, + "required": ["prompt", "maxTokens"] + } + """), + } + ] + }; + }; + options.Handlers.CallToolHandler = async (request, cancellationToken) => + { + if (request.Params?.Name == "echo") { - if (request.Params?.Name == "echo") + if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) { - if (request.Params?.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) - { - throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams); - } - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"Echo: {message}" }] - }; + throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams); } - else if (request.Params?.Name == "echoSessionId") + return new CallToolResult { - return new CallToolResult - { - Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] - }; - } - else if (request.Params?.Name == "sampleLLM") + Content = [new TextContentBlock { Text = $"Echo: {message}" }] + }; + } + else if (request.Params?.Name == "echoSessionId") + { + return new CallToolResult { - if (request.Params?.Arguments is null || - !request.Params.Arguments.TryGetValue("prompt", out var prompt) || - !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens)) - { - throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); - } - var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())), - cancellationToken); - - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] - }; - } - else + Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] + }; + } + else if (request.Params?.Name == "sampleLLM") + { + if (request.Params?.Arguments is null || + !request.Params.Arguments.TryGetValue("prompt", out var prompt) || + !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens)) { - throw new McpException($"Unknown tool: {request.Params?.Name}", McpErrorCode.InvalidParams); + throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); } + var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())), + cancellationToken); + + return new CallToolResult + { + Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] + }; + } + else + { + throw new McpException($"Unknown tool: {request.Params?.Name}", McpErrorCode.InvalidParams); } }; } - private static PromptsCapability ConfigurePrompts() + private static void ConfigurePrompts(McpServerOptions options) { - return new() + options.Handlers.ListPromptsHandler = async (request, cancellationToken) => { - ListPromptsHandler = async (request, cancellationToken) => + return new ListPromptsResult { - return new ListPromptsResult - { - Prompts = [ - new Prompt - { - Name = "simple_prompt", - Description = "A prompt without arguments" - }, - new Prompt - { - Name = "complex_prompt", - Description = "A prompt with arguments", - Arguments = - [ - new PromptArgument - { - Name = "temperature", - Description = "Temperature setting", - Required = true - }, - new PromptArgument - { - Name = "style", - Description = "Output style", - Required = false - } - ] - } - ] - }; - }, + Prompts = [ + new Prompt + { + Name = "simple_prompt", + Description = "A prompt without arguments" + }, + new Prompt + { + Name = "complex_prompt", + Description = "A prompt with arguments", + Arguments = + [ + new PromptArgument + { + Name = "temperature", + Description = "Temperature setting", + Required = true + }, + new PromptArgument + { + Name = "style", + Description = "Output style", + Required = false + } + ] + } + ] + }; + }; - GetPromptHandler = async (request, cancellationToken) => + options.Handlers.GetPromptHandler = async (request, cancellationToken) => + { + List messages = []; + if (request.Params?.Name == "simple_prompt") { - List messages = []; - if (request.Params?.Name == "simple_prompt") + messages.Add(new PromptMessage { - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, - }); - } - else if (request.Params?.Name == "complex_prompt") + Role = Role.User, + Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, + }); + } + else if (request.Params?.Name == "complex_prompt") + { + string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown"; + string style = request.Params.Arguments?["style"].ToString() ?? "unknown"; + messages.Add(new PromptMessage { - string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown"; - string style = request.Params.Arguments?["style"].ToString() ?? "unknown"; - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, - }); - messages.Add(new PromptMessage - { - Role = Role.Assistant, - Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, - }); - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new ImageContentBlock - { - Data = MCP_TINY_IMAGE, - MimeType = "image/png" - } - }); - } - else + Role = Role.User, + Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, + }); + messages.Add(new PromptMessage { - throw new McpException($"Unknown prompt: {request.Params?.Name}", McpErrorCode.InvalidParams); - } - - return new GetPromptResult + Role = Role.Assistant, + Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, + }); + messages.Add(new PromptMessage { - Messages = messages - }; + Role = Role.User, + Content = new ImageContentBlock + { + Data = MCP_TINY_IMAGE, + MimeType = "image/png" + } + }); } + else + { + throw new McpException($"Unknown prompt: {request.Params?.Name}", McpErrorCode.InvalidParams); + } + + return new GetPromptResult + { + Messages = messages + }; }; } private static LoggingLevel? _minimumLoggingLevel = null; - private static LoggingCapability ConfigureLogging() + private static void ConfigureLogging(McpServerOptions options) { - return new() + options.Handlers.SetLoggingLevelHandler = async (request, cancellationToken) => { - SetLoggingLevelHandler = async (request, cancellationToken) => + if (request.Params?.Level is null) { - if (request.Params?.Level is null) - { - throw new McpException("Missing required argument 'level'", McpErrorCode.InvalidParams); - } + throw new McpException("Missing required argument 'level'", McpErrorCode.InvalidParams); + } - _minimumLoggingLevel = request.Params.Level; + _minimumLoggingLevel = request.Params.Level; - return new EmptyResult(); - } + return new EmptyResult(); }; } private static readonly ConcurrentDictionary _subscribedResources = new(); - private static ResourcesCapability ConfigureResources() + private static void ConfigureResources(McpServerOptions options) { + var capabilities = options.Capabilities ??= new(); + capabilities.Resources = new() { Subscribe = true }; + List resources = []; List resourceContents = []; for (int i = 0; i < 100; ++i) @@ -361,128 +353,123 @@ private static ResourcesCapability ConfigureResources() const int pageSize = 10; - return new() + options.Handlers.ListResourceTemplatesHandler = async (request, cancellationToken) => { - ListResourceTemplatesHandler = async (request, cancellationToken) => - { - return new ListResourceTemplatesResult - { - ResourceTemplates = [ - new ResourceTemplate - { - UriTemplate = "test://dynamic/resource/{id}", - Name = "Dynamic Resource", - } - ] - }; - }, - - ListResourcesHandler = async (request, cancellationToken) => + return new ListResourceTemplatesResult { - int startIndex = 0; - if (request.Params?.Cursor is not null) - { - try + ResourceTemplates = [ + new ResourceTemplate { - var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(request.Params.Cursor)); - startIndex = Convert.ToInt32(startIndexAsString); + UriTemplate = "test://dynamic/resource/{id}", + Name = "Dynamic Resource", } - catch (Exception e) - { - throw new McpException($"Invalid cursor: '{request.Params.Cursor}'", e, McpErrorCode.InvalidParams); - } - } - - int endIndex = Math.Min(startIndex + pageSize, resources.Count); - string? nextCursor = null; + ] + }; + }; - if (endIndex < resources.Count) + options.Handlers.ListResourcesHandler = async (request, cancellationToken) => + { + int startIndex = 0; + if (request.Params?.Cursor is not null) + { + try { - nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString())); + var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(request.Params.Cursor)); + startIndex = Convert.ToInt32(startIndexAsString); } - return new ListResourcesResult + catch (Exception e) { - NextCursor = nextCursor, - Resources = resources.GetRange(startIndex, endIndex - startIndex) - }; - }, + throw new McpException($"Invalid cursor: '{request.Params.Cursor}'", e, McpErrorCode.InvalidParams); + } + } + + int endIndex = Math.Min(startIndex + pageSize, resources.Count); + string? nextCursor = null; - ReadResourceHandler = async (request, cancellationToken) => + if (endIndex < resources.Count) { - if (request.Params?.Uri is null) - { - throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); - } + nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString())); + } + return new ListResourcesResult + { + NextCursor = nextCursor, + Resources = resources.GetRange(startIndex, endIndex - startIndex) + }; + }; - if (request.Params.Uri.StartsWith("test://dynamic/resource/")) - { - var id = request.Params.Uri.Split('/').LastOrDefault(); - if (string.IsNullOrEmpty(id)) - { - throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); - } + options.Handlers.ReadResourceHandler = async (request, cancellationToken) => + { + if (request.Params?.Uri is null) + { + throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); + } - return new ReadResourceResult - { - Contents = [ - new TextResourceContents - { - Uri = request.Params.Uri, - MimeType = "text/plain", - Text = $"Dynamic resource {id}: This is a plaintext resource" - } - ] - }; + if (request.Params.Uri.StartsWith("test://dynamic/resource/")) + { + var id = request.Params.Uri.Split('/').LastOrDefault(); + if (string.IsNullOrEmpty(id)) + { + throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); } - ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) - ?? throw new McpException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams); - return new ReadResourceResult { - Contents = [contents] + Contents = [ + new TextResourceContents + { + Uri = request.Params.Uri, + MimeType = "text/plain", + Text = $"Dynamic resource {id}: This is a plaintext resource" + } + ] }; - }, + } - SubscribeToResourcesHandler = async (request, cancellationToken) => + ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) + ?? throw new McpException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams); + + return new ReadResourceResult { - if (request?.Params?.Uri is null) - { - throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); - } - if (!request.Params.Uri.StartsWith("test://static/resource/") - && !request.Params.Uri.StartsWith("test://dynamic/resource/")) - { - throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); - } + Contents = [contents] + }; + }; - _subscribedResources.TryAdd(request.Params.Uri, true); + options.Handlers.SubscribeToResourcesHandler = async (request, cancellationToken) => + { + if (request?.Params?.Uri is null) + { + throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); + } + if (!request.Params.Uri.StartsWith("test://static/resource/") + && !request.Params.Uri.StartsWith("test://dynamic/resource/")) + { + throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); + } - return new EmptyResult(); - }, + _subscribedResources.TryAdd(request.Params.Uri, true); - UnsubscribeFromResourcesHandler = async (request, cancellationToken) => - { - if (request?.Params?.Uri is null) - { - throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); - } - if (!request.Params.Uri.StartsWith("test://static/resource/") - && !request.Params.Uri.StartsWith("test://dynamic/resource/")) - { - throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); - } + return new EmptyResult(); + }; - _subscribedResources.TryRemove(request.Params.Uri, out _); + options.Handlers.UnsubscribeFromResourcesHandler = async (request, cancellationToken) => + { + if (request?.Params?.Uri is null) + { + throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); + } + if (!request.Params.Uri.StartsWith("test://static/resource/") + && !request.Params.Uri.StartsWith("test://dynamic/resource/")) + { + throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); + } - return new EmptyResult(); - }, + _subscribedResources.TryRemove(request.Params.Uri, out _); - Subscribe = true + return new EmptyResult(); }; } - private static CompletionsCapability ConfigureCompletions() + private static void ConfigureCompletions(McpServerOptions options) { List sampleResourceIds = ["1", "2", "3", "4", "5"]; Dictionary> exampleCompletions = new() @@ -491,7 +478,7 @@ private static CompletionsCapability ConfigureCompletions() {"temperature", ["0", "0.5", "0.7", "1.0"]}, }; - McpRequestHandler handler = async (request, cancellationToken) => + options.Handlers.CompleteHandler = async (request, cancellationToken) => { string[]? values; switch (request.Params?.Ref) @@ -517,8 +504,6 @@ private static CompletionsCapability ConfigureCompletions() throw new McpException($"Unknown reference type: '{request.Params?.Ref.Type}'", McpErrorCode.InvalidParams); } }; - - return new() { CompleteHandler = handler }; } static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index d4abf81f9..c5b5ce9ae 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -95,284 +95,271 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st } const int pageSize = 10; - - options.Capabilities = new() + options.Handlers.ListToolsHandler = async (request, cancellationToken) => { - Tools = new() + return new ListToolsResult { - ListToolsHandler = async (request, cancellationToken) => - { - return new ListToolsResult + Tools = + [ + new Tool { - Tools = - [ - new Tool + Name = "echo", + Description = "Echoes the input back to the client.", + InputSchema = JsonSerializer.Deserialize(""" { - Name = "echo", - Description = "Echoes the input back to the client.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "The input to echo back." - } - }, - "required": ["message"] + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The input to echo back." } - """, McpJsonUtilities.DefaultOptions), - }, - new Tool + }, + "required": ["message"] + } + """, McpJsonUtilities.DefaultOptions), + }, + new Tool + { + Name = "echoSessionId", + Description = "Echoes the session id back to the client.", + InputSchema = JsonSerializer.Deserialize(""" { - Name = "echoSessionId", - Description = "Echoes the session id back to the client.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object" - } - """, McpJsonUtilities.DefaultOptions), - }, - new Tool + "type": "object" + } + """, McpJsonUtilities.DefaultOptions), + }, + new Tool + { + Name = "sampleLLM", + Description = "Samples from an LLM using MCP's sampling feature.", + InputSchema = JsonSerializer.Deserialize(""" { - Name = "sampleLLM", - Description = "Samples from an LLM using MCP's sampling feature.", - InputSchema = JsonSerializer.Deserialize(""" - { - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "The prompt to send to the LLM" - }, - "maxTokens": { - "type": "number", - "description": "Maximum number of tokens to generate" - } - }, - "required": ["prompt", "maxTokens"] + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The prompt to send to the LLM" + }, + "maxTokens": { + "type": "number", + "description": "Maximum number of tokens to generate" } - """, McpJsonUtilities.DefaultOptions), + }, + "required": ["prompt", "maxTokens"] } - ] - }; - }, - CallToolHandler = async (request, cancellationToken) => - { - if (request.Params is null) - { - throw new McpException("Missing required parameter 'name'", McpErrorCode.InvalidParams); - } - if (request.Params.Name == "echo") - { - if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) - { - throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams); - } - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"Echo: {message}" }] - }; - } - else if (request.Params.Name == "echoSessionId") - { - return new CallToolResult - { - Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] - }; - } - else if (request.Params.Name == "sampleLLM") - { - if (request.Params.Arguments is null || - !request.Params.Arguments.TryGetValue("prompt", out var prompt) || - !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens)) - { - throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); - } - var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), - cancellationToken); - - return new CallToolResult - { - Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] - }; - } - else - { - throw new McpException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams); + """, McpJsonUtilities.DefaultOptions), } + ] + }; + }; + options.Handlers.CallToolHandler = async (request, cancellationToken) => + { + if (request.Params is null) + { + throw new McpException("Missing required parameter 'name'", McpErrorCode.InvalidParams); + } + if (request.Params.Name == "echo") + { + if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("message", out var message)) + { + throw new McpException("Missing required argument 'message'", McpErrorCode.InvalidParams); } - }, - Resources = new() + return new CallToolResult + { + Content = [new TextContentBlock { Text = $"Echo: {message}" }] + }; + } + else if (request.Params.Name == "echoSessionId") { - ListResourceTemplatesHandler = async (request, cancellationToken) => + return new CallToolResult { + Content = [new TextContentBlock { Text = request.Server.SessionId ?? string.Empty }] + }; + } + else if (request.Params.Name == "sampleLLM") + { + if (request.Params.Arguments is null || + !request.Params.Arguments.TryGetValue("prompt", out var prompt) || + !request.Params.Arguments.TryGetValue("maxTokens", out var maxTokens)) + { + throw new McpException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); + } + var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), + cancellationToken); - return new ListResourceTemplatesResult - { - ResourceTemplates = [ - new ResourceTemplate - { - UriTemplate = "test://dynamic/resource/{id}", - Name = "Dynamic Resource", - } - ] - }; - }, - - ListResourcesHandler = async (request, cancellationToken) => + return new CallToolResult { - int startIndex = 0; - var requestParams = request.Params ?? new(); - if (requestParams.Cursor is not null) + Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] + }; + } + else + { + throw new McpException($"Unknown tool: '{request.Params.Name}'", McpErrorCode.InvalidParams); + } + }; + options.Handlers.ListResourceTemplatesHandler = async (request, cancellationToken) => + { + + return new ListResourceTemplatesResult + { + ResourceTemplates = [ + new ResourceTemplate { - try - { - var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor)); - startIndex = Convert.ToInt32(startIndexAsString); - } - catch (Exception e) - { - throw new McpException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams); - } + UriTemplate = "test://dynamic/resource/{id}", + Name = "Dynamic Resource", } + ] + }; + }; - int endIndex = Math.Min(startIndex + pageSize, resources.Count); - string? nextCursor = null; + options.Handlers.ListResourcesHandler = async (request, cancellationToken) => + { + int startIndex = 0; + var requestParams = request.Params ?? new(); + if (requestParams.Cursor is not null) + { + try + { + var startIndexAsString = Encoding.UTF8.GetString(Convert.FromBase64String(requestParams.Cursor)); + startIndex = Convert.ToInt32(startIndexAsString); + } + catch (Exception e) + { + throw new McpException($"Invalid cursor: '{requestParams.Cursor}'", e, McpErrorCode.InvalidParams); + } + } - if (endIndex < resources.Count) - { - nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString())); - } + int endIndex = Math.Min(startIndex + pageSize, resources.Count); + string? nextCursor = null; - return new ListResourcesResult - { - NextCursor = nextCursor, - Resources = resources.GetRange(startIndex, endIndex - startIndex) - }; - }, - ReadResourceHandler = async (request, cancellationToken) => + if (endIndex < resources.Count) + { + nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(endIndex.ToString())); + } + + return new ListResourcesResult + { + NextCursor = nextCursor, + Resources = resources.GetRange(startIndex, endIndex - startIndex) + }; + }; + options.Handlers.ReadResourceHandler = async (request, cancellationToken) => + { + if (request.Params?.Uri is null) + { + throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); + } + + if (request.Params.Uri.StartsWith("test://dynamic/resource/")) + { + var id = request.Params.Uri.Split('/').LastOrDefault(); + if (string.IsNullOrEmpty(id)) { - if (request.Params?.Uri is null) - { - throw new McpException("Missing required argument 'uri'", McpErrorCode.InvalidParams); - } + throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); + } - if (request.Params.Uri.StartsWith("test://dynamic/resource/")) - { - var id = request.Params.Uri.Split('/').LastOrDefault(); - if (string.IsNullOrEmpty(id)) - { - throw new McpException($"Invalid resource URI: '{request.Params.Uri}'", McpErrorCode.InvalidParams); - } - - return new ReadResourceResult - { - Contents = [ - new TextResourceContents + return new ReadResourceResult + { + Contents = [ + new TextResourceContents { Uri = request.Params.Uri, MimeType = "text/plain", Text = $"Dynamic resource {id}: This is a plaintext resource" } - ] - }; - } + ] + }; + } - ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ?? - throw new McpException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams); + ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ?? + throw new McpException($"Resource not found: '{request.Params.Uri}'", McpErrorCode.InvalidParams); - return new ReadResourceResult - { - Contents = [contents] - }; - } - }, - Prompts = new() + return new ReadResourceResult { - ListPromptsHandler = async (request, cancellationToken) => - { - return new ListPromptsResult + Contents = [contents] + }; + }; + options.Handlers.ListPromptsHandler = async (request, cancellationToken) => + { + return new ListPromptsResult + { + Prompts = [ + new Prompt { - Prompts = [ - new Prompt + Name = "simple_prompt", + Description = "A prompt without arguments" + }, + new Prompt + { + Name = "complex_prompt", + Description = "A prompt with arguments", + Arguments = + [ + new PromptArgument { - Name = "simple_prompt", - Description = "A prompt without arguments" + Name = "temperature", + Description = "Temperature setting", + Required = true }, - new Prompt + new PromptArgument { - Name = "complex_prompt", - Description = "A prompt with arguments", - Arguments = - [ - new PromptArgument - { - Name = "temperature", - Description = "Temperature setting", - Required = true - }, - new PromptArgument - { - Name = "style", - Description = "Output style", - Required = false - } - ], + Name = "style", + Description = "Output style", + Required = false } - ] - }; - }, - GetPromptHandler = async (request, cancellationToken) => - { - if (request.Params is null) - { - throw new McpException("Missing required parameter 'name'", McpErrorCode.InvalidParams); + ], } - List messages = new(); - if (request.Params.Name == "simple_prompt") - { - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, - }); - } - else if (request.Params.Name == "complex_prompt") - { - string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown"; - string style = request.Params.Arguments?["style"].ToString() ?? "unknown"; - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, - }); - messages.Add(new PromptMessage - { - Role = Role.Assistant, - Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, - }); - messages.Add(new PromptMessage - { - Role = Role.User, - Content = new ImageContentBlock - { - Data = MCP_TINY_IMAGE, - MimeType = "image/png" - } - }); - } - else + ] + }; + }; + options.Handlers.GetPromptHandler = async (request, cancellationToken) => + { + if (request.Params is null) + { + throw new McpException("Missing required parameter 'name'", McpErrorCode.InvalidParams); + } + List messages = new(); + if (request.Params.Name == "simple_prompt") + { + messages.Add(new PromptMessage + { + Role = Role.User, + Content = new TextContentBlock { Text = "This is a simple prompt without arguments." }, + }); + } + else if (request.Params.Name == "complex_prompt") + { + string temperature = request.Params.Arguments?["temperature"].ToString() ?? "unknown"; + string style = request.Params.Arguments?["style"].ToString() ?? "unknown"; + messages.Add(new PromptMessage + { + Role = Role.User, + Content = new TextContentBlock { Text = $"This is a complex prompt with arguments: temperature={temperature}, style={style}" }, + }); + messages.Add(new PromptMessage + { + Role = Role.Assistant, + Content = new TextContentBlock { Text = "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?" }, + }); + messages.Add(new PromptMessage + { + Role = Role.User, + Content = new ImageContentBlock { - throw new McpException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams); + Data = MCP_TINY_IMAGE, + MimeType = "image/png" } + }); + } + else + { + throw new McpException($"Unknown prompt: {request.Params.Name}", McpErrorCode.InvalidParams); + } - return new GetPromptResult - { - Messages = messages - }; - } - }, + return new GetPromptResult + { + Messages = messages + }; }; } diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index e3d7ce44c..30e16ee2f 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -331,7 +331,7 @@ public async Task WithProgress_ProgressReported() int remainingProgress = TotalNotifications; TaskCompletionSource allProgressReceived = new(TaskCreationOptions.RunContinuationsAsynchronously); - Server.ServerOptions.Capabilities?.Tools?.ToolCollection?.Add(McpServerTool.Create(async (IProgress progress) => + Server.ServerOptions.ToolCollection?.Add(McpServerTool.Create(async (IProgress progress) => { for (int i = 0; i < TotalNotifications; i++) { diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs index 7516a2186..46e47ee75 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs @@ -65,24 +65,21 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) { Capabilities = new ClientCapabilities { - Sampling = new SamplingCapability - { - SamplingHandler = async (c, p, t) => - new CreateMessageResult - { - Content = new TextContentBlock { Text = "result" }, - Model = "test-model", - Role = Role.User, - StopReason = "endTurn" - }, - }, Roots = new RootsCapability { ListChanged = true, - RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }, } } }; + + clientOptions.Handlers.SamplingHandler = async (c, p, t) => new CreateMessageResult + { + Content = new TextContentBlock { Text = "result" }, + Model = "test-model", + Role = Role.User, + StopReason = "endTurn" + }; + clientOptions.Handlers.RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }; var clientTransport = (IClientTransport)Activator.CreateInstance(transportType)!; IMcpClient? client = null; diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 3e4361a57..42380687a 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -270,21 +270,18 @@ public async Task SubscribeResource_Stdio() // act TaskCompletionSource tcs = new(); - await using var client = await _fixture.CreateClientAsync(clientId, new() - { - Capabilities = new() + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.NotificationHandlers = + [ + new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => { - NotificationHandlers = - [ - new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => - { - var notificationParams = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); - tcs.TrySetResult(true); - return default; - }) - ] - } - }); + var notificationParams = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); + tcs.TrySetResult(true); + return default; + }) + ]; + + await using var client = await _fixture.CreateClientAsync(clientId, clientOptions); await client.SubscribeToResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); @@ -300,21 +297,18 @@ public async Task UnsubscribeResource_Stdio() // act TaskCompletionSource receivedNotification = new(); - await using var client = await _fixture.CreateClientAsync(clientId, new() - { - Capabilities = new() + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.NotificationHandlers = + [ + new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => { - NotificationHandlers = - [ - new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => - { - var notificationParams = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); - receivedNotification.TrySetResult(true); - return default; - }) - ] - } - }); + var notificationParams = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); + receivedNotification.TrySetResult(true); + return default; + }) + ]; + + await using var client = await _fixture.CreateClientAsync(clientId, clientOptions); await client.SubscribeToResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); // wait until we received a notification @@ -368,25 +362,19 @@ public async Task Sampling_Stdio(string clientId) { // Set up the sampling handler int samplingHandlerCalls = 0; - await using var client = await _fixture.CreateClientAsync(clientId, new() + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.SamplingHandler = async (_, _, _) => { - Capabilities = new() + samplingHandlerCalls++; + return new CreateMessageResult { - Sampling = new() - { - SamplingHandler = async (_, _, _) => - { - samplingHandlerCalls++; - return new CreateMessageResult - { - Model = "test-model", - Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, - }; - }, - }, - }, - }); + Model = "test-model", + Role = Role.Assistant, + Content = new TextContentBlock { Text = "Test response" }, + }; + }; + + await using var client = await _fixture.CreateClientAsync(clientId, clientOptions); // Call the server's sampleLLM tool which should trigger our sampling handler var result = await client.CallToolAsync( @@ -527,16 +515,10 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated() var samplingHandler = new OpenAIClient(s_openAIKey).GetChatClient("gpt-4o-mini") .AsIChatClient() .CreateSamplingHandler(); - await using var client = await McpClientFactory.CreateAsync(new StdioClientTransport(_fixture.EverythingServerTransportOptions), new() - { - Capabilities = new() - { - Sampling = new() - { - SamplingHandler = samplingHandler, - }, - }, - }, cancellationToken: TestContext.Current.CancellationToken); + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.SamplingHandler = samplingHandler; + + await using var client = await McpClientFactory.CreateAsync(new StdioClientTransport(_fixture.EverythingServerTransportOptions), clientOptions, cancellationToken: TestContext.Current.CancellationToken); var result = await client.CallToolAsync("sampleLLM", new Dictionary() { @@ -555,24 +537,21 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated() public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId) { TaskCompletionSource receivedNotification = new(); - await using var client = await _fixture.CreateClientAsync(clientId, new() - { - Capabilities = new() + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.NotificationHandlers = + [ + new(NotificationMethods.LoggingMessageNotification, (notification, cancellationToken) => { - NotificationHandlers = - [ - new(NotificationMethods.LoggingMessageNotification, (notification, cancellationToken) => - { - var loggingMessageNotificationParameters = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); - if (loggingMessageNotificationParameters is not null) - { - receivedNotification.TrySetResult(true); - } - return default; - }) - ] - } - }); + var loggingMessageNotificationParameters = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); + if (loggingMessageNotificationParameters is not null) + { + receivedNotification.TrySetResult(true); + } + return default; + }) + ]; + + await using var client = await _fixture.CreateClientAsync(clientId, clientOptions); // act await client.SetLoggingLevel(LoggingLevel.Debug, TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 38ef9ab5d..23cc9a135 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -90,7 +90,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer public void Adds_Prompts_To_Server() { var serverOptions = ServiceProvider.GetRequiredService>().Value; - var prompts = serverOptions?.Capabilities?.Prompts?.PromptCollection; + var prompts = serverOptions.PromptCollection; Assert.NotNull(prompts); Assert.NotEmpty(prompts); } @@ -137,7 +137,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() Assert.False(notificationRead.IsCompleted); var serverOptions = ServiceProvider.GetRequiredService>().Value; - var serverPrompts = serverOptions.Capabilities?.Prompts?.PromptCollection; + var serverPrompts = serverOptions.PromptCollection; Assert.NotNull(serverPrompts); var newPrompt = McpServerPrompt.Create([McpServerPrompt(Name = "NewPrompt")] () => "42"); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index c95fd7671..e81de8770 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -118,7 +118,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer public void Adds_Resources_To_Server() { var serverOptions = ServiceProvider.GetRequiredService>().Value; - var resources = serverOptions?.Capabilities?.Resources?.ResourceCollection; + var resources = serverOptions.ResourceCollection; Assert.NotNull(resources); Assert.NotEmpty(resources); } @@ -172,7 +172,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes() Assert.False(notificationRead.IsCompleted); var serverOptions = ServiceProvider.GetRequiredService>().Value; - var serverResources = serverOptions.Capabilities?.Resources?.ResourceCollection; + var serverResources = serverOptions.ResourceCollection; Assert.NotNull(serverResources); var newResource = McpServerResource.Create([McpServerResource(Name = "NewResource")] () => "42"); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 6313480f3..cea867a17 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -119,7 +119,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer public void Adds_Tools_To_Server() { var serverOptions = ServiceProvider.GetRequiredService>().Value; - var tools = serverOptions.Capabilities?.Tools?.ToolCollection; + var tools = serverOptions.ToolCollection; Assert.NotNull(tools); Assert.NotEmpty(tools); } @@ -201,7 +201,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes() Assert.False(notificationRead.IsCompleted); var serverOptions = ServiceProvider.GetRequiredService>().Value; - var serverTools = serverOptions.Capabilities?.Tools?.ToolCollection; + var serverTools = serverOptions.ToolCollection; Assert.NotNull(serverTools); var newTool = McpServerTool.Create([McpServerTool(Name = "NewTool")] () => "42"); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs new file mode 100644 index 000000000..151111db2 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerOptionsSetupTests.cs @@ -0,0 +1,194 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol.Tests.Configuration; + +public class McpServerOptionsSetupTests +{ + #region Prompt Handler Tests + [Fact] + public void Configure_WithListPromptsHandler_CreatesPromptsCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithListPromptsHandler(async (request, ct) => new ListPromptsResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ListPromptsHandler); + Assert.NotNull(options.Capabilities?.Prompts); + } + + [Fact] + public void Configure_WithGetPromptHandler_CreatesPromptsCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithGetPromptHandler(async (request, ct) => new GetPromptResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.GetPromptHandler); + Assert.NotNull(options.Capabilities?.Prompts); + } + #endregion + + #region Resource Handler Tests + [Fact] + public void Configure_WithListResourceTemplatesHandler_CreatesResourcesCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithListResourceTemplatesHandler(async (request, ct) => new ListResourceTemplatesResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ListResourceTemplatesHandler); + Assert.NotNull(options.Capabilities?.Resources); + } + + [Fact] + public void Configure_WithListResourcesHandler_CreatesResourcesCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithListResourcesHandler(async (request, ct) => new ListResourcesResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ListResourcesHandler); + Assert.NotNull(options.Capabilities?.Resources); + } + + [Fact] + public void Configure_WithReadResourceHandler_CreatesResourcesCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithReadResourceHandler(async (request, ct) => new ReadResourceResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ReadResourceHandler); + Assert.NotNull(options.Capabilities?.Resources); + } + + [Fact] + public void Configure_WithSubscribeToResourcesHandler_And_WithOtherResourcesHandler_EnablesSubscription() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithListResourcesHandler(async (request, ct) => new ListResourcesResult()) + .WithSubscribeToResourcesHandler(async (request, ct) => new EmptyResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ListResourcesHandler); + Assert.NotNull(options.Handlers.SubscribeToResourcesHandler); + Assert.NotNull(options.Capabilities?.Resources); + Assert.True(options.Capabilities.Resources.Subscribe); + } + + [Fact] + public void Configure_WithUnsubscribeFromResourcesHandler_And_WithOtherResourcesHandler_EnablesSubscription() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithListResourcesHandler(async (request, ct) => new ListResourcesResult()) + .WithUnsubscribeFromResourcesHandler(async (request, ct) => new EmptyResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ListResourcesHandler); + Assert.NotNull(options.Handlers.UnsubscribeFromResourcesHandler); + Assert.NotNull(options.Capabilities?.Resources); + Assert.True(options.Capabilities.Resources.Subscribe); + } + + [Fact] + public void Configure_WithSubscribeToResourcesHandler_WithoutOtherResourcesHandler_DoesNotCreateResourcesCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithSubscribeToResourcesHandler(async (request, ct) => new EmptyResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.Null(options.Handlers.SubscribeToResourcesHandler); + Assert.Null(options.Capabilities?.Resources); + } + + [Fact] + public void Configure_WithUnsubscribeFromResourcesHandler_WithoutOtherResourcesHandler_DoesNotCreateResourcesCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithUnsubscribeFromResourcesHandler(async (request, ct) => new EmptyResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.Null(options.Handlers.UnsubscribeFromResourcesHandler); + Assert.Null(options.Capabilities?.Resources); + } + #endregion + + #region Tool Handler Tests + [Fact] + public void Configure_WithListToolsHandler_CreatesToolsCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithListToolsHandler(async (request, ct) => new ListToolsResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.ListToolsHandler); + Assert.NotNull(options.Capabilities?.Tools); + } + + [Fact] + public void Configure_WithCallToolHandler_CreatesToolsCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithCallToolHandler(async (request, ct) => new CallToolResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.CallToolHandler); + Assert.NotNull(options.Capabilities?.Tools); + } + #endregion + + #region Logging Handler Tests + [Fact] + public void Configure_WithSetLoggingLevelHandler_CreatesLoggingCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithSetLoggingLevelHandler(async (request, ct) => new EmptyResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.SetLoggingLevelHandler); + Assert.NotNull(options.Capabilities?.Logging); + } + #endregion + + #region Completion Handler Tests + [Fact] + public void Configure_WithCompleteHandler_CreatesCompletionsCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithCompleteHandler(async (request, ct) => new CompleteResult()); + + var options = services.BuildServiceProvider().GetRequiredService>().Value; + + Assert.NotNull(options.Handlers.CompleteHandler); + Assert.NotNull(options.Capabilities?.Completions); + } + #endregion +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs index 116c62a15..ae7a6bb65 100644 --- a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs +++ b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs @@ -139,16 +139,10 @@ private static async Task RunConnected(Func action await using (IMcpServer server = McpServerFactory.Create(serverTransport, new() { - Capabilities = new() - { - Tools = new() - { - ToolCollection = [ - McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." }), - McpServerTool.Create(() => { throw new Exception("boom"); }, new() { Name = "Throw", Description = "Throws error." }), - ], - } - } + ToolCollection = [ + McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." }), + McpServerTool.Create(() => { throw new Exception("boom"); }, new() { Name = "Throw", Description = "Throws error." }), + ] })) { serverTask = server.RunAsync(TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index ffd95076f..0751504bf 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -70,24 +70,16 @@ public async Task Sampling_Sse_EverythingServer() }; int samplingHandlerCalls = 0; - var defaultOptions = new McpClientOptions + var defaultOptions = new McpClientOptions(); + defaultOptions.Handlers.SamplingHandler = async (_, _, _) => { - Capabilities = new() + samplingHandlerCalls++; + return new CreateMessageResult { - Sampling = new() - { - SamplingHandler = async (_, _, _) => - { - samplingHandlerCalls++; - return new CreateMessageResult - { - Model = "test-model", - Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, - }; - }, - }, - }, + Model = "test-model", + Role = Role.Assistant, + Content = new TextContentBlock { Text = "Test response" }, + }; }; await using var client = await McpClientFactory.CreateAsync( diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index f44743916..539a7398b 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -67,78 +67,72 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer [Fact] public async Task Can_Elicit_Information() { - await using IMcpClient client = await CreateMcpClientForServer(new McpClientOptions + var clientOptions = new McpClientOptions(); + clientOptions.Handlers.ElicitationHandler = async (request, cancellationtoken) => { - Capabilities = new() + Assert.NotNull(request); + Assert.Equal("Please provide more information.", request.Message); + Assert.Equal(4, request.RequestedSchema.Properties.Count); + + foreach (var entry in request.RequestedSchema.Properties) { - Elicitation = new() + switch (entry.Key) { - ElicitationHandler = async (request, cancellationtoken) => - { - Assert.NotNull(request); - Assert.Equal("Please provide more information.", request.Message); - Assert.Equal(4, request.RequestedSchema.Properties.Count); + case "prop1": + var primitiveString = Assert.IsType(entry.Value); + Assert.Equal("title1", primitiveString.Title); + Assert.Equal(1, primitiveString.MinLength); + Assert.Equal(100, primitiveString.MaxLength); + break; - foreach (var entry in request.RequestedSchema.Properties) - { - switch (entry.Key) - { - case "prop1": - var primitiveString = Assert.IsType(entry.Value); - Assert.Equal("title1", primitiveString.Title); - Assert.Equal(1, primitiveString.MinLength); - Assert.Equal(100, primitiveString.MaxLength); - break; + case "prop2": + var primitiveNumber = Assert.IsType(entry.Value); + Assert.Equal("description2", primitiveNumber.Description); + Assert.Equal(0, primitiveNumber.Minimum); + Assert.Equal(1000, primitiveNumber.Maximum); + break; - case "prop2": - var primitiveNumber = Assert.IsType(entry.Value); - Assert.Equal("description2", primitiveNumber.Description); - Assert.Equal(0, primitiveNumber.Minimum); - Assert.Equal(1000, primitiveNumber.Maximum); - break; + case "prop3": + var primitiveBool = Assert.IsType(entry.Value); + Assert.Equal("title3", primitiveBool.Title); + Assert.Equal("description4", primitiveBool.Description); + Assert.True(primitiveBool.Default); + break; - case "prop3": - var primitiveBool = Assert.IsType(entry.Value); - Assert.Equal("title3", primitiveBool.Title); - Assert.Equal("description4", primitiveBool.Description); - Assert.True(primitiveBool.Default); - break; + case "prop4": + var primitiveEnum = Assert.IsType(entry.Value); + Assert.Equal(["option1", "option2", "option3"], primitiveEnum.Enum); + Assert.Equal(["Name1", "Name2", "Name3"], primitiveEnum.EnumNames); + break; - case "prop4": - var primitiveEnum = Assert.IsType(entry.Value); - Assert.Equal(["option1", "option2", "option3"], primitiveEnum.Enum); - Assert.Equal(["Name1", "Name2", "Name3"], primitiveEnum.EnumNames); - break; + default: + Assert.Fail($"Unknown property: {entry.Key}"); + break; + } + } - default: - Assert.Fail($"Unknown property: {entry.Key}"); - break; - } - } - - return new ElicitResult - { - Action = "accept", - Content = new Dictionary - { - ["prop1"] = (JsonElement)JsonSerializer.Deserialize(""" - "string result" - """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, - ["prop2"] = (JsonElement)JsonSerializer.Deserialize(""" - 42 - """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, - ["prop3"] = (JsonElement)JsonSerializer.Deserialize(""" - true - """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, - ["prop4"] = (JsonElement)JsonSerializer.Deserialize(""" - "option2" - """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, - }, - }; - }, + return new ElicitResult + { + Action = "accept", + Content = new Dictionary + { + ["prop1"] = (JsonElement)JsonSerializer.Deserialize(""" + "string result" + """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, + ["prop2"] = (JsonElement)JsonSerializer.Deserialize(""" + 42 + """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, + ["prop3"] = (JsonElement)JsonSerializer.Deserialize(""" + true + """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, + ["prop4"] = (JsonElement)JsonSerializer.Deserialize(""" + "option2" + """, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!, }, - }, - }); + }; + }; + + await using IMcpClient client = await CreateMcpClientForServer(clientOptions); var result = await client.CallToolAsync("TestElicitation", cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs index b2e748730..3ac650624 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs @@ -42,7 +42,7 @@ public void AddingLoggingLevelHandlerSetsLoggingCapability() var server = provider.GetRequiredService(); Assert.NotNull(server.ServerOptions.Capabilities?.Logging); - Assert.NotNull(server.ServerOptions.Capabilities.Logging.SetLoggingLevelHandler); + Assert.NotNull(server.ServerOptions.Handlers.SetLoggingLevelHandler); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 6750b2cad..3cbe542ac 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -235,24 +235,24 @@ await Can_Handle_Requests( public async Task Can_Handle_Completion_Requests() { await Can_Handle_Requests( - new() + new ServerCapabilities { Completions = new() - { - CompleteHandler = async (request, ct) => - new CompleteResult + }, + method: RequestMethods.CompletionComplete, + configureOptions: options => + { + options.Handlers.CompleteHandler = async (request, ct) => + new CompleteResult + { + Completion = new() { - Completion = new() - { - Values = ["test"], - Total = 2, - HasMore = true - } + Values = ["test"], + Total = 2, + HasMore = true } - } + }; }, - method: RequestMethods.CompletionComplete, - configureOptions: null, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -270,26 +270,26 @@ await Can_Handle_Requests( new ServerCapabilities { Resources = new() + }, + RequestMethods.ResourcesTemplatesList, + configureOptions: options => + { + options.Handlers.ListResourceTemplatesHandler = async (request, ct) => { - ListResourceTemplatesHandler = async (request, ct) => + return new ListResourceTemplatesResult { - return new ListResourceTemplatesResult - { - ResourceTemplates = [new() { UriTemplate = "test", Name = "Test Resource" }] - }; - }, - ListResourcesHandler = async (request, ct) => + ResourceTemplates = [new() { UriTemplate = "test", Name = "Test Resource" }] + }; + }; + options.Handlers.ListResourcesHandler = async (request, ct) => + { + return new ListResourcesResult { - return new ListResourcesResult - { - Resources = [new() { Uri = "test", Name = "Test Resource" }] - }; - }, - ReadResourceHandler = (request, ct) => throw new NotImplementedException(), - } + Resources = [new() { Uri = "test", Name = "Test Resource" }] + }; + }; + options.Handlers.ReadResourceHandler = (request, ct) => throw new NotImplementedException(); }, - RequestMethods.ResourcesTemplatesList, - configureOptions: null, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -306,19 +306,19 @@ await Can_Handle_Requests( new ServerCapabilities { Resources = new() + }, + RequestMethods.ResourcesList, + configureOptions: options => + { + options.Handlers.ListResourcesHandler = async (request, ct) => { - ListResourcesHandler = async (request, ct) => + return new ListResourcesResult { - return new ListResourcesResult - { - Resources = [new() { Uri = "test", Name = "Test Resource" }] - }; - }, - ReadResourceHandler = (request, ct) => throw new NotImplementedException(), - } + Resources = [new() { Uri = "test", Name = "Test Resource" }] + }; + }; + options.Handlers.ReadResourceHandler = (request, ct) => throw new NotImplementedException(); }, - RequestMethods.ResourcesList, - configureOptions: null, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -341,19 +341,19 @@ await Can_Handle_Requests( new ServerCapabilities { Resources = new() - { - ReadResourceHandler = async (request, ct) => - { - return new ReadResourceResult - { - Contents = [new TextResourceContents { Text = "test" }] - }; - }, - ListResourcesHandler = (request, ct) => throw new NotImplementedException(), - } }, method: RequestMethods.ResourcesRead, - configureOptions: null, + configureOptions: options => + { + options.Handlers.ReadResourceHandler = async (request, ct) => + { + return new ReadResourceResult + { + Contents = [new TextResourceContents { Text = "test" }] + }; + }; + options.Handlers.ListResourcesHandler = (request, ct) => throw new NotImplementedException(); + }, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -378,19 +378,19 @@ await Can_Handle_Requests( new ServerCapabilities { Prompts = new() + }, + method: RequestMethods.PromptsList, + configureOptions: options => + { + options.Handlers.ListPromptsHandler = async (request, ct) => { - ListPromptsHandler = async (request, ct) => + return new ListPromptsResult { - return new ListPromptsResult - { - Prompts = [new() { Name = "test" }] - }; - }, - GetPromptHandler = (request, ct) => throw new NotImplementedException(), - }, + Prompts = [new() { Name = "test" }] + }; + }; + options.Handlers.GetPromptHandler = (request, ct) => throw new NotImplementedException(); }, - method: RequestMethods.PromptsList, - configureOptions: null, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -413,13 +413,13 @@ await Can_Handle_Requests( new ServerCapabilities { Prompts = new() - { - GetPromptHandler = async (request, ct) => new GetPromptResult { Description = "test" }, - ListPromptsHandler = (request, ct) => throw new NotImplementedException(), - } }, method: RequestMethods.PromptsGet, - configureOptions: null, + configureOptions: options => + { + options.Handlers.GetPromptHandler = async (request, ct) => new GetPromptResult { Description = "test" }; + options.Handlers.ListPromptsHandler = (request, ct) => throw new NotImplementedException(); + }, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -441,19 +441,19 @@ await Can_Handle_Requests( new ServerCapabilities { Tools = new() + }, + method: RequestMethods.ToolsList, + configureOptions: options => + { + options.Handlers.ListToolsHandler = async (request, ct) => { - ListToolsHandler = async (request, ct) => + return new ListToolsResult { - return new ListToolsResult - { - Tools = [new() { Name = "test" }] - }; - }, - CallToolHandler = (request, ct) => throw new NotImplementedException(), - } + Tools = [new() { Name = "test" }] + }; + }; + options.Handlers.CallToolHandler = (request, ct) => throw new NotImplementedException(); }, - method: RequestMethods.ToolsList, - configureOptions: null, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -476,19 +476,19 @@ await Can_Handle_Requests( new ServerCapabilities { Tools = new() - { - CallToolHandler = async (request, ct) => - { - return new CallToolResult - { - Content = [new TextContentBlock { Text = "test" }] - }; - }, - ListToolsHandler = (request, ct) => throw new NotImplementedException(), - } }, method: RequestMethods.ToolsCall, - configureOptions: null, + configureOptions: options => + { + options.Handlers.CallToolHandler = async (request, ct) => + { + return new CallToolResult + { + Content = [new TextContentBlock { Text = "test" }] + }; + }; + options.Handlers.ListToolsHandler = (request, ct) => throw new NotImplementedException(); + }, assertResult: response => { var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); @@ -674,14 +674,12 @@ public async Task NotifyProgress_Should_Be_Handled() var options = CreateOptions(); var notificationReceived = new TaskCompletionSource(); - options.Capabilities = new() - { - NotificationHandlers = [new(NotificationMethods.ProgressNotification, (notification, cancellationToken) => + options.Handlers.NotificationHandlers = + [new(NotificationMethods.ProgressNotification, (notification, cancellationToken) => { notificationReceived.TrySetResult(notification); return default; - })], - }; + })]; var server = McpServerFactory.Create(transport, options, LoggerFactory);