From f636f4b08ad4a9ba44f7f2c05b9afadc12cba5f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:23:11 +0000 Subject: [PATCH 01/12] Initial plan From 701597c9bfdcca7ce1e02cd98d8dd96acab55f91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:34:39 +0000 Subject: [PATCH 02/12] Remove internal usages of obsolete capability properties Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> --- .../Client/McpClientImpl.cs | 10 +++---- .../Server/McpServerImpl.cs | 26 ------------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index 3a289d13e..105d46590 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -58,12 +58,10 @@ private void RegisterHandlers(McpClientOptions options, NotificationHandlers not { McpClientHandlers handlers = options.Handlers; -#pragma warning disable CS0618 // Type or member is obsolete - var notificationHandlersFromOptions = handlers.NotificationHandlers ?? options.Capabilities?.NotificationHandlers; - var samplingHandler = handlers.SamplingHandler ?? options.Capabilities?.Sampling?.SamplingHandler; - var rootsHandler = handlers.RootsHandler ?? options.Capabilities?.Roots?.RootsHandler; - var elicitationHandler = handlers.ElicitationHandler ?? options.Capabilities?.Elicitation?.ElicitationHandler; -#pragma warning restore CS0618 // Type or member is obsolete + var notificationHandlersFromOptions = handlers.NotificationHandlers; + var samplingHandler = handlers.SamplingHandler; + var rootsHandler = handlers.RootsHandler; + var elicitationHandler = handlers.ElicitationHandler; if (notificationHandlersFromOptions is not null) { diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 41408c22b..706578d9b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -230,10 +230,6 @@ private void ConfigureCompletion(McpServerOptions options) var completeHandler = options.Handlers.CompleteHandler; var completionsCapability = options.Capabilities?.Completions; -#pragma warning disable CS0618 // Type or member is obsolete - completeHandler ??= completionsCapability?.CompleteHandler; -#pragma warning restore CS0618 // Type or member is obsolete - if (completeHandler is null && completionsCapability is null) { return; @@ -266,14 +262,6 @@ private void ConfigureResources(McpServerOptions options) var resources = options.ResourceCollection; var resourcesCapability = options.Capabilities?.Resources; -#pragma warning disable CS0618 // Type or member is obsolete - listResourcesHandler ??= resourcesCapability?.ListResourcesHandler; - listResourceTemplatesHandler ??= resourcesCapability?.ListResourceTemplatesHandler; - readResourceHandler ??= resourcesCapability?.ReadResourceHandler; - subscribeHandler ??= resourcesCapability?.SubscribeToResourcesHandler; - unsubscribeHandler ??= resourcesCapability?.UnsubscribeFromResourcesHandler; -#pragma warning restore CS0618 // Type or member is obsolete - if (listResourcesHandler is null && listResourceTemplatesHandler is null && readResourceHandler is null && subscribeHandler is null && unsubscribeHandler is null && resources is null && resourcesCapability is null) @@ -427,11 +415,6 @@ private void ConfigurePrompts(McpServerOptions options) var prompts = options.PromptCollection; var promptsCapability = options.Capabilities?.Prompts; -#pragma warning disable CS0618 // Type or member is obsolete - listPromptsHandler ??= promptsCapability?.ListPromptsHandler; - getPromptHandler ??= promptsCapability?.GetPromptHandler; -#pragma warning restore CS0618 // Type or member is obsolete - if (listPromptsHandler is null && getPromptHandler is null && prompts is null && promptsCapability is null) { @@ -515,11 +498,6 @@ private void ConfigureTools(McpServerOptions options) var tools = options.ToolCollection; var toolsCapability = options.Capabilities?.Tools; -#pragma warning disable CS0618 // Type or member is obsolete - listToolsHandler ??= toolsCapability?.ListToolsHandler; - callToolHandler ??= toolsCapability?.CallToolHandler; -#pragma warning restore CS0618 // Type or member is obsolete - if (listToolsHandler is null && callToolHandler is null && tools is null && toolsCapability is null) { @@ -618,10 +596,6 @@ 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.Handlers.SetLoggingLevelHandler; -#pragma warning disable CS0618 // Type or member is obsolete - setLoggingLevelHandler ??= options.Capabilities?.Logging?.SetLoggingLevelHandler; -#pragma warning restore CS0618 // Type or member is obsolete - // Apply filters to the handler if (setLoggingLevelHandler is not null) { From d826cd3dc1d9584e4e8e299f63310eb2168567ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:45:40 +0000 Subject: [PATCH 03/12] Remove all obsolete API definitions and related tests Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> --- .../Client/McpClientExtensions.cs | 589 ------------------ src/ModelContextProtocol.Core/IMcpEndpoint.cs | 5 +- .../McpEndpointExtensions.cs | 126 ---- .../Protocol/ClientCapabilities.cs | 21 - .../Protocol/CompletionsCapability.cs | 12 - .../Protocol/ElicitationCapability.cs | 17 - .../Protocol/LoggingCapability.cs | 7 - .../Protocol/PromptsCapability.cs | 54 -- .../Protocol/ResourcesCapability.cs | 87 --- .../Protocol/RootsCapability.cs | 11 - .../Protocol/SamplingCapability.cs | 22 - .../Protocol/ServerCapabilities.cs | 21 - .../Protocol/ToolsCapability.cs | 44 -- .../Server/McpServerExtensions.cs | 122 ---- .../Server/McpServerFactory.cs | 34 - .../Client/McpClientExtensionsTests.cs | 396 ------------ .../McpEndpointExtensionsTests.cs | 119 ---- .../Server/McpServerExtensionsTests.cs | 221 ------- 18 files changed, 2 insertions(+), 1906 deletions(-) delete mode 100644 src/ModelContextProtocol.Core/Server/McpServerFactory.cs delete mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index f0cd3c4f9..86f77a900 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -66,595 +66,6 @@ public static class McpClientExtensions }; } - /// - /// Sends a ping request to verify server connectivity. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A task that completes when the ping is successful. - /// - /// - /// This method is used to check if the MCP server is online and responding to requests. - /// It can be useful for health checking, ensuring the connection is established, or verifying - /// that the client has proper authorization to communicate with the server. - /// - /// - /// The ping operation is lightweight and does not require any parameters. A successful completion - /// of the task indicates that the server is operational and accessible. - /// - /// - /// is . - /// Thrown when the server cannot be reached or returns an error response. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).PingAsync(cancellationToken); - - /// - /// Retrieves a list of available tools from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The serializer options governing tool parameter serialization. If null, the default options will be used. - /// The to monitor for cancellation requests. The default is . - /// A list of all available tools as instances. - /// - /// - /// This method fetches all available tools from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of tools and that responds with paginated responses, consider using - /// instead, as it streams tools as they arrive rather than loading them all at once. - /// - /// - /// The serializer options provided are flowed to each and will be used - /// when invoking tools in order to serialize any parameters. - /// - /// - /// - /// - /// // Get all tools available on the server - /// var tools = await mcpClient.ListToolsAsync(); - /// - /// // Use tools with an AI client - /// ChatOptions chatOptions = new() - /// { - /// Tools = [.. tools] - /// }; - /// - /// await foreach (var update in chatClient.GetStreamingResponseAsync(userMessage, chatOptions)) - /// { - /// Console.Write(update); - /// } - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask> ListToolsAsync( - this IMcpClient client, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListToolsAsync(serializerOptions, cancellationToken); - - /// - /// Creates an enumerable for asynchronously enumerating all available tools from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The serializer options governing tool parameter serialization. If null, the default options will be used. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available tools as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve tools from the server, which allows processing tools - /// as they arrive rather than waiting for all tools to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with tools split across multiple responses. - /// - /// - /// The serializer options provided are flowed to each and will be used - /// when invoking tools in order to serialize any parameters. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available tools. - /// - /// - /// - /// - /// // Enumerate all tools available on the server - /// await foreach (var tool in client.EnumerateToolsAsync()) - /// { - /// Console.WriteLine($"Tool: {tool.Name}"); - /// } - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncEnumerable EnumerateToolsAsync( - this IMcpClient client, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumerateToolsAsync(serializerOptions, cancellationToken); - - /// - /// Retrieves a list of available prompts from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A list of all available prompts as instances. - /// - /// - /// This method fetches all available prompts from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of prompts and that responds with paginated responses, consider using - /// instead, as it streams prompts as they arrive rather than loading them all at once. - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask> ListPromptsAsync( - this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListPromptsAsync(cancellationToken); - - /// - /// Creates an enumerable for asynchronously enumerating all available prompts from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available prompts as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve prompts from the server, which allows processing prompts - /// as they arrive rather than waiting for all prompts to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with prompts split across multiple responses. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available prompts. - /// - /// - /// - /// - /// // Enumerate all prompts available on the server - /// await foreach (var prompt in client.EnumeratePromptsAsync()) - /// { - /// Console.WriteLine($"Prompt: {prompt.Name}"); - /// } - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncEnumerable EnumeratePromptsAsync( - this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumeratePromptsAsync(cancellationToken); - - /// - /// Retrieves a specific prompt from the MCP server. - /// - /// The client instance used to communicate with the MCP server. - /// The name of the prompt to retrieve. - /// Optional arguments for the prompt. Keys are parameter names, and values are the argument values. - /// The serialization options governing argument serialization. - /// The to monitor for cancellation requests. The default is . - /// A task containing the prompt's result with content and messages. - /// - /// - /// This method sends a request to the MCP server to create the specified prompt with the provided arguments. - /// The server will process the arguments and return a prompt containing messages or other content. - /// - /// - /// Arguments are serialized into JSON and passed to the server, where they may be used to customize the - /// prompt's behavior or content. Each prompt may have different argument requirements. - /// - /// - /// The returned contains a collection of objects, - /// which can be converted to objects using the method. - /// - /// - /// Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt. - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask GetPromptAsync( - this IMcpClient client, - string name, - IReadOnlyDictionary? arguments = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - => AsClientOrThrow(client).GetPromptAsync(name, arguments, serializerOptions, cancellationToken); - - /// - /// Retrieves a list of available resource templates from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A list of all available resource templates as instances. - /// - /// - /// This method fetches all available resource templates from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of resource templates and that responds with paginated responses, consider using - /// instead, as it streams templates as they arrive rather than loading them all at once. - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask> ListResourceTemplatesAsync( - this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListResourceTemplatesAsync(cancellationToken); - - /// - /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available resource templates as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve resource templates from the server, which allows processing templates - /// as they arrive rather than waiting for all templates to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with templates split across multiple responses. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available resource templates. - /// - /// - /// - /// - /// // Enumerate all resource templates available on the server - /// await foreach (var template in client.EnumerateResourceTemplatesAsync()) - /// { - /// Console.WriteLine($"Template: {template.Name}"); - /// } - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncEnumerable EnumerateResourceTemplatesAsync( - this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumerateResourceTemplatesAsync(cancellationToken); - - /// - /// Retrieves a list of available resources from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A list of all available resources as instances. - /// - /// - /// This method fetches all available resources from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of resources and that responds with paginated responses, consider using - /// instead, as it streams resources as they arrive rather than loading them all at once. - /// - /// - /// - /// - /// // Get all resources available on the server - /// var resources = await client.ListResourcesAsync(); - /// - /// // Display information about each resource - /// foreach (var resource in resources) - /// { - /// Console.WriteLine($"Resource URI: {resource.Uri}"); - /// } - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask> ListResourcesAsync( - this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListResourcesAsync(cancellationToken); - - /// - /// Creates an enumerable for asynchronously enumerating all available resources from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available resources as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve resources from the server, which allows processing resources - /// as they arrive rather than waiting for all resources to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with resources split across multiple responses. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available resources. - /// - /// - /// - /// - /// // Enumerate all resources available on the server - /// await foreach (var resource in client.EnumerateResourcesAsync()) - /// { - /// Console.WriteLine($"Resource URI: {resource.Uri}"); - /// } - /// - /// - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static IAsyncEnumerable EnumerateResourcesAsync( - this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumerateResourcesAsync(cancellationToken); - - /// - /// Reads a resource from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The uri of the resource. - /// The to monitor for cancellation requests. The default is . - /// is . - /// is . - /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask ReadResourceAsync( - this IMcpClient client, string uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); - - /// - /// Reads a resource from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The uri of the resource. - /// The to monitor for cancellation requests. The default is . - /// is . - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask ReadResourceAsync( - this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); - - /// - /// Reads a resource from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The uri template of the resource. - /// Arguments to use to format . - /// The to monitor for cancellation requests. The default is . - /// is . - /// is . - /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask ReadResourceAsync( - this IMcpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, cancellationToken); - - /// - /// Requests completion suggestions for a prompt argument or resource reference. - /// - /// The client instance used to communicate with the MCP server. - /// The reference object specifying the type and optional URI or name. - /// The name of the argument for which completions are requested. - /// The current value of the argument, used to filter relevant completions. - /// The to monitor for cancellation requests. The default is . - /// A containing completion suggestions. - /// - /// - /// This method allows clients to request auto-completion suggestions for arguments in a prompt template - /// or for resource references. - /// - /// - /// When working with prompt references, the server will return suggestions for the specified argument - /// that match or begin with the current argument value. This is useful for implementing intelligent - /// auto-completion in user interfaces. - /// - /// - /// When working with resource references, the server will return suggestions relevant to the specified - /// resource URI. - /// - /// - /// is . - /// is . - /// is . - /// is empty or composed entirely of whitespace. - /// The server returned an error response. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).CompleteAsync(reference, argumentName, argumentValue, cancellationToken); - - /// - /// Subscribes to a resource on the server to receive notifications when it changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to which to subscribe. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method allows the client to register interest in a specific resource identified by its URI. - /// When the resource changes, the server will send notifications to the client, enabling real-time - /// updates without polling. - /// - /// - /// The subscription remains active until explicitly unsubscribed using - /// or until the client disconnects from the server. - /// - /// - /// To handle resource change notifications, register an event handler for the appropriate notification events, - /// such as with . - /// - /// - /// is . - /// is . - /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); - - /// - /// Subscribes to a resource on the server to receive notifications when it changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to which to subscribe. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method allows the client to register interest in a specific resource identified by its URI. - /// When the resource changes, the server will send notifications to the client, enabling real-time - /// updates without polling. - /// - /// - /// The subscription remains active until explicitly unsubscribed using - /// or until the client disconnects from the server. - /// - /// - /// To handle resource change notifications, register an event handler for the appropriate notification events, - /// such as with . - /// - /// - /// is . - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); - - /// - /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to unsubscribe from. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method cancels a previous subscription to a resource, stopping the client from receiving - /// notifications when that resource changes. - /// - /// - /// The unsubscribe operation is idempotent, meaning it can be called multiple times for the same - /// resource without causing errors, even if there is no active subscription. - /// - /// - /// Due to the nature of the MCP protocol, it is possible the client may receive notifications after - /// unsubscribing if those notifications were issued by the server prior to the unsubscribe request being received. - /// - /// - /// is . - /// is . - /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); - - /// - /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to unsubscribe from. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method cancels a previous subscription to a resource, stopping the client from receiving - /// notifications when that resource changes. - /// - /// - /// The unsubscribe operation is idempotent, meaning it can be called multiple times for the same - /// resource without causing errors, even if there is no active subscription. - /// - /// - /// Due to the nature of the MCP protocol, it is possible the client may receive notifications after - /// unsubscribing if those notifications were issued by the server prior to the unsubscribe request being received. - /// - /// - /// is . - /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); - - /// - /// Invokes a tool on the server. - /// - /// The client instance used to communicate with the MCP server. - /// The name of the tool to call on the server.. - /// An optional dictionary of arguments to pass to the tool. Each key represents a parameter name, - /// and its associated value represents the argument value. - /// - /// - /// An optional to have progress notifications reported to it. Setting this to a non- - /// value will result in a progress token being included in the call, and any resulting progress notifications during the operation - /// routed to this instance. - /// - /// - /// The JSON serialization options governing argument serialization. If , the default serialization options will be used. - /// - /// The to monitor for cancellation requests. The default is . - /// - /// A task containing the from the tool execution. The response includes - /// the tool's output content, which may be structured data, text, or an error message. - /// - /// is . - /// is . - /// The server could not find the requested tool, or the server encountered an error while processing the request. - /// - /// - /// // Call a simple echo tool with a string argument - /// var result = await client.CallToolAsync( - /// "echo", - /// new Dictionary<string, object?> - /// { - /// ["message"] = "Hello MCP!" - /// }); - /// - /// - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask CallToolAsync( - this IMcpClient client, - string toolName, - IReadOnlyDictionary? arguments = null, - IProgress? progress = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - => AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, serializerOptions, cancellationToken); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#pragma warning disable CS0618 // Type or member is obsolete - private static McpClient AsClientOrThrow(IMcpClient client, [CallerMemberName] string memberName = "") -#pragma warning restore CS0618 // Type or member is obsolete - { - if (client is not McpClient mcpClient) - { - ThrowInvalidEndpointType(memberName); - } - - return mcpClient; - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - static void ThrowInvalidEndpointType(string memberName) - => throw new InvalidOperationException( - $"Only arguments assignable to '{nameof(McpClient)}' are supported. " + - $"Prefer using '{nameof(McpClient)}.{memberName}' instead, as " + - $"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " + - $"removed in the future."); - } - /// /// Converts the contents of a into a pair of /// and instances to use diff --git a/src/ModelContextProtocol.Core/IMcpEndpoint.cs b/src/ModelContextProtocol.Core/IMcpEndpoint.cs index 40106cb07..9fe5d6484 100644 --- a/src/ModelContextProtocol.Core/IMcpEndpoint.cs +++ b/src/ModelContextProtocol.Core/IMcpEndpoint.cs @@ -67,9 +67,8 @@ public interface IMcpEndpoint : IAsyncDisposable /// /// /// This method provides low-level access to send any JSON-RPC message. For specific message types, - /// consider using the higher-level methods such as or extension methods - /// like , - /// which provide a simpler API. + /// consider using the higher-level methods such as or + /// , which provide a simpler API. /// /// /// The method will serialize the message and transmit it using the underlying transport mechanism. diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs index 1a5b5c1e2..a84cf5e73 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -23,130 +23,4 @@ namespace ModelContextProtocol; /// public static class McpEndpointExtensions { - /// - /// Sends a JSON-RPC request and attempts to deserialize the result to . - /// - /// The type of the request parameters to serialize from. - /// The type of the result to deserialize to. - /// The MCP client or server instance. - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The request id for the request. - /// The options governing request serialization. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. The task result contains the deserialized result. - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendRequestAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask SendRequestAsync( - this IMcpEndpoint endpoint, - string method, - TParameters parameters, - JsonSerializerOptions? serializerOptions = null, - RequestId requestId = default, - CancellationToken cancellationToken = default) - where TResult : notnull - => AsSessionOrThrow(endpoint).SendRequestAsync(method, parameters, serializerOptions, requestId, cancellationToken); - - /// - /// Sends a parameterless notification to the connected endpoint. - /// - /// The MCP client or server instance. - /// The notification method name. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// - /// - /// This method sends a notification without any parameters. Notifications are one-way messages - /// that don't expect a response. They are commonly used for events, status updates, or to signal - /// changes in state. - /// - /// - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task SendNotificationAsync(this IMcpEndpoint client, string method, CancellationToken cancellationToken = default) - => AsSessionOrThrow(client).SendNotificationAsync(method, cancellationToken); - - /// - /// Sends a notification with parameters to the connected endpoint. - /// - /// The type of the notification parameters to serialize. - /// The MCP client or server instance. - /// The JSON-RPC method name for the notification. - /// Object representing the notification parameters. - /// The options governing parameter serialization. If null, default options are used. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// - /// - /// This method sends a notification with parameters to the connected endpoint. Notifications are one-way - /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. - /// - /// - /// The parameters object is serialized to JSON according to the provided serializer options or the default - /// options if none are specified. - /// - /// - /// The Model Context Protocol defines several standard notification methods in , - /// but custom methods can also be used for application-specific notifications. - /// - /// - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task SendNotificationAsync( - this IMcpEndpoint endpoint, - string method, - TParameters parameters, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - => AsSessionOrThrow(endpoint).SendNotificationAsync(method, parameters, serializerOptions, cancellationToken); - - /// - /// Notifies the connected endpoint of progress for a long-running operation. - /// - /// The endpoint issuing the notification. - /// The identifying the operation for which progress is being reported. - /// The progress update to send, containing information such as percentage complete or status message. - /// The to monitor for cancellation requests. The default is . - /// A task representing the completion of the notification operation (not the operation being tracked). - /// is . - /// - /// - /// This method sends a progress notification to the connected endpoint using the Model Context Protocol's - /// standardized progress notification format. Progress updates are identified by a - /// that allows the recipient to correlate multiple updates with a specific long-running operation. - /// - /// - /// Progress notifications are sent asynchronously and don't block the operation from continuing. - /// - /// - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.NotifyProgressAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task NotifyProgressAsync( - this IMcpEndpoint endpoint, - ProgressToken progressToken, - ProgressNotificationValue progress, - CancellationToken cancellationToken = default) - => AsSessionOrThrow(endpoint).NotifyProgressAsync(progressToken, progress, cancellationToken); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#pragma warning disable CS0618 // Type or member is obsolete - private static McpSession AsSessionOrThrow(IMcpEndpoint endpoint, [CallerMemberName] string memberName = "") -#pragma warning restore CS0618 // Type or member is obsolete - { - if (endpoint is not McpSession session) - { - ThrowInvalidEndpointType(memberName); - } - - return session; - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - static void ThrowInvalidEndpointType(string memberName) - => throw new InvalidOperationException( - $"Only arguments assignable to '{nameof(McpSession)}' are supported. " + - $"Prefer using '{nameof(McpServer)}.{memberName}' instead, as " + - $"'{nameof(McpEndpointExtensions)}.{memberName}' is obsolete and will be " + - $"removed in the future."); - } } diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index 102960e36..5b559ce95 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -67,25 +67,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] - [Obsolete($"Use {nameof(McpClientOptions.Handlers.NotificationHandlers)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 8e28e67d3..54260a52e 100644 --- a/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs @@ -28,16 +28,4 @@ namespace ModelContextProtocol.Protocol; /// public sealed class CompletionsCapability { - /// - /// 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.CompleteHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 9d46bcc43..3fdf5ddcb 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitationCapability.cs @@ -23,21 +23,4 @@ namespace ModelContextProtocol.Protocol; /// public sealed class ElicitationCapability { - /// - /// 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] - [Obsolete($"Use {nameof(McpClientOptions.Handlers.ElicitationHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 c166a223a..ac8f03f95 100644 --- a/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/LoggingCapability.cs @@ -20,11 +20,4 @@ namespace ModelContextProtocol.Protocol; /// public sealed class LoggingCapability { - /// - /// Gets or sets the handler for set logging level requests from clients. - /// - [JsonIgnore] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.SetLoggingLevelHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 223576254..e0e09fdb2 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs @@ -32,58 +32,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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.ListPromptsHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.GetPromptHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.PromptCollection)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 15ab02a06..d30418384 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs @@ -30,91 +30,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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.ListResourceTemplatesHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.ListResourcesHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.ReadResourceHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.SubscribeToResourcesHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.UnsubscribeFromResourcesHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.ResourceCollection)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 8e2bcacfe..a6b2bbb8a 100644 --- a/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs @@ -34,15 +34,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] - [Obsolete($"Use {nameof(McpClientOptions.Handlers.RootsHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 8ddc7ecf8..fa1773e42 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs @@ -24,26 +24,4 @@ namespace ModelContextProtocol.Protocol; /// public sealed class SamplingCapability { - /// - /// 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] - [Obsolete($"Use {nameof(McpClientOptions.Handlers.SamplingHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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 ffe38d221..7cd3e705e 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -66,25 +66,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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.NotificationHandlers)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable>>? NotificationHandlers { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs index b6903a7db..70b998dea 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs @@ -23,48 +23,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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.ListToolsHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.Handlers.CallToolHandler)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - 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] - [Obsolete($"Use {nameof(McpServerOptions.ToolCollection)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public McpServerPrimitiveCollection? ToolCollection { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index b20865576..640e6f606 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -13,126 +13,4 @@ namespace ModelContextProtocol.Server; /// public static class McpServerExtensions { - /// - /// Requests to sample an LLM via the client using the specified request parameters. - /// - /// The server instance initiating the request. - /// The parameters for the sampling request. - /// The to monitor for cancellation requests. - /// A task containing the sampling result from the client. - /// is . - /// The client does not support sampling. - /// - /// This method requires the client to support sampling capabilities. - /// It allows detailed control over sampling parameters including messages, system prompt, temperature, - /// and token limits. - /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask SampleAsync( - this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) - => AsServerOrThrow(server).SampleAsync(request, cancellationToken); - - /// - /// Requests to sample an LLM via the client using the provided chat messages and options. - /// - /// The server initiating the request. - /// The messages to send as part of the request. - /// The options to use for the request, including model parameters and constraints. - /// The to monitor for cancellation requests. The default is . - /// A task containing the chat response from the model. - /// is . - /// is . - /// The client does not support sampling. - /// - /// This method converts the provided chat messages into a format suitable for the sampling API, - /// handling different content types such as text, images, and audio. - /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static Task SampleAsync( - this IMcpServer server, - IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) - => AsServerOrThrow(server).SampleAsync(messages, options, cancellationToken); - - /// - /// Creates an wrapper that can be used to send sampling requests to the client. - /// - /// The server to be wrapped as an . - /// The that can be used to issue sampling requests to the client. - /// is . - /// The client does not support sampling. - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static IChatClient AsSamplingChatClient(this IMcpServer server) - => AsServerOrThrow(server).AsSamplingChatClient(); - - /// Gets an on which logged messages will be sent as notifications to the client. - /// The server to wrap as an . - /// An that can be used to log to the client.. - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) - => AsServerOrThrow(server).AsClientLoggerProvider(); - - /// - /// Requests the client to list the roots it exposes. - /// - /// The server initiating the request. - /// The parameters for the list roots request. - /// The to monitor for cancellation requests. - /// A task containing the list of roots exposed by the client. - /// is . - /// The client does not support roots. - /// - /// This method requires the client to support the roots capability. - /// Root resources allow clients to expose a hierarchical structure of resources that can be - /// navigated and accessed by the server. These resources might include file systems, databases, - /// or other structured data sources that the client makes available through the protocol. - /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.RequestRootsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask RequestRootsAsync( - this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) - => AsServerOrThrow(server).RequestRootsAsync(request, cancellationToken); - - /// - /// Requests additional information from the user via the client, allowing the server to elicit structured data. - /// - /// The server initiating the request. - /// The parameters for the elicitation request. - /// The to monitor for cancellation requests. - /// A task containing the elicitation result. - /// is . - /// The client does not support elicitation. - /// - /// This method requires the client to support the elicitation capability. - /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.ElicitAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 - [EditorBrowsable(EditorBrowsableState.Never)] - public static ValueTask ElicitAsync( - this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) - => AsServerOrThrow(server).ElicitAsync(request, cancellationToken); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] -#pragma warning disable CS0618 // Type or member is obsolete - private static McpServer AsServerOrThrow(IMcpServer server, [CallerMemberName] string memberName = "") -#pragma warning restore CS0618 // Type or member is obsolete - { - if (server is not McpServer mcpServer) - { - ThrowInvalidSessionType(memberName); - } - - return mcpServer; - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - static void ThrowInvalidSessionType(string memberName) - => throw new InvalidOperationException( - $"Only arguments assignable to '{nameof(McpServer)}' are supported. " + - $"Prefer using '{nameof(McpServer)}.{memberName}' instead, as " + - $"'{nameof(McpServerExtensions)}.{memberName}' is obsolete and will be " + - $"removed in the future."); - } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs deleted file mode 100644 index 7a6609d0d..000000000 --- a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Protocol; -using System.ComponentModel; - -namespace ModelContextProtocol.Server; - -/// -/// Provides a factory for creating instances. -/// -/// -/// This is the recommended way to create instances. -/// The factory handles proper initialization of server instances with the required dependencies. -/// -[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 -[EditorBrowsable(EditorBrowsableState.Never)] -public static class McpServerFactory -{ - /// - /// Creates a new instance of an . - /// - /// Transport to use for the server representing an already-established MCP session. - /// Configuration options for this server, including capabilities. - /// Logger factory to use for logging. If null, logging will be disabled. - /// Optional service provider to create new instances of tools and other dependencies. - /// An instance that should be disposed when no longer needed. - /// is . - /// is . - public static IMcpServer Create( - ITransport transport, - McpServerOptions serverOptions, - ILoggerFactory? loggerFactory = null, - IServiceProvider? serviceProvider = null) - => McpServer.Create(transport, serverOptions, loggerFactory, serviceProvider); -} diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs deleted file mode 100644 index 8fb7d2203..000000000 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ /dev/null @@ -1,396 +0,0 @@ -using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; -using Moq; -using System.Text.Json; - -namespace ModelContextProtocol.Tests; - -#pragma warning disable CS0618 // Type or member is obsolete - -public class McpClientExtensionsTests -{ - [Fact] - public async Task PingAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.PingAsync(TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.PingAsync' instead", ex.Message); - } - - [Fact] - public async Task GetPromptAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( - "name", cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.GetPromptAsync' instead", ex.Message); - } - - [Fact] - public async Task CallToolAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.CallToolAsync( - "tool", cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.CallToolAsync' instead", ex.Message); - } - - [Fact] - public async Task ListResourcesAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ListResourcesAsync( - cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ListResourcesAsync' instead", ex.Message); - } - - [Fact] - public void EnumerateResourcesAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = Assert.Throws(() => client.EnumerateResourcesAsync(cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.EnumerateResourcesAsync' instead", ex.Message); - } - - [Fact] - public async Task SubscribeToResourceAsync_String_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.SubscribeToResourceAsync( - "mcp://resource/1", TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.SubscribeToResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task SubscribeToResourceAsync_Uri_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.SubscribeToResourceAsync( - new Uri("mcp://resource/1"), TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.SubscribeToResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task UnsubscribeFromResourceAsync_String_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.UnsubscribeFromResourceAsync( - "mcp://resource/1", TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.UnsubscribeFromResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task UnsubscribeFromResourceAsync_Uri_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.UnsubscribeFromResourceAsync( - new Uri("mcp://resource/1"), TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.UnsubscribeFromResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task ReadResourceAsync_String_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( - "mcp://resource/1", TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ReadResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task ReadResourceAsync_Uri_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( - new Uri("mcp://resource/1"), TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ReadResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task ReadResourceAsync_Template_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( - "mcp://resource/{id}", new Dictionary { ["id"] = 1 }, TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ReadResourceAsync' instead", ex.Message); - } - - [Fact] - public async Task CompleteAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - var reference = new PromptReference { Name = "prompt" }; - - var ex = await Assert.ThrowsAsync(async () => await client.CompleteAsync( - reference, "arg", "val", TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.CompleteAsync' instead", ex.Message); - } - - [Fact] - public async Task ListToolsAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ListToolsAsync( - cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ListToolsAsync' instead", ex.Message); - } - - [Fact] - public void EnumerateToolsAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = Assert.Throws(() => client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.EnumerateToolsAsync' instead", ex.Message); - } - - [Fact] - public async Task ListPromptsAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ListPromptsAsync( - cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ListPromptsAsync' instead", ex.Message); - } - - [Fact] - public void EnumeratePromptsAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = Assert.Throws(() => client.EnumeratePromptsAsync(cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.EnumeratePromptsAsync' instead", ex.Message); - } - - [Fact] - public async Task ListResourceTemplatesAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await client.ListResourceTemplatesAsync( - cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.ListResourceTemplatesAsync' instead", ex.Message); - } - - [Fact] - public void EnumerateResourceTemplatesAsync_Throws_When_Not_McpClient() - { - var client = new Mock(MockBehavior.Strict).Object; - - var ex = Assert.Throws(() => client.EnumerateResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpClient.EnumerateResourceTemplatesAsync' instead", ex.Message); - } - - [Fact] - public async Task PingAsync_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(new object(), McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - await client.PingAsync(TestContext.Current.CancellationToken); - - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task GetPromptAsync_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - var resultPayload = new GetPromptResult { Messages = [new PromptMessage { Role = Role.User, Content = new TextContentBlock { Text = "hi" } }] }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - var result = await client.GetPromptAsync("name", cancellationToken: TestContext.Current.CancellationToken); - - Assert.Equal("hi", Assert.IsType(result.Messages[0].Content).Text); - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task CallToolAsync_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - var callResult = new CallToolResult { Content = [new TextContentBlock { Text = "ok" }] }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(callResult, McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - var result = await client.CallToolAsync("tool", cancellationToken: TestContext.Current.CancellationToken); - - Assert.Equal("ok", Assert.IsType(result.Content[0]).Text); - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SubscribeToResourceAsync_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(new EmptyResult(), McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - await client.SubscribeToResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); - - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task UnsubscribeFromResourceAsync_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(new EmptyResult(), McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - await client.UnsubscribeFromResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); - - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task CompleteAsync_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - var completion = new Completion { Values = ["one", "two"] }; - var resultPayload = new CompleteResult { Completion = completion }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - var result = await client.CompleteAsync(new PromptReference { Name = "p" }, "arg", "val", TestContext.Current.CancellationToken); - - Assert.Contains("one", result.Completion.Values); - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task ReadResourceAsync_String_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - var resultPayload = new ReadResourceResult { Contents = [] }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - var result = await client.ReadResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); - - Assert.NotNull(result); - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task ReadResourceAsync_Uri_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - var resultPayload = new ReadResourceResult { Contents = [] }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - var result = await client.ReadResourceAsync(new Uri("mcp://resource/1"), TestContext.Current.CancellationToken); - - Assert.NotNull(result); - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task ReadResourceAsync_Template_Forwards_To_McpClient_SendRequestAsync() - { - var mockClient = new Mock { CallBase = true }; - - var resultPayload = new ReadResourceResult { Contents = [] }; - - mockClient - .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpClient client = mockClient.Object; - - var result = await client.ReadResourceAsync("mcp://resource/{id}", new Dictionary { ["id"] = 1 }, TestContext.Current.CancellationToken); - - Assert.NotNull(result); - mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } -} diff --git a/tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs b/tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs deleted file mode 100644 index 402b1d09d..000000000 --- a/tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -using ModelContextProtocol.Protocol; -using Moq; -using System.Text.Json; - -namespace ModelContextProtocol.Tests; - -#pragma warning disable CS0618 // Type or member is obsolete - -public class McpEndpointExtensionsTests -{ - [Fact] - public async Task SendRequestAsync_Generic_Throws_When_Not_McpSession() - { - var endpoint = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.SendRequestAsync( - endpoint, "method", "param", cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.SendRequestAsync' instead", ex.Message); - } - - [Fact] - public async Task SendNotificationAsync_Parameterless_Throws_When_Not_McpSession() - { - var endpoint = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.SendNotificationAsync( - endpoint, "notify", cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.SendNotificationAsync' instead", ex.Message); - } - - [Fact] - public async Task SendNotificationAsync_Generic_Throws_When_Not_McpSession() - { - var endpoint = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.SendNotificationAsync( - endpoint, "notify", "payload", cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.SendNotificationAsync' instead", ex.Message); - } - - [Fact] - public async Task NotifyProgressAsync_Throws_When_Not_McpSession() - { - var endpoint = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.NotifyProgressAsync( - endpoint, new ProgressToken("t1"), new ProgressNotificationValue { Progress = 0.5f }, cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.NotifyProgressAsync' instead", ex.Message); - } - - [Fact] - public async Task SendRequestAsync_Generic_Forwards_To_McpSession_SendRequestAsync() - { - var mockSession = new Mock { CallBase = true }; - - mockSession - .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(42, McpJsonUtilities.DefaultOptions), - }); - - IMcpEndpoint endpoint = mockSession.Object; - - var result = await endpoint.SendRequestAsync("method", "param", cancellationToken: TestContext.Current.CancellationToken); - - Assert.Equal(42, result); - mockSession.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendNotificationAsync_Parameterless_Forwards_To_McpSession_SendMessageAsync() - { - var mockSession = new Mock { CallBase = true }; - - mockSession - .Setup(s => s.SendMessageAsync(It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); - - IMcpEndpoint endpoint = mockSession.Object; - - await endpoint.SendNotificationAsync("notify", cancellationToken: TestContext.Current.CancellationToken); - - mockSession.Verify(s => s.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SendNotificationAsync_Generic_Forwards_To_McpSession_SendMessageAsync() - { - var mockSession = new Mock { CallBase = true }; - - mockSession - .Setup(s => s.SendMessageAsync(It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); - - IMcpEndpoint endpoint = mockSession.Object; - - await endpoint.SendNotificationAsync("notify", "payload", cancellationToken: TestContext.Current.CancellationToken); - - mockSession.Verify(s => s.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task NotifyProgressAsync_Forwards_To_McpSession_SendMessageAsync() - { - var mockSession = new Mock { CallBase = true }; - - mockSession - .Setup(s => s.SendMessageAsync(It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); - - IMcpEndpoint endpoint = mockSession.Object; - - await endpoint.NotifyProgressAsync(new ProgressToken("progress-token"), new ProgressNotificationValue { Progress = 1 }, cancellationToken: TestContext.Current.CancellationToken); - - mockSession.Verify(s => s.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); - } -} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs deleted file mode 100644 index bf90b218a..000000000 --- a/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -using Microsoft.Extensions.AI; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using Moq; -using System.Text.Json; - -namespace ModelContextProtocol.Tests.Server; - -#pragma warning disable CS0618 // Type or member is obsolete - -public class McpServerExtensionsTests -{ - [Fact] - public async Task SampleAsync_Request_Throws_When_Not_McpServer() - { - var server = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await server.SampleAsync( - new CreateMessageRequestParams - { - Messages = [new SamplingMessage { Role = Role.User, Content = new TextContentBlock { Text = "hi" } }], - MaxTokens = 1000 - }, - TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.SampleAsync' instead", ex.Message); - } - - [Fact] - public async Task SampleAsync_Messages_Throws_When_Not_McpServer() - { - var server = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await server.SampleAsync( - [new ChatMessage(ChatRole.User, "hi")], cancellationToken: TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.SampleAsync' instead", ex.Message); - } - - [Fact] - public void AsSamplingChatClient_Throws_When_Not_McpServer() - { - var server = new Mock(MockBehavior.Strict).Object; - - var ex = Assert.Throws(server.AsSamplingChatClient); - Assert.Contains("Prefer using 'McpServer.AsSamplingChatClient' instead", ex.Message); - } - - [Fact] - public void AsClientLoggerProvider_Throws_When_Not_McpServer() - { - var server = new Mock(MockBehavior.Strict).Object; - - var ex = Assert.Throws(server.AsClientLoggerProvider); - Assert.Contains("Prefer using 'McpServer.AsClientLoggerProvider' instead", ex.Message); - } - - [Fact] - public async Task RequestRootsAsync_Throws_When_Not_McpServer() - { - var server = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await server.RequestRootsAsync( - new ListRootsRequestParams(), TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.RequestRootsAsync' instead", ex.Message); - } - - [Fact] - public async Task ElicitAsync_Throws_When_Not_McpServer() - { - var server = new Mock(MockBehavior.Strict).Object; - - var ex = await Assert.ThrowsAsync(async () => await server.ElicitAsync( - new ElicitRequestParams { Message = "hello" }, TestContext.Current.CancellationToken)); - Assert.Contains("Prefer using 'McpServer.ElicitAsync' instead", ex.Message); - } - - [Fact] - public async Task SampleAsync_Request_Forwards_To_McpServer_SendRequestAsync() - { - var mockServer = new Mock { CallBase = true }; - - var resultPayload = new CreateMessageResult - { - Content = new TextContentBlock { Text = "resp" }, - Model = "test-model", - Role = Role.Assistant, - StopReason = "endTurn", - }; - - mockServer - .Setup(s => s.ClientCapabilities) - .Returns(new ClientCapabilities() { Sampling = new() }); - - mockServer - .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpServer server = mockServer.Object; - - var result = await server.SampleAsync(new CreateMessageRequestParams - { - Messages = [new SamplingMessage { Role = Role.User, Content = new TextContentBlock { Text = "hi" } }], - MaxTokens = 1000 - }, TestContext.Current.CancellationToken); - - Assert.Equal("test-model", result.Model); - Assert.Equal(Role.Assistant, result.Role); - Assert.Equal("resp", Assert.IsType(result.Content).Text); - mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task SampleAsync_Messages_Forwards_To_McpServer_SendRequestAsync() - { - var mockServer = new Mock { CallBase = true }; - - var resultPayload = new CreateMessageResult - { - Content = new TextContentBlock { Text = "resp" }, - Model = "test-model", - Role = Role.Assistant, - StopReason = "endTurn", - }; - - const int CustomMaxSamplingOutputTokens = 500; - - mockServer - .Setup(s => s.ClientCapabilities) - .Returns(new ClientCapabilities() { Sampling = new() }); - - mockServer - .Setup(s => s.ServerOptions) - .Returns(new McpServerOptions { MaxSamplingOutputTokens = CustomMaxSamplingOutputTokens }); - - CreateMessageRequestParams? capturedRequest = null; - mockServer - .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) - .Callback((request, _) => - { - capturedRequest = JsonSerializer.Deserialize( - request.Params ?? throw new InvalidOperationException(), - McpJsonUtilities.DefaultOptions); - }) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpServer server = mockServer.Object; - - var chatResponse = await server.SampleAsync([new ChatMessage(ChatRole.User, "hi")], cancellationToken: TestContext.Current.CancellationToken); - - Assert.Equal("test-model", chatResponse.ModelId); - var last = chatResponse.Messages.Last(); - Assert.Equal(ChatRole.Assistant, last.Role); - Assert.Equal("resp", last.Text); - mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - - // Verify that the default value was used - Assert.NotNull(capturedRequest); - Assert.Equal(CustomMaxSamplingOutputTokens, capturedRequest.MaxTokens); - } - - [Fact] - public async Task RequestRootsAsync_Forwards_To_McpServer_SendRequestAsync() - { - var mockServer = new Mock { CallBase = true }; - - var resultPayload = new ListRootsResult { Roots = [new Root { Uri = "root://a" }] }; - - mockServer - .Setup(s => s.ClientCapabilities) - .Returns(new ClientCapabilities() { Roots = new() }); - - mockServer - .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpServer server = mockServer.Object; - - var result = await server.RequestRootsAsync(new ListRootsRequestParams(), TestContext.Current.CancellationToken); - - Assert.Equal("root://a", result.Roots[0].Uri); - mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task ElicitAsync_Forwards_To_McpServer_SendRequestAsync() - { - var mockServer = new Mock { CallBase = true }; - - var resultPayload = new ElicitResult { Action = "accept" }; - - mockServer - .Setup(s => s.ClientCapabilities) - .Returns(new ClientCapabilities() { Elicitation = new() }); - - mockServer - .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new JsonRpcResponse - { - Id = default, - Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), - }); - - IMcpServer server = mockServer.Object; - - var result = await server.ElicitAsync(new ElicitRequestParams { Message = "hi" }, TestContext.Current.CancellationToken); - - Assert.Equal("accept", result.Action); - mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); - } -} From cd8f7ae328a8f862027a0cfeaed2036b02b8c4ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:11:32 +0000 Subject: [PATCH 04/12] Merge main branch and resolve conflicts Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs | 1 - src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs | 2 +- .../StatelessServerTests.cs | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index 9528ddba7..9f4af7ea5 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -250,7 +250,6 @@ private static Task WriteJsonRpcErrorAsync(HttpContext context, string errorMess { var jsonRpcError = new JsonRpcError { - Id = default, Error = new() { Code = errorCode, diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs index 79f4b8687..495a432ee 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs @@ -26,5 +26,5 @@ private protected JsonRpcMessageWithId() /// Each ID is expected to be unique within the context of a given session. /// [JsonPropertyName("id")] - public required RequestId Id { get; set; } + public RequestId Id { get; set; } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index 252af4b88..a843e2975 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -203,7 +203,6 @@ public static async Task TestSamplingErrors(McpServer server) var ex = await Assert.ThrowsAsync(() => server.SendRequestAsync(new JsonRpcRequest { - Id = default, Method = RequestMethods.SamplingCreateMessage })); return ex.Message; @@ -222,7 +221,6 @@ public static async Task TestRootsErrors(McpServer server) var ex = await Assert.ThrowsAsync(() => server.SendRequestAsync(new JsonRpcRequest { - Id = default, Method = RequestMethods.RootsList })); return ex.Message; @@ -241,7 +239,6 @@ public static async Task TestElicitationErrors(McpServer server) var ex = await Assert.ThrowsAsync(() => server.SendRequestAsync(new JsonRpcRequest { - Id = default, Method = RequestMethods.ElicitationCreate })); return ex.Message; From eb31afb02db5720976a31a589d57e56def01ce57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:13:33 +0000 Subject: [PATCH 05/12] Remove unused duplicate CreateSamplingHandler and helper methods from McpClient.Methods.cs Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../Client/McpClient.Methods.cs | 112 ------------------ 1 file changed, 112 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index 5550e786e..6397d8e78 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -555,118 +555,6 @@ async ValueTask SendRequestWithProgressAsync( } } - /// - /// Converts the contents of a into a pair of - /// and instances to use - /// as inputs into a operation. - /// - /// - /// The created pair of messages and options. - /// is . - internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( - CreateMessageRequestParams requestParams) - { - Throw.IfNull(requestParams); - - ChatOptions? options = null; - - if (requestParams.MaxTokens is int maxTokens) - { - (options ??= new()).MaxOutputTokens = maxTokens; - } - - if (requestParams.Temperature is float temperature) - { - (options ??= new()).Temperature = temperature; - } - - if (requestParams.StopSequences is { } stopSequences) - { - (options ??= new()).StopSequences = stopSequences.ToArray(); - } - - List messages = - (from sm in requestParams.Messages - let aiContent = sm.Content.ToAIContent() - where aiContent is not null - select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) - .ToList(); - - return (messages, options); - } - - /// Converts the contents of a into a . - /// The whose contents should be extracted. - /// The created . - /// is . - internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatResponse) - { - Throw.IfNull(chatResponse); - - // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports - // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one - // in any of the response messages, or we'll use all the text from them concatenated, otherwise. - - ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); - - ContentBlock? content = null; - if (lastMessage is not null) - { - foreach (var lmc in lastMessage.Contents) - { - if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) - { - content = dc.ToContent(); - } - } - } - - return new() - { - Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, - Model = chatResponse.ModelId ?? "unknown", - Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, - StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", - }; - } - - /// - /// 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 . - /// is . - public static Func, CancellationToken, ValueTask> CreateSamplingHandler( - IChatClient chatClient) - { - Throw.IfNull(chatClient); - - return async (requestParams, progress, cancellationToken) => - { - Throw.IfNull(requestParams); - - var (messages, options) = ToChatClientArguments(requestParams); - var progressToken = requestParams.ProgressToken; - - List updates = []; - await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) - { - updates.Add(update); - - if (progressToken is not null) - { - progress.Report(new() - { - Progress = updates.Count, - }); - } - } - - return ToCreateMessageResult(updates.ToChatResponse()); - }; - } - /// /// Sets the logging level for the server to control which log messages are sent to the client. /// From c7deb7f037c4194c5f029c6e2e533d4ae8dcbf37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 16:19:08 +0000 Subject: [PATCH 06/12] Delete empty McpEndpointExtensions and McpServerExtensions classes Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../McpEndpointExtensions.cs | 26 ------------------- .../Server/McpServerExtensions.cs | 16 ------------ 2 files changed, 42 deletions(-) delete mode 100644 src/ModelContextProtocol.Core/McpEndpointExtensions.cs delete mode 100644 src/ModelContextProtocol.Core/Server/McpServerExtensions.cs diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs deleted file mode 100644 index a84cf5e73..000000000 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text.Json; - -namespace ModelContextProtocol; - -/// -/// Provides extension methods for interacting with an . -/// -/// -/// -/// This class provides strongly-typed methods for working with the Model Context Protocol (MCP) endpoints, -/// simplifying JSON-RPC communication by handling serialization and deserialization of parameters and results. -/// -/// -/// These extension methods are designed to be used with both client () and -/// server () implementations of the interface. -/// -/// -public static class McpEndpointExtensions -{ -} diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs deleted file mode 100644 index 640e6f606..000000000 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Protocol; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text.Json; - -namespace ModelContextProtocol.Server; - -/// -/// Provides extension methods for interacting with an instance. -/// -public static class McpServerExtensions -{ -} From 4d6fe0a55af1ca37bc2fb74f6d66af39800e20f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 19:44:15 +0000 Subject: [PATCH 07/12] Remove obsolete IMcpEndpoint, IMcpClient, IMcpServer interfaces and McpClientFactory Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> --- .../Client/IMcpClient.cs | 50 ----------- .../Client/McpClient.Methods.cs | 4 +- .../Client/McpClient.cs | 4 +- .../Client/McpClientFactory.cs | 35 -------- .../Client/McpClientHandlers.cs | 2 +- src/ModelContextProtocol.Core/IMcpEndpoint.cs | 84 ------------------- .../McpSession.Methods.cs | 4 +- src/ModelContextProtocol.Core/McpSession.cs | 4 +- .../Server/IMcpServer.cs | 65 -------------- .../Server/McpServer.Methods.cs | 4 +- .../Server/McpServer.cs | 4 +- .../Server/McpServerHandlers.cs | 2 +- .../Server/RequestServiceProvider.cs | 7 +- 13 files changed, 9 insertions(+), 260 deletions(-) delete mode 100644 src/ModelContextProtocol.Core/Client/IMcpClient.cs delete mode 100644 src/ModelContextProtocol.Core/Client/McpClientFactory.cs delete mode 100644 src/ModelContextProtocol.Core/IMcpEndpoint.cs delete mode 100644 src/ModelContextProtocol.Core/Server/IMcpServer.cs diff --git a/src/ModelContextProtocol.Core/Client/IMcpClient.cs b/src/ModelContextProtocol.Core/Client/IMcpClient.cs deleted file mode 100644 index 141add86a..000000000 --- a/src/ModelContextProtocol.Core/Client/IMcpClient.cs +++ /dev/null @@ -1,50 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.ComponentModel; - -namespace ModelContextProtocol.Client; - -/// -/// Represents an instance of a Model Context Protocol (MCP) client that connects to and communicates with an MCP server. -/// -[Obsolete($"Use {nameof(McpClient)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IMcpClient : IMcpEndpoint -{ - /// - /// Gets the capabilities supported by the connected server. - /// - /// The client is not connected. - ServerCapabilities ServerCapabilities { get; } - - /// - /// Gets the implementation information of the connected server. - /// - /// - /// - /// This property provides identification details about the connected server, including its name and version. - /// It is populated during the initialization handshake and is available after a successful connection. - /// - /// - /// This information can be useful for logging, debugging, compatibility checks, and displaying server - /// information to users. - /// - /// - /// The client is not connected. - Implementation ServerInfo { get; } - - /// - /// Gets any instructions describing how to use the connected server and its features. - /// - /// - /// - /// This property contains instructions provided by the server during initialization that explain - /// how to effectively use its capabilities. These instructions can include details about available - /// tools, expected input formats, limitations, or any other helpful information. - /// - /// - /// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources. - /// It can be thought of like a "hint" to the model and may be added to a system prompt. - /// - /// - string? ServerInstructions { get; } -} diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index 6397d8e78..387cc615a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -10,9 +10,7 @@ namespace ModelContextProtocol.Client; /// /// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. /// -#pragma warning disable CS0618 // Type or member is obsolete -public abstract partial class McpClient : McpSession, IMcpClient -#pragma warning restore CS0618 // Type or member is obsolete +public abstract partial class McpClient : McpSession { /// Creates an , connecting it to the specified server. /// The transport instance used to communicate with the server. diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index c4abe33b7..2af310163 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -5,9 +5,7 @@ namespace ModelContextProtocol.Client; /// /// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. /// -#pragma warning disable CS0618 // Type or member is obsolete -public abstract partial class McpClient : McpSession, IMcpClient -#pragma warning restore CS0618 // Type or member is obsolete +public abstract partial class McpClient : McpSession { /// /// Gets the capabilities supported by the connected server. diff --git a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs deleted file mode 100644 index 805787256..000000000 --- a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.ComponentModel; - -namespace ModelContextProtocol.Client; - -/// -/// Provides factory methods for creating Model Context Protocol (MCP) clients. -/// -/// -/// This factory class is the primary way to instantiate instances -/// that connect to MCP servers. It handles the creation and connection -/// of appropriate implementations through the supplied transport. -/// -[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 -[EditorBrowsable(EditorBrowsableState.Never)] -public static partial class McpClientFactory -{ - /// Creates an , connecting it to the specified server. - /// The transport instance used to communicate with the server. - /// - /// A client configuration object which specifies client capabilities and protocol version. - /// If , details based on the current process will be employed. - /// - /// A logger factory for creating loggers for clients. - /// The to monitor for cancellation requests. The default is . - /// An that's connected to the specified server. - /// is . - /// is . - public static async Task CreateAsync( - IClientTransport clientTransport, - McpClientOptions? clientOptions = null, - ILoggerFactory? loggerFactory = null, - CancellationToken cancellationToken = default) - => await McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory, cancellationToken); -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs index fecb83299..6200cede3 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs @@ -36,7 +36,7 @@ public class McpClientHandlers /// /// /// 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 + /// For transient handlers, may be used to register a handler that can /// then be unregistered by disposing of the returned from the method. /// /// diff --git a/src/ModelContextProtocol.Core/IMcpEndpoint.cs b/src/ModelContextProtocol.Core/IMcpEndpoint.cs deleted file mode 100644 index 9fe5d6484..000000000 --- a/src/ModelContextProtocol.Core/IMcpEndpoint.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.ComponentModel; -using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; - -namespace ModelContextProtocol; - -/// -/// Represents a client or server Model Context Protocol (MCP) endpoint. -/// -/// -/// -/// The MCP endpoint provides the core communication functionality used by both clients and servers: -/// -/// Sending JSON-RPC requests and receiving responses. -/// Sending notifications to the connected endpoint. -/// Registering handlers for receiving notifications. -/// -/// -/// -/// serves as the base interface for both and -/// interfaces, providing the common functionality needed for MCP protocol -/// communication. Most applications will use these more specific interfaces rather than working with -/// directly. -/// -/// -/// All MCP endpoints should be properly disposed after use as they implement . -/// -/// -[Obsolete($"Use {nameof(McpSession)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IMcpEndpoint : IAsyncDisposable -{ - /// Gets an identifier associated with the current MCP session. - /// - /// Typically populated in transports supporting multiple sessions such as Streamable HTTP or SSE. - /// Can return if the session hasn't initialized or if the transport doesn't - /// support multiple sessions (as is the case with STDIO). - /// - string? SessionId { get; } - - /// - /// Sends a JSON-RPC request to the connected endpoint and waits for a response. - /// - /// The JSON-RPC request to send. - /// The to monitor for cancellation requests. The default is . - /// A task containing the endpoint's response. - /// The transport is not connected, or another error occurs during request processing. - /// An error occured during request processing. - /// - /// This method provides low-level access to send raw JSON-RPC requests. For most use cases, - /// consider using the strongly-typed extension methods that provide a more convenient API. - /// - Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default); - - /// - /// Sends a JSON-RPC message to the connected endpoint. - /// - /// - /// The JSON-RPC message to send. This can be any type that implements JsonRpcMessage, such as - /// JsonRpcRequest, JsonRpcResponse, JsonRpcNotification, or JsonRpcError. - /// - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// The transport is not connected. - /// is . - /// - /// - /// This method provides low-level access to send any JSON-RPC message. For specific message types, - /// consider using the higher-level methods such as or - /// , which provide a simpler API. - /// - /// - /// The method will serialize the message and transmit it using the underlying transport mechanism. - /// - /// - Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default); - - /// Registers a handler to be invoked when a notification for the specified method is received. - /// The notification method. - /// The handler to be invoked. - /// An that will remove the registered handler when disposed. - IAsyncDisposable RegisterNotificationHandler(string method, Func handler); -} diff --git a/src/ModelContextProtocol.Core/McpSession.Methods.cs b/src/ModelContextProtocol.Core/McpSession.Methods.cs index c537732f1..1ad7d7107 100644 --- a/src/ModelContextProtocol.Core/McpSession.Methods.cs +++ b/src/ModelContextProtocol.Core/McpSession.Methods.cs @@ -5,9 +5,7 @@ namespace ModelContextProtocol; -#pragma warning disable CS0618 // Type or member is obsolete -public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable -#pragma warning restore CS0618 // Type or member is obsolete +public abstract partial class McpSession : IAsyncDisposable { /// /// Sends a JSON-RPC request and attempts to deserialize the result to . diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index 429fdbfd4..7ceb1ad90 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -26,9 +26,7 @@ namespace ModelContextProtocol; /// All MCP sessions should be properly disposed after use as they implement . /// /// -#pragma warning disable CS0618 // Type or member is obsolete -public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable -#pragma warning restore CS0618 // Type or member is obsolete +public abstract partial class McpSession : IAsyncDisposable { /// Gets an identifier associated with the current MCP session. /// diff --git a/src/ModelContextProtocol.Core/Server/IMcpServer.cs b/src/ModelContextProtocol.Core/Server/IMcpServer.cs deleted file mode 100644 index 8b88aa7a2..000000000 --- a/src/ModelContextProtocol.Core/Server/IMcpServer.cs +++ /dev/null @@ -1,65 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.ComponentModel; - -namespace ModelContextProtocol.Server; - -/// -/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. -/// -[Obsolete($"Use {nameof(McpServer)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IMcpServer : IMcpEndpoint -{ - /// - /// Gets the capabilities supported by the client. - /// - /// - /// - /// These capabilities are established during the initialization handshake and indicate - /// which features the client supports, such as sampling, roots, and other - /// protocol-specific functionality. - /// - /// - /// Server implementations can check these capabilities to determine which features - /// are available when interacting with the client. - /// - /// - ClientCapabilities? ClientCapabilities { get; } - - /// - /// Gets the version and implementation information of the connected client. - /// - /// - /// - /// This property contains identification information about the client that has connected to this server, - /// including its name and version. This information is provided by the client during initialization. - /// - /// - /// Server implementations can use this information for logging, tracking client versions, - /// or implementing client-specific behaviors. - /// - /// - Implementation? ClientInfo { get; } - - /// - /// Gets the options used to construct this server. - /// - /// - /// These options define the server's capabilities, protocol version, and other configuration - /// settings that were used to initialize the server. - /// - McpServerOptions ServerOptions { get; } - - /// - /// Gets the service provider for the server. - /// - IServiceProvider? Services { get; } - - /// Gets the last logging level set by the client, or if it's never been set. - LoggingLevel? LoggingLevel { get; } - - /// - /// Runs the server, listening for and handling client requests. - /// - Task RunAsync(CancellationToken cancellationToken = default); -} diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index 609da53c1..877b9cda7 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -14,9 +14,7 @@ namespace ModelContextProtocol.Server; /// /// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. /// -#pragma warning disable CS0618 // Type or member is obsolete -public abstract partial class McpServer : McpSession, IMcpServer -#pragma warning restore CS0618 // Type or member is obsolete +public abstract partial class McpServer : McpSession { /// /// Caches request schemas for elicitation requests based on the type and serializer options. diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index 02c17de1a..2d8ea6826 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -5,9 +5,7 @@ namespace ModelContextProtocol.Server; /// /// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. /// -#pragma warning disable CS0618 // Type or member is obsolete -public abstract partial class McpServer : McpSession, IMcpServer -#pragma warning restore CS0618 // Type or member is obsolete +public abstract partial class McpServer : McpSession { /// /// Gets the capabilities supported by the client. diff --git a/src/ModelContextProtocol.Core/Server/McpServerHandlers.cs b/src/ModelContextProtocol.Core/Server/McpServerHandlers.cs index 0d8deba13..4251f487e 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerHandlers.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerHandlers.cs @@ -169,7 +169,7 @@ public sealed class McpServerHandlers /// /// /// 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 + /// For transient handlers, may be used to register a handler that can /// then be unregistered by disposing of the returned from the method. /// /// diff --git a/src/ModelContextProtocol.Core/Server/RequestServiceProvider.cs b/src/ModelContextProtocol.Core/Server/RequestServiceProvider.cs index 9359ea157..86f0a6884 100644 --- a/src/ModelContextProtocol.Core/Server/RequestServiceProvider.cs +++ b/src/ModelContextProtocol.Core/Server/RequestServiceProvider.cs @@ -19,18 +19,13 @@ internal sealed class RequestServiceProvider(RequestContext serviceType == typeof(RequestContext) || serviceType == typeof(McpServer) || -#pragma warning disable CS0618 // Type or member is obsolete - serviceType == typeof(IMcpServer) || -#pragma warning restore CS0618 // Type or member is obsolete serviceType == typeof(IProgress) || serviceType == typeof(ClaimsPrincipal); /// public object? GetService(Type serviceType) => serviceType == typeof(RequestContext) ? request : -#pragma warning disable CS0618 // Type or member is obsolete - serviceType == typeof(McpServer) || serviceType == typeof(IMcpServer) ? request.Server : -#pragma warning restore CS0618 // Type or member is obsolete + serviceType == typeof(McpServer) ? request.Server : serviceType == typeof(IProgress) ? (request.Params?.ProgressToken is { } progressToken ? new TokenProgress(request.Server, progressToken) : NullProgress.Instance) : serviceType == typeof(ClaimsPrincipal) ? request.User : From 2decb2d0d9d6606d4bc1fae55db9a82741b2cec6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 21:40:35 +0000 Subject: [PATCH 08/12] Remove extra newlines before closing braces in capability classes Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> --- src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs | 1 - src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs | 1 - src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs | 1 - src/ModelContextProtocol.Core/Protocol/RootsCapability.cs | 1 - src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs | 1 - src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index 5b559ce95..942d90aec 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -66,5 +66,4 @@ public sealed class ClientCapabilities /// [JsonPropertyName("elicitation")] public ElicitationCapability? Elicitation { 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 e0e09fdb2..650ebe8c0 100644 --- a/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/PromptsCapability.cs @@ -31,5 +31,4 @@ public sealed class PromptsCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { 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 d30418384..47cfb3d50 100644 --- a/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ResourcesCapability.cs @@ -29,5 +29,4 @@ public sealed class ResourcesCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { 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 a6b2bbb8a..966c23e2b 100644 --- a/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/RootsCapability.cs @@ -33,5 +33,4 @@ public sealed class RootsCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { 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 7cd3e705e..1b5d0c882 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -65,5 +65,4 @@ public sealed class ServerCapabilities /// [JsonPropertyName("completions")] public CompletionsCapability? Completions { get; set; } - } diff --git a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs index 70b998dea..2779e03aa 100644 --- a/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/ToolsCapability.cs @@ -22,5 +22,4 @@ public sealed class ToolsCapability /// [JsonPropertyName("listChanged")] public bool? ListChanged { get; set; } - } \ No newline at end of file From 4443cfcb6ec07179fa801d3eebd9cef04e32c137 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:18:31 +0000 Subject: [PATCH 09/12] Merge main branch and resolve conflicts (PR #976 integration) Co-authored-by: MackinnonBuck <10456961+MackinnonBuck@users.noreply.github.com> --- .github/dependabot.yml | 1 - Directory.Build.props | 5 +- Directory.Packages.props | 30 +- Makefile | 19 +- ModelContextProtocol.slnx | 2 + README.md | 10 +- docs/concepts/elicitation/elicitation.md | 9 +- .../samples/server/Tools/InteractiveTools.cs | 148 ++ docs/index.md | 6 + docs/toc.yml | 6 +- docs/versioning.md | 61 + global.json | 3 - samples/ChatWithTools/Program.cs | 1 + .../EverythingServer/Tools/SampleLlmTool.cs | 4 +- .../Tools/SampleLlmTool.cs | 4 +- .../Diagnostics.cs | 31 + .../ModelContextProtocol.Analyzers.csproj | 20 + .../XmlToDescriptionGenerator.cs | 414 +++++ .../AIContentExtensions.cs | 212 ++- .../Client/McpClientExtensions.cs | 143 -- .../Client/McpClientHandlers.cs | 2 +- src/ModelContextProtocol.Core/Diagnostics.cs | 2 +- .../ModelContextProtocol.Core.csproj | 17 + .../Protocol/ContentBlock.cs | 248 ++- .../Protocol/ContextInclusion.cs | 15 + .../Protocol/CreateMessageRequestParams.cs | 20 + .../Protocol/CreateMessageResult.cs | 26 +- .../Protocol/ElicitRequestParams.cs | 499 +++++- .../Protocol/ElicitResult.cs | 2 +- .../Protocol/SamplingCapability.cs | 24 +- .../Protocol/SamplingContextCapability.cs | 6 + .../Protocol/SamplingMessage.cs | 25 +- .../Protocol/SamplingToolsCapability.cs | 6 + .../Protocol/SingleItemOrListConverter.cs | 67 + .../Protocol/ToolChoice.cs | 32 + .../Server/AIFunctionMcpServerTool.cs | 4 +- .../Server/McpServer.Methods.cs | 98 +- .../Server/McpServerPrompt.cs | 2 +- .../Server/McpServerPromptAttribute.cs | 2 +- .../Server/McpServerPromptCreateOptions.cs | 4 +- .../Server/McpServerResourceCreateOptions.cs | 6 +- .../Server/McpServerTool.cs | 4 +- .../Server/McpServerToolAttribute.cs | 4 +- .../Server/McpServerToolCreateOptions.cs | 2 +- tests/Common/Utils/TestServerTransport.cs | 4 +- ...odelContextProtocol.Analyzers.Tests.csproj | 36 + .../XmlToDescriptionGeneratorTests.cs | 1545 +++++++++++++++++ .../HttpServerIntegrationTests.cs | 5 +- .../MapMcpTests.cs | 12 +- ...delContextProtocol.AspNetCore.Tests.csproj | 29 +- .../Program.cs | 6 +- .../Program.cs | 8 +- .../AIContentExtensionsTests.cs | 123 ++ .../Client/McpClientCreationTests.cs | 4 +- .../Client/McpClientTests.cs | 139 +- .../Client/McpClientToolTests.cs | 9 +- .../ClientIntegrationTests.cs | 4 +- .../DockerEverythingServerTests.cs | 4 +- .../ModelContextProtocol.Tests.csproj | 28 +- .../Protocol/ContentBlockTests.cs | 102 +- .../CreateMessageRequestParamsTests.cs | 174 ++ .../Protocol/CreateMessageResultTests.cs | 247 +++ .../Protocol/ElicitationDefaultValuesTests.cs | 8 +- .../Protocol/ElicitationTests.cs | 2 + .../Protocol/ElicitationTypedTests.cs | 4 +- .../Protocol/EnumSchemaTests.cs | 309 ++++ .../PrimitiveSchemaDefinitionTests.cs | 2 + .../Protocol/SamplingMessageTests.cs | 111 ++ .../Protocol/ToolChoiceTests.cs | 30 + .../Server/McpServerTests.cs | 8 +- .../Transport/StdioClientTransportTests.cs | 3 +- 71 files changed, 4803 insertions(+), 399 deletions(-) create mode 100644 docs/versioning.md create mode 100644 src/ModelContextProtocol.Analyzers/Diagnostics.cs create mode 100644 src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj create mode 100644 src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs delete mode 100644 src/ModelContextProtocol.Core/Client/McpClientExtensions.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs create mode 100644 src/ModelContextProtocol.Core/Protocol/ToolChoice.cs create mode 100644 tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj create mode 100644 tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 57236c8cf..7ffd6a269 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,7 +17,6 @@ updates: patterns: - "xunit.*" - "Microsoft.NET.Test.Sdk" - - "Microsoft.Testing.*" - "coverlet.*" - "GitHubActionsTestLogger" - "Moq" diff --git a/Directory.Build.props b/Directory.Build.props index 1fad98569..bd2aed325 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,9 +29,8 @@ - true - <_MTPResultsDirectory>$(ArtifactsTestResultsDir) - $(TestingPlatformCommandLineArguments) --results-directory $(_MTPResultsDirectory) --report-trx --report-trx-filename $(MSBuildProjectName).$(TargetFramework).$(OS).trx + trx%3bLogFileName=$(MSBuildProjectName).$(TargetFramework).$(OS).trx + $(ArtifactsTestResultsDir) diff --git a/Directory.Packages.props b/Directory.Packages.props index f21988562..65f62f16e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,6 @@ 8.0.22 9.0.11 10.0.0 - 2.0.2 @@ -47,16 +46,25 @@ + + + + + + + - + - + + - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + @@ -64,6 +72,7 @@ + @@ -72,13 +81,14 @@ - - + + - + + diff --git a/Makefile b/Makefile index 8fd72b1e1..f300ca158 100644 --- a/Makefile +++ b/Makefile @@ -18,15 +18,18 @@ build: restore test: build dotnet test \ --no-build \ - --no-progress \ --configuration $(CONFIGURATION) \ - --filter-not-trait 'Execution=Manual' \ - --crashdump \ - --hangdump \ - --hangdump-timeout 7m \ - --coverage \ - --coverage-output-format cobertura \ - -p:_MTPResultsDirectory=$(ARTIFACT_PATH)/testresults \ + --filter '(Execution!=Manual)' \ + --blame \ + --blame-crash \ + --blame-hang-timeout 7m \ + --diag "$(ARTIFACT_PATH)/diag.txt" \ + --logger "trx" \ + --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" \ + --collect "XPlat Code Coverage" \ + --results-directory $(ARTIFACT_PATH)/testresults \ + -- \ + RunConfiguration.CollectSourceInformation=true pack: restore dotnet pack --no-restore --configuration $(CONFIGURATION) diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index a70e3e310..1f6dce1ed 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -62,11 +62,13 @@ + + diff --git a/README.md b/README.md index 3099dfcd3..73c71bd2b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ To get started writing a client, the `McpClient.CreateAsync` method is used to i to a server. Once you have an `McpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. ```csharp +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; + var clientTransport = new StdioClientTransport(new StdioClientTransportOptions { Name = "Everything", @@ -63,7 +66,7 @@ var result = await client.CallToolAsync( cancellationToken:CancellationToken.None); // echo always returns one and only one text content object -Console.WriteLine(result.Content.First(c => c.Type == "text").Text); +Console.WriteLine(result.Content.OfType().First().Text); ``` You can find samples demonstrating how to use ModelContextProtocol with an LLM SDK in the [samples](samples) directory, and also refer to the [tests](tests/ModelContextProtocol.Tests) project for more examples. Additional examples and documentation will be added as in the near future. @@ -225,6 +228,11 @@ await using McpServer server = McpServer.Create(new StdioServerTransport("MyServ await server.RunAsync(); ``` +Descriptions can be added to tools, prompts, and resources in a variety of ways, including via the `[Description]` attribute from `System.ComponentModel`. +This attribute may be placed on a method to provide for the tool, prompt, or resource, or on individual parameters to describe each's purpose. +XML comments may also be used; if an `[McpServerTool]`, `[McpServerPrompt]`, or `[McpServerResource]`-attributed method is marked as `partial`, +XML comments placed on the method will be used automatically to generate `[Description]` attributes for the method and its parameters. + ## Acknowledgements The starting point for this library was a project called [mcpdotnet](https://github.com/PederHP/mcpdotnet), initiated by [Peder Holdgaard Pedersen](https://github.com/PederHP). We are grateful for the work done by Peder and other contributors to that repository, which created a solid foundation for this library. diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index 2d9d43c9f..bc1fc7ee2 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -16,9 +16,16 @@ The C# SDK registers an instance of so tools can simply add a parameter of type to their method signature to access it. The MCP Server must specify the schema of each input value it is requesting from the user. -Only primitive types (string, number, boolean) are supported for elicitation requests. +Primitive types (string, number, boolean) and enum types are supported for elicitation requests. The schema may include a description to help the user understand what is being requested. +For enum types, the SDK supports several schema formats: +- **UntitledSingleSelectEnumSchema**: A single-select enum where the enum values serve as both the value and display text +- **TitledSingleSelectEnumSchema**: A single-select enum with separate display titles for each option (using JSON Schema `oneOf` with `const` and `title`) +- **UntitledMultiSelectEnumSchema**: A multi-select enum allowing multiple values to be selected +- **TitledMultiSelectEnumSchema**: A multi-select enum with display titles for each option +- **LegacyTitledEnumSchema** (deprecated): The legacy enum schema using `enumNames` for backward compatibility + The server can request a single input or multiple inputs at once. To help distinguish multiple inputs, each input has a unique name. diff --git a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs index 1528fa5a6..1dfcf9ac4 100644 --- a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs +++ b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs @@ -123,4 +123,152 @@ CancellationToken token } } } + + // + [McpServerTool, Description("Example tool demonstrating various enum schema types")] + public async Task EnumExamples( + McpServer server, + CancellationToken token + ) + { + // Example 1: UntitledSingleSelectEnumSchema - Simple enum without display titles + var prioritySchema = new RequestSchema + { + Properties = + { + ["Priority"] = new UntitledSingleSelectEnumSchema + { + Title = "Priority Level", + Description = "Select the priority level", + Enum = ["low", "medium", "high", "critical"], + Default = "medium" + } + } + }; + + var priorityResponse = await server.ElicitAsync(new ElicitRequestParams + { + Message = "Select a priority level:", + RequestedSchema = prioritySchema + }, token); + + if (priorityResponse.Action != "accept") + { + return "Operation cancelled"; + } + + string? priority = priorityResponse.Content?["Priority"].GetString(); + + // Example 2: TitledSingleSelectEnumSchema - Enum with custom display titles + var severitySchema = new RequestSchema + { + Properties = + { + ["Severity"] = new TitledSingleSelectEnumSchema + { + Title = "Issue Severity", + Description = "Select the issue severity level", + OneOf = + [ + new EnumSchemaOption { Const = "p0", Title = "P0 - Critical (Immediate attention required)" }, + new EnumSchemaOption { Const = "p1", Title = "P1 - High (Urgent, within 24 hours)" }, + new EnumSchemaOption { Const = "p2", Title = "P2 - Medium (Within a week)" }, + new EnumSchemaOption { Const = "p3", Title = "P3 - Low (As time permits)" } + ], + Default = "p2" + } + } + }; + + var severityResponse = await server.ElicitAsync(new ElicitRequestParams + { + Message = "Select the issue severity:", + RequestedSchema = severitySchema + }, token); + + if (severityResponse.Action != "accept") + { + return "Operation cancelled"; + } + + string? severity = severityResponse.Content?["Severity"].GetString(); + + // Example 3: UntitledMultiSelectEnumSchema - Select multiple values + var tagsSchema = new RequestSchema + { + Properties = + { + ["Tags"] = new UntitledMultiSelectEnumSchema + { + Title = "Tags", + Description = "Select one or more tags", + MinItems = 1, + MaxItems = 3, + Items = new UntitledEnumItemsSchema + { + Type = "string", + Enum = ["bug", "feature", "documentation", "enhancement", "question"] + }, + Default = ["bug"] + } + } + }; + + var tagsResponse = await server.ElicitAsync(new ElicitRequestParams + { + Message = "Select up to 3 tags:", + RequestedSchema = tagsSchema + }, token); + + if (tagsResponse.Action != "accept") + { + return "Operation cancelled"; + } + + // For multi-select, the value is an array + var tags = tagsResponse.Content?["Tags"].EnumerateArray() + .Select(e => e.GetString()) + .ToArray(); + + // Example 4: TitledMultiSelectEnumSchema - Multi-select with custom titles + var featuresSchema = new RequestSchema + { + Properties = + { + ["Features"] = new TitledMultiSelectEnumSchema + { + Title = "Features", + Description = "Select desired features", + Items = new TitledEnumItemsSchema + { + AnyOf = + [ + new EnumSchemaOption { Const = "auth", Title = "Authentication & Authorization" }, + new EnumSchemaOption { Const = "api", Title = "RESTful API" }, + new EnumSchemaOption { Const = "ui", Title = "Modern UI Components" }, + new EnumSchemaOption { Const = "db", Title = "Database Integration" } + ] + } + } + } + }; + + var featuresResponse = await server.ElicitAsync(new ElicitRequestParams + { + Message = "Select desired features:", + RequestedSchema = featuresSchema + }, token); + + if (featuresResponse.Action != "accept") + { + return "Operation cancelled"; + } + + var features = featuresResponse.Content?["Features"].EnumerateArray() + .Select(e => e.GetString()) + .ToArray(); + + return $"Selected: Priority={priority}, Severity={severity}, Tags=[{string.Join(", ", tags ?? [])}], Features=[{string.Join(", ", features ?? [])}]"; + } + // } \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index facf2093b..f2163f094 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,6 +18,12 @@ For more information about MCP: For how-to guides, tutorials, and additional guidance, refer to the [official MCP documentation](https://modelcontextprotocol.io/). +## Official SDK packages + +The official C# SDK packages for stable and pre-release versions are published to the [NuGet Gallery](https://www.nuget.org) under the [ModelContextProtocolOfficial](https://www.nuget.org/profiles/ModelContextProtocolOfficial) profile. + +Continuous integration builds are published to the modelcontextprotocol organization's [GitHub NuGet package registry](https://github.com/orgs/modelcontextprotocol/packages?ecosystem=nuget). + ## License This project is licensed under the [MIT License](https://github.com/modelcontextprotocol/csharp-sdk/blob/main/LICENSE). diff --git a/docs/toc.yml b/docs/toc.yml index 350a2ae3b..84cf4de03 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -3,5 +3,7 @@ items: href: concepts/index.md - name: API Reference href: api/ModelContextProtocol.yml -- name: Github - href: https://github.com/ModelContextProtocol/csharp-sdk \ No newline at end of file +- name: Versioning + href: versioning.md +- name: GitHub + href: https://github.com/ModelContextProtocol/csharp-sdk diff --git a/docs/versioning.md b/docs/versioning.md new file mode 100644 index 000000000..c9174a74e --- /dev/null +++ b/docs/versioning.md @@ -0,0 +1,61 @@ +--- +title: C# SDK Versioning +author: jeffhandley +description: ModelContextProtocol C# SDK approach to versioning, breaking changes, and support +uid: versioning +--- +The ModelContextProtocol specification continues to evolve rapidly, and it's important for the C# SDK to remain current with specification additions and updates. To enable this, all NuGet packages that compose the SDK will follow [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) with MAJOR.MINOR.PATCH version numbers, and optional pre-release versions. + +Given a version number MAJOR.MINOR.PATCH, the package versions will increment the: + +* MAJOR version when incompatible API changes are included +* MINOR version when functionality is added in a backward-compatible manner +* PATCH version when backward-compatible bug fixes are included + +*A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements.* + +## Supported versions + +Beginning with the 1.0.0 release, the following support policy will be applied for the official C# ModelContextProtocol SDK packages: + +1. New functionality and additive APIs will be introduced in MINOR releases within the current MAJOR version only + * New functionality will not be added to an earlier MAJOR version +2. Bugs will be fixed within either: + 1. A new PATCH release against the latest MAJOR.MINOR version + 2. A new MINOR release against the latest MAJOR version +3. Critical, blocking issues will be fixed against: + 1. The latest MINOR version within _the current_ MAJOR version + 2. The latest MINOR version within _one previous_ MAJOR version, until the latest MAJOR version has been published for 3 months + +## Experimental APIs + +MAJOR or MINOR version updates might introduce or alter APIs annotated as [`[Experimental]`](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute). This attribute indicates that an API is experimental and it may change at any time--including within PATCH or MINOR version updates. + +Experimental APIs require suppression of diagnostic codes specific to the MCP SDK APIs, using an `MCPEXP` prefix. + +## Breaking changes + +Prior to the release of a stable 1.0.0 set of NuGet packages, the SDK remains in preview and breaking changes can be introduced without prior notice. All versions beginning with the stable 1.0.0 release will follow semantic versioning, and breaking changes will require increments to the MAJOR version. + +If feasible, the SDK will support all versions of the MCP spec. However, if breaking changes to the spec make this infeasible, preference will be given to the most recent version of the MCP spec, and this would be considered a breaking change necessitating a new MAJOR version. + +All releases are posted to https://github.com/modelcontextprotocol/csharp-sdk/releases with release notes. Issues and pull requests labeled with `breaking-change` are highlighted in the corresponding release notes. + +### Specification schema changes + +If the MCP specification changes the schema for JSON payloads, the C# SDK may use the [`McpSession.NegotiatedProtocolVersion`](https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpSession.html#ModelContextProtocol_McpSession_NegotiatedProtocolVersion) to dynamically change the payload schema, potentially using internal data transfer objects (DTOs) to achieve the needed deserialization behavior. These techniques will be applied where feasible to maintain backward- and forward-compatibility between MCP specification versions. + +Refer to the following prototypes for illustrations of how this could be achieved: + +* [Support multiple contents in sampling results](https://github.com/eiriktsarpalis/csharp-sdk/pull/2) +* [Support multiple contents in sampling results (using DTOs)](https://github.com/eiriktsarpalis/csharp-sdk/pull/3) + +### Obsolete APIs + +If APIs within the SDK become obsolete due to changes in the MCP spec or other evolution of the SDK's APIs, the [`[Obsolete]`](https://learn.microsoft.com/dotnet/api/system.obsoleteattribute) attribute will be applied to the affected APIs. + +1. Within a MINOR version update, APIs may be marked as `[Obsolete]` to produce _build warnings_ while the API remains functional. The build warnings will provide guidance specific to the affected APIs. +2. Within a MAJOR version update, APIs may be marked as `[Obsolete]` to produce _build errors_ indicating the API is no longer functional and always throws exceptions. The build errors will provide guidance specific to the affected APIs. +3. Within a MAJOR version update, obsolete APIs may be removed. API removals are expected to be rare and avoided wherever possible, and `[Obsolete]` attributes will be applied ahead of the API removal. + +Beginning with the 1.0.0 release, all obsoletions will use diagnostic codes specific to the MCP SDK APIs, using an `MCPOBS` prefix. diff --git a/global.json b/global.json index 4ed7c32bc..fcb4599c2 100644 --- a/global.json +++ b/global.json @@ -2,8 +2,5 @@ "sdk": { "version": "10.0.100", "rollForward": "minor" - }, - "test": { - "runner": "Microsoft.Testing.Platform" } } diff --git a/samples/ChatWithTools/Program.cs b/samples/ChatWithTools/Program.cs index c6fca0493..c5870cdc3 100644 --- a/samples/ChatWithTools/Program.cs +++ b/samples/ChatWithTools/Program.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; +using ModelContextProtocol; using ModelContextProtocol.Client; using OpenAI; using OpenTelemetry; diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs index 6bbe6e51d..48c5184b3 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -17,7 +17,7 @@ public static async Task SampleLLM( var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await server.SampleAsync(samplingParams, cancellationToken); - return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; + return $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}"; } private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) @@ -27,7 +27,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, + Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 2c96b8c35..7d4c61784 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -20,7 +20,7 @@ public static async Task SampleLLM( var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken); - return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; + return $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}"; } private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) @@ -30,7 +30,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, + Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/src/ModelContextProtocol.Analyzers/Diagnostics.cs b/src/ModelContextProtocol.Analyzers/Diagnostics.cs new file mode 100644 index 000000000..e2c70412b --- /dev/null +++ b/src/ModelContextProtocol.Analyzers/Diagnostics.cs @@ -0,0 +1,31 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Collections.Immutable; +using System.Text; +using System.Xml.Linq; + +namespace ModelContextProtocol.Analyzers; + +/// Provides the diagnostic descriptors used by the assembly. +internal static class Diagnostics +{ + public static DiagnosticDescriptor InvalidXmlDocumentation { get; } = new( + id: "MCP001", + title: "Invalid XML documentation for MCP method", + messageFormat: "XML comment for method '{0}' is invalid and cannot be processed to generate [Description] attributes.", + category: "mcp", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The XML documentation comment contains invalid XML and cannot be processed to generate Description attributes."); + + public static DiagnosticDescriptor McpMethodMustBePartial { get; } = new( + id: "MCP002", + title: "MCP method must be partial to generate [Description] attributes", + messageFormat: "Method '{0}' has XML documentation that could be used to generate [Description] attributes, but the method is not declared as partial.", + category: "mcp", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Methods with MCP attributes should be declared as partial to allow the source generator to emit Description attributes from XML documentation comments."); +} diff --git a/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj b/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj new file mode 100644 index 000000000..5338bbb84 --- /dev/null +++ b/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + true + false + true + + + + + + + + + + + + + diff --git a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs new file mode 100644 index 000000000..a5dff0c70 --- /dev/null +++ b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs @@ -0,0 +1,414 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.CodeDom.Compiler; +using System.Collections.Immutable; +using System.Text; +using System.Xml.Linq; + +namespace ModelContextProtocol.Analyzers; + +/// +/// Source generator that creates [Description] attributes from XML comments +/// for partial methods tagged with MCP attributes. +/// +[Generator] +public sealed class XmlToDescriptionGenerator : IIncrementalGenerator +{ + private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs"; + private const string McpServerToolAttributeName = "ModelContextProtocol.Server.McpServerToolAttribute"; + private const string McpServerPromptAttributeName = "ModelContextProtocol.Server.McpServerPromptAttribute"; + private const string McpServerResourceAttributeName = "ModelContextProtocol.Server.McpServerResourceAttribute"; + private const string DescriptionAttributeName = "System.ComponentModel.DescriptionAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Use ForAttributeWithMetadataName for each MCP attribute type + var toolMethods = CreateProviderForAttribute(context, McpServerToolAttributeName); + var promptMethods = CreateProviderForAttribute(context, McpServerPromptAttributeName); + var resourceMethods = CreateProviderForAttribute(context, McpServerResourceAttributeName); + + // Combine all three providers + var allMethods = toolMethods + .Collect() + .Combine(promptMethods.Collect()) + .Combine(resourceMethods.Collect()) + .Select(static (tuple, _) => + { + var ((tool, prompt), resource) = tuple; + return tool.AddRange(prompt).AddRange(resource); + }); + + // Combine with compilation to get well-known type symbols. + var compilationAndMethods = context.CompilationProvider.Combine(allMethods); + + // Write out the source for all methods. + context.RegisterSourceOutput(compilationAndMethods, static (spc, source) => Execute(source.Left, source.Right, spc)); + } + + private static IncrementalValuesProvider CreateProviderForAttribute( + IncrementalGeneratorInitializationContext context, + string attributeMetadataName) => + context.SyntaxProvider.ForAttributeWithMetadataName( + attributeMetadataName, + static (node, _) => node is MethodDeclarationSyntax, + static (ctx, ct) => + { + var methodDeclaration = (MethodDeclarationSyntax)ctx.TargetNode; + var methodSymbol = (IMethodSymbol)ctx.TargetSymbol; + return new MethodToGenerate(methodDeclaration, methodSymbol); + }); + + private static void Execute(Compilation compilation, ImmutableArray methods, SourceProductionContext context) + { + if (methods.IsDefaultOrEmpty || + compilation.GetTypeByMetadataName(DescriptionAttributeName) is not { } descriptionAttribute) + { + return; + } + + // Gather a list of all methods needing generation. + List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methodsToGenerate = new(methods.Length); + foreach (var methodModel in methods) + { + var xmlDocs = ExtractXmlDocumentation(methodModel.MethodSymbol, context); + + // Generate implementation for partial methods. + if (methodModel.MethodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + methodsToGenerate.Add((methodModel.MethodSymbol, methodModel.MethodDeclaration, xmlDocs)); + } + else if (xmlDocs is not null && HasGeneratableContent(xmlDocs, methodModel.MethodSymbol, descriptionAttribute)) + { + // The method is not partial but has XML docs that would generate attributes; issue a diagnostic. + context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.McpMethodMustBePartial, + methodModel.MethodDeclaration.Identifier.GetLocation(), + methodModel.MethodSymbol.Name)); + } + } + + // Generate a single file with all partial declarations. + if (methodsToGenerate.Count > 0) + { + string source = GenerateSourceFile(compilation, methodsToGenerate, descriptionAttribute); + context.AddSource(GeneratedFileName, SourceText.From(source, Encoding.UTF8)); + } + } + + private static XmlDocumentation? ExtractXmlDocumentation(IMethodSymbol methodSymbol, SourceProductionContext context) + { + string? xmlDoc = methodSymbol.GetDocumentationCommentXml(); + if (string.IsNullOrWhiteSpace(xmlDoc)) + { + return null; + } + + try + { + if (XDocument.Parse(xmlDoc).Element("member") is not { } memberElement) + { + return null; + } + + var summary = CleanXmlDocText(memberElement.Element("summary")?.Value); + var remarks = CleanXmlDocText(memberElement.Element("remarks")?.Value); + var returns = CleanXmlDocText(memberElement.Element("returns")?.Value); + + // Combine summary and remarks for method description. + var methodDescription = + string.IsNullOrWhiteSpace(remarks) ? summary : + string.IsNullOrWhiteSpace(summary) ? remarks : + $"{summary}\n{remarks}"; + + Dictionary paramDocs = new(StringComparer.Ordinal); + foreach (var paramElement in memberElement.Elements("param")) + { + var name = paramElement.Attribute("name")?.Value; + var value = CleanXmlDocText(paramElement.Value); + if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(value)) + { + paramDocs[name!] = value; + } + } + + // Return documentation even if empty - we'll still generate the partial implementation + return new(methodDescription ?? string.Empty, returns ?? string.Empty, paramDocs); + } + catch (System.Xml.XmlException) + { + // Emit warning for invalid XML + context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.InvalidXmlDocumentation, + methodSymbol.Locations.FirstOrDefault(), + methodSymbol.Name)); + return null; + } + } + + private static string CleanXmlDocText(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + // Remove leading/trailing whitespace and normalize line breaks + var lines = text!.Split('\n') + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrEmpty(line)); + + return string.Join(" ", lines).Trim(); + } + + private static string GenerateSourceFile( + Compilation compilation, + List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methods, + INamedTypeSymbol descriptionAttribute) + { + StringWriter sw = new(); + IndentedTextWriter writer = new(sw); + + writer.WriteLine("// "); + writer.WriteLine($"// ModelContextProtocol.Analyzers {typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}"); + writer.WriteLine(); + writer.WriteLine("#pragma warning disable"); + writer.WriteLine(); + writer.WriteLine("using System.ComponentModel;"); + writer.WriteLine("using ModelContextProtocol.Server;"); + writer.WriteLine(); + + // Group methods by namespace and containing type + var groupedMethods = methods.GroupBy(m => + m.MethodSymbol.ContainingNamespace.Name == compilation.GlobalNamespace.Name ? "" : + m.MethodSymbol.ContainingNamespace?.ToDisplayString() ?? + ""); + + bool firstNamespace = true; + foreach (var namespaceGroup in groupedMethods) + { + if (!firstNamespace) + { + writer.WriteLine(); + } + firstNamespace = false; + + // Check if this is the global namespace (methods with null ContainingNamespace) + bool isGlobalNamespace = string.IsNullOrEmpty(namespaceGroup.Key); + if (!isGlobalNamespace) + { + writer.WriteLine($"namespace {namespaceGroup.Key}"); + writer.WriteLine("{"); + writer.Indent++; + } + + // Group by containing type within namespace + bool isFirstTypeInNamespace = true; + foreach (var typeGroup in namespaceGroup.GroupBy(m => m.MethodSymbol.ContainingType, SymbolEqualityComparer.Default)) + { + if (typeGroup.Key is not INamedTypeSymbol containingType) + { + continue; + } + + if (!isFirstTypeInNamespace) + { + writer.WriteLine(); + } + isFirstTypeInNamespace = false; + + // Write out the type, which could include parent types. + AppendNestedTypeDeclarations(writer, containingType, typeGroup, descriptionAttribute); + } + + if (!isGlobalNamespace) + { + writer.Indent--; + writer.WriteLine("}"); + } + } + + return sw.ToString(); + } + + private static void AppendNestedTypeDeclarations( + IndentedTextWriter writer, + INamedTypeSymbol typeSymbol, + IGrouping typeGroup, + INamedTypeSymbol descriptionAttribute) + { + // Build stack of nested types from innermost to outermost + Stack types = []; + for (var current = typeSymbol; current is not null; current = current.ContainingType) + { + types.Push(current); + } + + // Generate type declarations from outermost to innermost + int nestingCount = types.Count; + while (types.Count > 0) + { + // Get the type keyword and handle records + var type = types.Pop(); + var typeDecl = type.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as TypeDeclarationSyntax; + string typeKeyword; + if (typeDecl is RecordDeclarationSyntax rds) + { + string classOrStruct = rds.ClassOrStructKeyword.ValueText; + if (string.IsNullOrEmpty(classOrStruct)) + { + classOrStruct = "class"; + } + + typeKeyword = $"{typeDecl.Keyword.ValueText} {classOrStruct}"; + } + else + { + typeKeyword = typeDecl?.Keyword.ValueText ?? "class"; + } + + writer.WriteLine($"partial {typeKeyword} {type.Name}"); + writer.WriteLine("{"); + writer.Indent++; + } + + // Generate methods for this type. + bool firstMethodInType = true; + foreach (var (methodSymbol, methodDeclaration, xmlDocs) in typeGroup) + { + AppendMethodDeclaration(writer, methodSymbol, methodDeclaration, xmlDocs, descriptionAttribute, firstMethodInType); + firstMethodInType = false; + } + + // Close all type declarations. + for (int i = 0; i < nestingCount; i++) + { + writer.Indent--; + writer.WriteLine("}"); + } + } + + private static void AppendMethodDeclaration( + IndentedTextWriter writer, + IMethodSymbol methodSymbol, + MethodDeclarationSyntax methodDeclaration, + XmlDocumentation? xmlDocs, + INamedTypeSymbol descriptionAttribute, + bool firstMethodInType) + { + if (!firstMethodInType) + { + writer.WriteLine(); + } + + // Add the Description attribute for method if needed and documentation exists + if (xmlDocs is not null && + !string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) && + !HasAttribute(methodSymbol, descriptionAttribute)) + { + writer.WriteLine($"[Description(\"{EscapeString(xmlDocs.MethodDescription)}\")]"); + } + + // Add return: Description attribute if needed and documentation exists + if (xmlDocs is not null && + !string.IsNullOrWhiteSpace(xmlDocs.Returns) && + methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute))) + { + writer.WriteLine($"[return: Description(\"{EscapeString(xmlDocs.Returns)}\")]"); + } + + // Copy modifiers from original method syntax. + // Add return type (without nullable annotations). + // Add method name. + writer.Write(string.Join(" ", methodDeclaration.Modifiers.Select(m => m.Text))); + writer.Write(' '); + writer.Write(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); + writer.Write(' '); + writer.Write(methodSymbol.Name); + + // Add parameters with their Description attributes. + writer.Write("("); + for (int i = 0; i < methodSymbol.Parameters.Length; i++) + { + IParameterSymbol param = methodSymbol.Parameters[i]; + + if (i > 0) + { + writer.Write(", "); + } + + if (xmlDocs is not null && + !HasAttribute(param, descriptionAttribute) && + xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) && + !string.IsNullOrWhiteSpace(paramDoc)) + { + writer.Write($"[Description(\"{EscapeString(paramDoc)}\")] "); + } + + writer.Write(param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); + writer.Write(' '); + writer.Write(param.Name); + } + writer.WriteLine(");"); + } + + /// Checks if a symbol has a specific attribute applied. + private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType) + { + foreach (var attr in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType)) + { + return true; + } + } + + return false; + } + + /// Escape special characters for C# string literals. + private static string EscapeString(string text) => + string.IsNullOrEmpty(text) ? text : + text.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", "\\t"); + + /// Checks if XML documentation would generate any Description attributes for a method. + private static bool HasGeneratableContent(XmlDocumentation xmlDocs, IMethodSymbol methodSymbol, INamedTypeSymbol descriptionAttribute) + { + // Check if method description would be generated + if (!string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) && !HasAttribute(methodSymbol, descriptionAttribute)) + { + return true; + } + + // Check if return description would be generated + if (!string.IsNullOrWhiteSpace(xmlDocs.Returns) && + methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute))) + { + return true; + } + + // Check if any parameter descriptions would be generated + foreach (var param in methodSymbol.Parameters) + { + if (!HasAttribute(param, descriptionAttribute) && + xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) && + !string.IsNullOrWhiteSpace(paramDoc)) + { + return true; + } + } + + return false; + } + + /// Represents a method that may need Description attributes generated. + private readonly record struct MethodToGenerate(MethodDeclarationSyntax MethodDeclaration, IMethodSymbol MethodSymbol); + + /// Holds extracted XML documentation for a method. + private sealed record XmlDocumentation(string MethodDescription, string Returns, Dictionary Parameters); +} diff --git a/src/ModelContextProtocol.Core/AIContentExtensions.cs b/src/ModelContextProtocol.Core/AIContentExtensions.cs index 8686b7b6a..374f00555 100644 --- a/src/ModelContextProtocol.Core/AIContentExtensions.cs +++ b/src/ModelContextProtocol.Core/AIContentExtensions.cs @@ -1,9 +1,11 @@ using Microsoft.Extensions.AI; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; #if !NET using System.Runtime.InteropServices; #endif using System.Text.Json; +using System.Text.Json.Nodes; namespace ModelContextProtocol; @@ -16,6 +18,140 @@ namespace ModelContextProtocol; /// public static class AIContentExtensions { + /// + /// 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 . + /// + /// + /// This method creates a function that converts MCP message requests into chat client calls, enabling + /// an MCP client to generate text or other content using an actual AI model via the provided chat client. + /// + /// + /// The handler can process text messages, image messages, resource messages, and tool use/results as defined in the + /// Model Context Protocol. + /// + /// + /// is . + public static Func, CancellationToken, ValueTask> CreateSamplingHandler( + this IChatClient chatClient) + { + Throw.IfNull(chatClient); + + return async (requestParams, progress, cancellationToken) => + { + Throw.IfNull(requestParams); + + var (messages, options) = ToChatClientArguments(requestParams); + var progressToken = requestParams.ProgressToken; + + List updates = []; + await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + updates.Add(update); + + if (progressToken is not null) + { + progress.Report(new() { Progress = updates.Count }); + } + } + + ChatResponse? chatResponse = updates.ToChatResponse(); + ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); + + IList? contents = lastMessage?.Contents.Select(c => c.ToContentBlock()).ToList(); + if (contents is not { Count: > 0 }) + { + (contents ??= []).Add(new TextContentBlock() { Text = "" }); + } + + return new() + { + Model = chatResponse.ModelId ?? "", + StopReason = + chatResponse.FinishReason == ChatFinishReason.Stop ? CreateMessageResult.StopReasonEndTurn : + chatResponse.FinishReason == ChatFinishReason.Length ? CreateMessageResult.StopReasonMaxTokens : + chatResponse.FinishReason == ChatFinishReason.ToolCalls ? CreateMessageResult.StopReasonToolUse : + chatResponse.FinishReason.ToString(), + Meta = chatResponse.AdditionalProperties?.ToJsonObject(), + Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, + Content = contents, + }; + + static (IList Messages, ChatOptions? Options) ToChatClientArguments(CreateMessageRequestParams requestParams) + { + ChatOptions? options = null; + + if (requestParams.MaxTokens is int maxTokens) + { + (options ??= new()).MaxOutputTokens = maxTokens; + } + + if (requestParams.Temperature is float temperature) + { + (options ??= new()).Temperature = temperature; + } + + if (requestParams.StopSequences is { } stopSequences) + { + (options ??= new()).StopSequences = stopSequences.ToArray(); + } + + if (requestParams.SystemPrompt is { } systemPrompt) + { + (options ??= new()).Instructions = systemPrompt; + } + + if (requestParams.Tools is { } tools) + { + foreach (var tool in tools) + { + ((options ??= new()).Tools ??= []).Add(new ToolAIFunctionDeclaration(tool)); + } + + if (options.Tools is { Count: > 0 } && requestParams.ToolChoice is { } toolChoice) + { + options.ToolMode = toolChoice.Mode switch + { + ToolChoice.ModeAuto => ChatToolMode.Auto, + ToolChoice.ModeRequired => ChatToolMode.RequireAny, + ToolChoice.ModeNone => ChatToolMode.None, + _ => null, + }; + } + } + + List messages = []; + foreach (var sm in requestParams.Messages) + { + if (sm.Content?.Select(b => b.ToAIContent()).OfType().ToList() is { Count: > 0 } aiContents) + { + messages.Add(new ChatMessage(sm.Role is Role.Assistant ? ChatRole.Assistant : ChatRole.User, aiContents)); + } + } + + return (messages, options); + } + }; + } + + /// Converts the specified dictionary to a . + internal static JsonObject? ToJsonObject(this IReadOnlyDictionary properties) => + JsonSerializer.SerializeToNode(properties, McpJsonUtilities.JsonContext.Default.IReadOnlyDictionaryStringObject) as JsonObject; + + internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj) + { + AdditionalPropertiesDictionary d = []; + foreach (var kvp in obj) + { + d.Add(kvp.Key, kvp.Value); + } + + return d; + } + /// /// Converts a to a object. /// @@ -99,7 +235,7 @@ public static IList ToPromptMessages(this ChatMessage chatMessage { if (content is TextContent or DataContent) { - messages.Add(new PromptMessage { Role = r, Content = content.ToContent() }); + messages.Add(new PromptMessage { Role = r, Content = content.ToContentBlock() }); } } @@ -122,13 +258,31 @@ public static IList ToPromptMessages(this ChatMessage chatMessage AIContent? ac = content switch { TextContentBlock textContent => new TextContent(textContent.Text), + ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType), + AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType), + EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(), + + ToolUseContentBlock toolUse => FunctionCallContent.CreateFromParsedArguments(toolUse.Input, toolUse.Id, toolUse.Name, + static json => JsonSerializer.Deserialize(json, McpJsonUtilities.JsonContext.Default.IDictionaryStringObject)), + + ToolResultContentBlock toolResult => new FunctionResultContent( + toolResult.ToolUseId, + toolResult.Content.Count == 1 ? toolResult.Content[0].ToAIContent() : toolResult.Content.Select(c => c.ToAIContent()).OfType().ToList()) + { + Exception = toolResult.IsError is true ? new() : null, + }, + _ => null, }; - ac?.RawRepresentation = content; + if (ac is not null) + { + ac.RawRepresentation = content; + ac.AdditionalProperties = content.Meta?.ToAdditionalProperties(); + } return ac; } @@ -200,8 +354,12 @@ public static IList ToAIContents(this IEnumerable c return [.. contents.Select(ToAIContent)]; } - internal static ContentBlock ToContent(this AIContent content) => - content switch + /// Creates a new from the content of an . + /// The to convert. + /// The created . + public static ContentBlock ToContentBlock(this AIContent content) + { + ContentBlock contentBlock = content switch { TextContent textContent => new TextContentBlock { @@ -230,9 +388,55 @@ internal static ContentBlock ToContent(this AIContent content) => } }, + FunctionCallContent callContent => new ToolUseContentBlock() + { + Id = callContent.CallId, + Name = callContent.Name, + Input = JsonSerializer.SerializeToElement(callContent.Arguments, McpJsonUtilities.DefaultOptions.GetTypeInfo>()!), + }, + + FunctionResultContent resultContent => new ToolResultContentBlock() + { + ToolUseId = resultContent.CallId, + IsError = resultContent.Exception is not null, + Content = + resultContent.Result is AIContent c ? [c.ToContentBlock()] : + resultContent.Result is IEnumerable ec ? [.. ec.Select(c => c.ToContentBlock())] : + [new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo()) }], + StructuredContent = resultContent.Result is JsonElement je ? je : null, + }, + _ => new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), } }; + + contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(); + + return contentBlock; + } + + private sealed class ToolAIFunctionDeclaration(Tool tool) : AIFunctionDeclaration + { + public override string Name => tool.Name; + + public override string Description => tool.Description ?? ""; + + public override IReadOnlyDictionary AdditionalProperties => + field ??= tool.Meta is { } meta ? meta.ToDictionary(p => p.Key, p => (object?)p.Value) : []; + + public override JsonElement JsonSchema => tool.InputSchema; + + public override JsonElement? ReturnJsonSchema => tool.OutputSchema; + + public override object? GetService(Type serviceType, object? serviceKey = null) + { + Throw.IfNull(serviceType); + + return + serviceKey is null && serviceType.IsInstanceOfType(tool) ? tool : + base.GetService(serviceType, serviceKey); + } + } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs deleted file mode 100644 index 86f77a900..000000000 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ /dev/null @@ -1,143 +0,0 @@ -using Microsoft.Extensions.AI; -using ModelContextProtocol.Protocol; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Text.Json; - -namespace ModelContextProtocol.Client; - -/// -/// Provides extension methods for interacting with an . -/// -/// -/// -/// This class contains extension methods that simplify common operations with an MCP client, -/// such as pinging a server, listing and working with tools, prompts, and resources, and -/// managing subscriptions to resources. -/// -/// -public static class McpClientExtensions -{ - /// - /// 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 . - /// - /// - /// This method creates a function that converts MCP message requests into chat client calls, enabling - /// an MCP client to generate text or other content using an actual AI model via the provided chat client. - /// - /// - /// The handler can process text messages, image messages, and resource messages as defined in the - /// Model Context Protocol. - /// - /// - /// is . - public static Func, CancellationToken, ValueTask> CreateSamplingHandler( - this IChatClient chatClient) - { - Throw.IfNull(chatClient); - - return async (requestParams, progress, cancellationToken) => - { - Throw.IfNull(requestParams); - - var (messages, options) = requestParams.ToChatClientArguments(); - var progressToken = requestParams.ProgressToken; - - List updates = []; - await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) - { - updates.Add(update); - - if (progressToken is not null) - { - progress.Report(new() - { - Progress = updates.Count, - }); - } - } - - return updates.ToChatResponse().ToCreateMessageResult(); - }; - } - - /// - /// Converts the contents of a into a pair of - /// and instances to use - /// as inputs into a operation. - /// - /// - /// The created pair of messages and options. - /// is . - internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( - this CreateMessageRequestParams requestParams) - { - Throw.IfNull(requestParams); - - ChatOptions? options = null; - - if (requestParams.MaxTokens is int maxTokens) - { - (options ??= new()).MaxOutputTokens = maxTokens; - } - - if (requestParams.Temperature is float temperature) - { - (options ??= new()).Temperature = temperature; - } - - if (requestParams.StopSequences is { } stopSequences) - { - (options ??= new()).StopSequences = stopSequences.ToArray(); - } - - List messages = - (from sm in requestParams.Messages - let aiContent = sm.Content.ToAIContent() - where aiContent is not null - select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) - .ToList(); - - return (messages, options); - } - - /// Converts the contents of a into a . - /// The whose contents should be extracted. - /// The created . - /// is . - internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chatResponse) - { - Throw.IfNull(chatResponse); - - // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports - // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one - // in any of the response messages, or we'll use all the text from them concatenated, otherwise. - - ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); - - ContentBlock? content = null; - if (lastMessage is not null) - { - foreach (var lmc in lastMessage.Contents) - { - if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) - { - content = dc.ToContent(); - } - } - } - - return new() - { - Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, - Model = chatResponse.ModelId ?? "unknown", - Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, - StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", - }; - } -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs index 6200cede3..c21ec7b48 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs @@ -80,7 +80,7 @@ public class McpClientHandlers /// generated content. /// /// - /// You can create a handler using the extension + /// You can create a handler using the extension /// method with any implementation of . /// /// diff --git a/src/ModelContextProtocol.Core/Diagnostics.cs b/src/ModelContextProtocol.Core/Diagnostics.cs index bed648868..083422d9c 100644 --- a/src/ModelContextProtocol.Core/Diagnostics.cs +++ b/src/ModelContextProtocol.Core/Diagnostics.cs @@ -13,7 +13,7 @@ internal static class Diagnostics internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol"); internal static Histogram CreateDurationHistogram(string name, string description, bool longBuckets) => - Meter.CreateHistogram(name, "s", description, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries); + Meter.CreateHistogram(name, "s", description, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries); /// /// Follows boundaries from http.server.request.duration/http.client.request.duration diff --git a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj index d39c008eb..cdbe25a2d 100644 --- a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj +++ b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj @@ -43,6 +43,23 @@ + + + + + + + + + + + diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index 012038f1d..af8efdba6 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.AI; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -37,7 +36,7 @@ private protected ContentBlock() /// When overridden in a derived class, gets the type of content. /// /// - /// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", and "resource_link". + /// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", "resource_link", "tool_use", and "tool_result". /// [JsonPropertyName("type")] public abstract string Type { get; } @@ -52,6 +51,15 @@ private protected ContentBlock() [JsonPropertyName("annotations")] public Annotations? Annotations { get; set; } + /// + /// Gets or sets metadata reserved by MCP for protocol-level metadata. + /// + /// + /// Implementations must not make assumptions about its contents. + /// + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } + /// /// Provides a for . /// @@ -84,6 +92,12 @@ public class Converter : JsonConverter ResourceContents? resource = null; Annotations? annotations = null; JsonObject? meta = null; + string? id = null; + JsonElement? input = null; + string? toolUseId = null; + List? content = null; + JsonElement? structuredContent = null; + bool? isError = null; while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { @@ -142,42 +156,71 @@ public class Converter : JsonConverter meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); break; + case "id": + id = reader.GetString(); + break; + + case "input": + input = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement); + break; + + case "toolUseId": + toolUseId = reader.GetString(); + break; + + case "content": + if (reader.TokenType == JsonTokenType.StartArray) + { + content = []; + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + content.Add(Read(ref reader, typeof(ContentBlock), options) ?? + throw new JsonException("Unexpected null item in content array.")); + } + } + else + { + content = [Read(ref reader, typeof(ContentBlock), options) ?? + throw new JsonException("Unexpected null content item.")]; + } + break; + + case "structuredContent": + structuredContent = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement); + break; + + case "isError": + isError = reader.GetBoolean(); + break; + default: reader.Skip(); break; } } - return type switch + ContentBlock block = type switch { "text" => new TextContentBlock { Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), - Annotations = annotations, - Meta = meta, }, "image" => new ImageContentBlock { Data = data ?? throw new JsonException("Image data must be provided for 'image' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'image' type."), - Annotations = annotations, - Meta = meta, }, "audio" => new AudioContentBlock { Data = data ?? throw new JsonException("Audio data must be provided for 'audio' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'audio' type."), - Annotations = annotations, - Meta = meta, }, "resource" => new EmbeddedResourceBlock { Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), - Annotations = annotations, - Meta = meta, }, "resource_link" => new ResourceLinkBlock @@ -187,11 +230,30 @@ public class Converter : JsonConverter Description = description, MimeType = mimeType, Size = size, - Annotations = annotations, + }, + + "tool_use" => new ToolUseContentBlock + { + Id = id ?? throw new JsonException("ID must be provided for 'tool_use' type."), + Name = name ?? throw new JsonException("Name must be provided for 'tool_use' type."), + Input = input ?? throw new JsonException("Input must be provided for 'tool_use' type."), + }, + + "tool_result" => new ToolResultContentBlock + { + ToolUseId = toolUseId ?? throw new JsonException("ToolUseId must be provided for 'tool_result' type."), + Content = content ?? throw new JsonException("Content must be provided for 'tool_result' type."), + StructuredContent = structuredContent, + IsError = isError, }, _ => throw new JsonException($"Unknown content type: '{type}'"), }; + + block.Annotations = annotations; + block.Meta = meta; + + return block; } /// @@ -211,41 +273,21 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial { case TextContentBlock textContent: writer.WriteString("text", textContent.Text); - if (textContent.Meta is not null) - { - writer.WritePropertyName("_meta"); - JsonSerializer.Serialize(writer, textContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); - } break; case ImageContentBlock imageContent: writer.WriteString("data", imageContent.Data); writer.WriteString("mimeType", imageContent.MimeType); - if (imageContent.Meta is not null) - { - writer.WritePropertyName("_meta"); - JsonSerializer.Serialize(writer, imageContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); - } break; case AudioContentBlock audioContent: writer.WriteString("data", audioContent.Data); writer.WriteString("mimeType", audioContent.MimeType); - if (audioContent.Meta is not null) - { - writer.WritePropertyName("_meta"); - JsonSerializer.Serialize(writer, audioContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); - } break; case EmbeddedResourceBlock embeddedResource: writer.WritePropertyName("resource"); JsonSerializer.Serialize(writer, embeddedResource.Resource, McpJsonUtilities.JsonContext.Default.ResourceContents); - if (embeddedResource.Meta is not null) - { - writer.WritePropertyName("_meta"); - JsonSerializer.Serialize(writer, embeddedResource.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); - } break; case ResourceLinkBlock resourceLink: @@ -264,6 +306,33 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial writer.WriteNumber("size", resourceLink.Size.Value); } break; + + case ToolUseContentBlock toolUse: + writer.WriteString("id", toolUse.Id); + writer.WriteString("name", toolUse.Name); + writer.WritePropertyName("input"); + JsonSerializer.Serialize(writer, toolUse.Input, McpJsonUtilities.JsonContext.Default.JsonElement); + break; + + case ToolResultContentBlock toolResult: + writer.WriteString("toolUseId", toolResult.ToolUseId); + writer.WritePropertyName("content"); + writer.WriteStartArray(); + foreach (var item in toolResult.Content) + { + Write(writer, item, options); + } + writer.WriteEndArray(); + if (toolResult.StructuredContent.HasValue) + { + writer.WritePropertyName("structuredContent"); + JsonSerializer.Serialize(writer, toolResult.StructuredContent.Value, McpJsonUtilities.JsonContext.Default.JsonElement); + } + if (toolResult.IsError.HasValue) + { + writer.WriteBoolean("isError", toolResult.IsError.Value); + } + break; } if (value.Annotations is { } annotations) @@ -272,6 +341,12 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial JsonSerializer.Serialize(writer, annotations, McpJsonUtilities.JsonContext.Default.Annotations); } + if (value.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, value.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } + writer.WriteEndObject(); } } @@ -288,15 +363,6 @@ public sealed class TextContentBlock : ContentBlock /// [JsonPropertyName("text")] public required string Text { get; set; } - - /// - /// Gets or sets metadata reserved by MCP for protocol-level metadata. - /// - /// - /// Implementations must not make assumptions about its contents. - /// - [JsonPropertyName("_meta")] - public JsonObject? Meta { get; set; } } /// Represents an image provided to or from an LLM. @@ -321,15 +387,6 @@ public sealed class ImageContentBlock : ContentBlock /// [JsonPropertyName("mimeType")] public required string MimeType { get; set; } - - /// - /// Gets or sets metadata reserved by MCP for protocol-level metadata. - /// - /// - /// Implementations must not make assumptions about its contents. - /// - [JsonPropertyName("_meta")] - public JsonObject? Meta { get; set; } } /// Represents audio provided to or from an LLM. @@ -354,15 +411,6 @@ public sealed class AudioContentBlock : ContentBlock /// [JsonPropertyName("mimeType")] public required string MimeType { get; set; } - - /// - /// Gets or sets metadata reserved by MCP for protocol-level metadata. - /// - /// - /// Implementations must not make assumptions about its contents. - /// - [JsonPropertyName("_meta")] - public JsonObject? Meta { get; set; } } /// Represents the contents of a resource, embedded into a prompt or tool call result. @@ -386,15 +434,6 @@ public sealed class EmbeddedResourceBlock : ContentBlock /// [JsonPropertyName("resource")] public required ResourceContents Resource { get; set; } - - /// - /// Gets or sets metadata reserved by MCP for protocol-level metadata. - /// - /// - /// Implementations must not make assumptions about its contents. - /// - [JsonPropertyName("_meta")] - public JsonObject? Meta { get; set; } } /// Represents a resource that the server is capable of reading, included in a prompt or tool call result. @@ -463,3 +502,76 @@ public sealed class ResourceLinkBlock : ContentBlock [JsonPropertyName("size")] public long? Size { get; set; } } + +/// Represents a request from the assistant to call a tool. +public sealed class ToolUseContentBlock : ContentBlock +{ + /// + public override string Type => "tool_use"; + + /// + /// Gets or sets a unique identifier for this tool use. + /// + /// + /// This ID is used to match tool results to their corresponding tool uses. + /// + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// + /// Gets or sets the name of the tool to call. + /// + [JsonPropertyName("name")] + public required string Name { get; set; } + + /// + /// Gets or sets the arguments to pass to the tool, conforming to the tool's input schema. + /// + [JsonPropertyName("input")] + public required JsonElement Input { get; set; } +} + +/// Represents the result of a tool use, provided by the user back to the assistant. +public sealed class ToolResultContentBlock : ContentBlock +{ + /// + public override string Type => "tool_result"; + + /// + /// Gets or sets the ID of the tool use this result corresponds to. + /// + /// + /// This must match the ID from a previous . + /// + [JsonPropertyName("toolUseId")] + public required string ToolUseId { get; set; } + + /// + /// Gets or sets the unstructured result content of the tool use. + /// + /// + /// This has the same format as CallToolResult.Content and can include text, images, + /// audio, resource links, and embedded resources. + /// + [JsonPropertyName("content")] + public required List Content { get; set; } + + /// + /// Gets or sets an optional structured result object. + /// + /// + /// If the tool defined an outputSchema, this should conform to that schema. + /// + [JsonPropertyName("structuredContent")] + public JsonElement? StructuredContent { get; set; } + + /// + /// Gets or sets whether the tool use resulted in an error. + /// + /// + /// If true, the content typically describes the error that occurred. + /// Default: false + /// + [JsonPropertyName("isError")] + public bool? IsError { get; set; } +} diff --git a/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs b/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs index 8894a3b3a..ec1bc2977 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs @@ -6,7 +6,14 @@ namespace ModelContextProtocol.Protocol; /// Specifies the context inclusion options for a request in the Model Context Protocol (MCP). /// /// +/// /// See the schema for details. +/// +/// +/// , and in particular and , are deprecated. +/// Servers should only use these values if the client declares with +/// set. These values may be removed in future spec releases. +/// /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum ContextInclusion @@ -20,12 +27,20 @@ public enum ContextInclusion /// /// Indicates that context from the server that sent the request should be included. /// + /// + /// This value is soft-deprecated. Servers should only use this value if the client + /// declares ClientCapabilities.Sampling.Context. + /// [JsonStringEnumMemberName("thisServer")] ThisServer, /// /// Indicates that context from all servers that the client is connected to should be included. /// + /// + /// This value is soft-deprecated. Servers should only use this value if the client + /// declares ClientCapabilities.Sampling.Context. + /// [JsonStringEnumMemberName("allServers")] AllServers } diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs index d3086c0be..c910053fb 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs @@ -16,7 +16,15 @@ public sealed class CreateMessageRequestParams : RequestParams /// Gets or sets an indication as to which server contexts should be included in the prompt. /// /// + /// /// The client may ignore this request. + /// + /// + /// , and in particular and + /// , are deprecated. Servers should only use these values if the client + /// declares with set. + /// These values may be removed in future spec releases. + /// /// [JsonPropertyName("includeContext")] public ContextInclusion? IncludeContext { get; set; } @@ -100,4 +108,16 @@ public sealed class CreateMessageRequestParams : RequestParams /// [JsonPropertyName("temperature")] public float? Temperature { get; set; } + + /// + /// Gets or sets tools that the model may use during generation. + /// + [JsonPropertyName("tools")] + public IList? Tools { get; set; } + + /// + /// Gets or sets controls for how the model uses tools. + /// + [JsonPropertyName("toolChoice")] + public ToolChoice? ToolChoice { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs index 7fada6399..d6891c747 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs @@ -11,10 +11,14 @@ namespace ModelContextProtocol.Protocol; public sealed class CreateMessageResult : Result { /// - /// Gets or sets the content of the message. + /// Gets or sets the content of the assistant's response. /// + /// + /// In the corresponding JSON, this may be a single content block or an array of content blocks. + /// [JsonPropertyName("content")] - public required ContentBlock Content { get; set; } + [JsonConverter(typeof(SingleItemOrListConverter))] + public required IList Content { get; set; } /// /// Gets or sets the name of the model that generated the message. @@ -35,12 +39,14 @@ public sealed class CreateMessageResult : Result /// Gets or sets the reason why message generation (sampling) stopped, if known. /// /// - /// Common values include: + /// Standard values include: /// /// endTurnThe model naturally completed its response. /// maxTokensThe response was truncated due to reaching token limits. /// stopSequenceA specific stop sequence was encountered during generation. + /// toolUseThe model wants to use one or more tools. /// + /// This field is an open string to allow for provider-specific stop reasons. /// [JsonPropertyName("stopReason")] public string? StopReason { get; set; } @@ -49,5 +55,17 @@ public sealed class CreateMessageResult : Result /// Gets or sets the role of the user who generated the message. /// [JsonPropertyName("role")] - public required Role Role { get; set; } + public Role Role { get; set; } = Role.Assistant; + + /// The stop reason "endTurn". + internal const string StopReasonEndTurn = "endTurn"; + + /// The stop reason "maxTokens". + internal const string StopReasonMaxTokens = "maxTokens"; + + /// The stop reason "stopSequence". + internal const string StopReasonStopSequence = "stopSequence"; + + /// The stop reason "toolUse". + internal const string StopReasonToolUse = "toolUse"; } diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index 50f9d4d51..74d0fd8a9 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -21,7 +21,10 @@ public sealed class ElicitRequestParams /// Gets or sets the requested schema. /// /// - /// May be one of , , , or . + /// May be one of , , , + /// , , + /// , , + /// or (deprecated). /// [JsonPropertyName("requestedSchema")] [field: MaybeNull] @@ -59,7 +62,10 @@ public IDictionary Properties /// /// Represents restricted subset of JSON Schema: - /// , , , or . + /// , , , + /// , , + /// , , + /// or (deprecated). /// [JsonConverter(typeof(Converter))] public abstract class PrimitiveSchemaDefinition @@ -113,8 +119,13 @@ public class Converter : JsonConverter bool? defaultBool = null; double? defaultNumber = null; string? defaultString = null; + IList? defaultStringArray = null; IList? enumValues = null; IList? enumNames = null; + IList? oneOf = null; + int? minItems = null; + int? maxItems = null; + object? items = null; // Can be UntitledEnumItemsSchema or TitledEnumItemsSchema while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { @@ -161,6 +172,14 @@ public class Converter : JsonConverter maximum = reader.GetDouble(); break; + case "minItems": + minItems = reader.GetInt32(); + break; + + case "maxItems": + maxItems = reader.GetInt32(); + break; + case "default": // We need to handle different types for default values // Store the value based on the JSON token type @@ -178,6 +197,9 @@ public class Converter : JsonConverter case JsonTokenType.String: defaultString = reader.GetString(); break; + case JsonTokenType.StartArray: + defaultStringArray = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); + break; } break; @@ -189,6 +211,14 @@ public class Converter : JsonConverter enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); break; + case "oneOf": + oneOf = DeserializeEnumOptions(ref reader); + break; + + case "items": + items = DeserializeEnumItemsSchema(ref reader); + break; + default: reader.Skip(); break; @@ -204,15 +234,39 @@ public class Converter : JsonConverter switch (type) { case "string": - if (enumValues is not null) + if (oneOf is not null) { - psd = new EnumSchema + // TitledSingleSelectEnumSchema + psd = new TitledSingleSelectEnumSchema { - Enum = enumValues, - EnumNames = enumNames, + OneOf = oneOf, Default = defaultString, }; } + else if (enumValues is not null) + { + if (enumNames is not null) + { + // EnumSchema for backward compatibility +#pragma warning disable CS0618 // Type or member is obsolete + psd = new EnumSchema +#pragma warning restore CS0618 // Type or member is obsolete + { + Enum = enumValues, + EnumNames = enumNames, + Default = defaultString, + }; + } + else + { + // UntitledSingleSelectEnumSchema + psd = new UntitledSingleSelectEnumSchema + { + Enum = enumValues, + Default = defaultString, + }; + } + } else { psd = new StringSchema @@ -225,6 +279,31 @@ public class Converter : JsonConverter } break; + case "array": + if (items is TitledEnumItemsSchema titledItems) + { + // TitledMultiSelectEnumSchema + psd = new TitledMultiSelectEnumSchema + { + MinItems = minItems, + MaxItems = maxItems, + Items = titledItems, + Default = defaultStringArray, + }; + } + else if (items is UntitledEnumItemsSchema untitledItems) + { + // UntitledMultiSelectEnumSchema + psd = new UntitledMultiSelectEnumSchema + { + MinItems = minItems, + MaxItems = maxItems, + Items = untitledItems, + Default = defaultStringArray, + }; + } + break; + case "integer": case "number": psd = new NumberSchema @@ -253,6 +332,108 @@ public class Converter : JsonConverter return psd; } + private static IList DeserializeEnumOptions(ref Utf8JsonReader reader) + { + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException("Expected array for oneOf property."); + } + + var options = new List(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected object in oneOf array."); + } + + string? constValue = null; + string? titleValue = null; + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "const": + constValue = reader.GetString(); + break; + case "title": + titleValue = reader.GetString(); + break; + default: + reader.Skip(); + break; + } + } + } + + if (constValue is null || titleValue is null) + { + throw new JsonException("Each option in oneOf must have both 'const' and 'title' properties."); + } + + options.Add(new EnumSchemaOption { Const = constValue, Title = titleValue }); + } + + return options; + } + + private static object DeserializeEnumItemsSchema(ref Utf8JsonReader reader) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected object for items property."); + } + + string? type = null; + IList? enumValues = null; + IList? anyOf = null; + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "type": + type = reader.GetString(); + break; + case "enum": + enumValues = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); + break; + case "anyOf": + anyOf = DeserializeEnumOptions(ref reader); + break; + default: + reader.Skip(); + break; + } + } + } + + // Determine which type to create based on the properties + if (anyOf is not null) + { + return new TitledEnumItemsSchema { AnyOf = anyOf }; + } + else if (enumValues is not null) + { + return new UntitledEnumItemsSchema { Type = type ?? "string", Enum = enumValues }; + } + else + { + throw new JsonException("Items schema must have either 'enum' or 'anyOf' property."); + } + } + /// public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition value, JsonSerializerOptions options) { @@ -317,20 +498,82 @@ public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition valu } break; - case EnumSchema enumSchema: - if (enumSchema.Enum is not null) + case UntitledSingleSelectEnumSchema untitledSingleSelect: + if (untitledSingleSelect.Enum is not null) + { + writer.WritePropertyName("enum"); + JsonSerializer.Serialize(writer, untitledSingleSelect.Enum, McpJsonUtilities.JsonContext.Default.IListString); + } + if (untitledSingleSelect.Default is not null) + { + writer.WriteString("default", untitledSingleSelect.Default); + } + break; + + case TitledSingleSelectEnumSchema titledSingleSelect: + if (titledSingleSelect.OneOf is not null && titledSingleSelect.OneOf.Count > 0) + { + writer.WritePropertyName("oneOf"); + SerializeEnumOptions(writer, titledSingleSelect.OneOf); + } + if (titledSingleSelect.Default is not null) + { + writer.WriteString("default", titledSingleSelect.Default); + } + break; + + case UntitledMultiSelectEnumSchema untitledMultiSelect: + if (untitledMultiSelect.MinItems.HasValue) + { + writer.WriteNumber("minItems", untitledMultiSelect.MinItems.Value); + } + if (untitledMultiSelect.MaxItems.HasValue) + { + writer.WriteNumber("maxItems", untitledMultiSelect.MaxItems.Value); + } + writer.WritePropertyName("items"); + SerializeUntitledEnumItemsSchema(writer, untitledMultiSelect.Items); + if (untitledMultiSelect.Default is not null) + { + writer.WritePropertyName("default"); + JsonSerializer.Serialize(writer, untitledMultiSelect.Default, McpJsonUtilities.JsonContext.Default.IListString); + } + break; + + case TitledMultiSelectEnumSchema titledMultiSelect: + if (titledMultiSelect.MinItems.HasValue) + { + writer.WriteNumber("minItems", titledMultiSelect.MinItems.Value); + } + if (titledMultiSelect.MaxItems.HasValue) + { + writer.WriteNumber("maxItems", titledMultiSelect.MaxItems.Value); + } + writer.WritePropertyName("items"); + SerializeTitledEnumItemsSchema(writer, titledMultiSelect.Items); + if (titledMultiSelect.Default is not null) + { + writer.WritePropertyName("default"); + JsonSerializer.Serialize(writer, titledMultiSelect.Default, McpJsonUtilities.JsonContext.Default.IListString); + } + break; + +#pragma warning disable CS0618 // Type or member is obsolete + case LegacyTitledEnumSchema legacyEnum: +#pragma warning restore CS0618 // Type or member is obsolete + if (legacyEnum.Enum is not null) { writer.WritePropertyName("enum"); - JsonSerializer.Serialize(writer, enumSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString); + JsonSerializer.Serialize(writer, legacyEnum.Enum, McpJsonUtilities.JsonContext.Default.IListString); } - if (enumSchema.EnumNames is not null) + if (legacyEnum.EnumNames is not null) { writer.WritePropertyName("enumNames"); - JsonSerializer.Serialize(writer, enumSchema.EnumNames, McpJsonUtilities.JsonContext.Default.IListString); + JsonSerializer.Serialize(writer, legacyEnum.EnumNames, McpJsonUtilities.JsonContext.Default.IListString); } - if (enumSchema.Default is not null) + if (legacyEnum.Default is not null) { - writer.WriteString("default", enumSchema.Default); + writer.WriteString("default", legacyEnum.Default); } break; @@ -340,6 +583,36 @@ public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition valu writer.WriteEndObject(); } + + private static void SerializeEnumOptions(Utf8JsonWriter writer, IList options) + { + writer.WriteStartArray(); + foreach (var option in options) + { + writer.WriteStartObject(); + writer.WriteString("const", option.Const); + writer.WriteString("title", option.Title); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + private static void SerializeUntitledEnumItemsSchema(Utf8JsonWriter writer, UntitledEnumItemsSchema itemsSchema) + { + writer.WriteStartObject(); + writer.WriteString("type", itemsSchema.Type); + writer.WritePropertyName("enum"); + JsonSerializer.Serialize(writer, itemsSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString); + writer.WriteEndObject(); + } + + private static void SerializeTitledEnumItemsSchema(Utf8JsonWriter writer, TitledEnumItemsSchema itemsSchema) + { + writer.WriteStartObject(); + writer.WritePropertyName("anyOf"); + SerializeEnumOptions(writer, itemsSchema.AnyOf); + writer.WriteEndObject(); + } } } @@ -467,8 +740,12 @@ public override string Type public bool? Default { get; set; } } - /// Represents a schema for an enum type. - public sealed class EnumSchema : PrimitiveSchemaDefinition + /// + /// Represents a legacy schema for an enum type with enumNames. + /// This schema is deprecated in favor of . + /// + [Obsolete("Use TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")] + public class LegacyTitledEnumSchema : PrimitiveSchemaDefinition { /// [JsonPropertyName("type")] @@ -505,4 +782,196 @@ public IList Enum [JsonPropertyName("default")] public string? Default { get; set; } } + + /// + /// Represents a schema for single-selection enumeration without display titles for options. + /// + public sealed class UntitledSingleSelectEnumSchema : PrimitiveSchemaDefinition + { + /// + [JsonPropertyName("type")] + public override string Type + { + get => "string"; + set + { + if (value is not "string") + { + throw new ArgumentException("Type must be 'string'.", nameof(value)); + } + } + } + + /// Gets or sets the list of allowed string values for the enum. + [JsonPropertyName("enum")] + [field: MaybeNull] + public IList Enum + { + get => field ??= []; + set + { + Throw.IfNull(value); + field = value; + } + } + + /// Gets or sets the default value for the enum. + [JsonPropertyName("default")] + public string? Default { get; set; } + } + + /// + /// Represents a single option in a titled enum schema with a constant value and display title. + /// + public sealed class EnumSchemaOption + { + /// Gets or sets the constant value for this option. + [JsonPropertyName("const")] + public required string Const { get; set; } + + /// Gets or sets the display title for this option. + [JsonPropertyName("title")] + public required string Title { get; set; } + } + + /// + /// Represents a schema for single-selection enumeration with display titles for each option. + /// + public sealed class TitledSingleSelectEnumSchema : PrimitiveSchemaDefinition + { + /// + [JsonPropertyName("type")] + public override string Type + { + get => "string"; + set + { + if (value is not "string") + { + throw new ArgumentException("Type must be 'string'.", nameof(value)); + } + } + } + + /// Gets or sets the list of enum options with their constant values and display titles. + [JsonPropertyName("oneOf")] + [field: MaybeNull] + public IList OneOf + { + get => field ??= []; + set + { + Throw.IfNull(value); + field = value; + } + } + + /// Gets or sets the default value for the enum. + [JsonPropertyName("default")] + public string? Default { get; set; } + } + + /// + /// Represents the items schema for untitled multi-select enum arrays. + /// + public sealed class UntitledEnumItemsSchema + { + /// Gets or sets the type of the items. + [JsonPropertyName("type")] + public string Type { get; set; } = "string"; + + /// Gets or sets the list of allowed string values. + [JsonPropertyName("enum")] + public required IList Enum { get; set; } + } + + /// + /// Represents the items schema for titled multi-select enum arrays. + /// + public sealed class TitledEnumItemsSchema + { + /// Gets or sets the list of enum options with constant values and display titles. + [JsonPropertyName("anyOf")] + public required IList AnyOf { get; set; } + } + + /// + /// Represents a schema for multiple-selection enumeration without display titles for options. + /// + public sealed class UntitledMultiSelectEnumSchema : PrimitiveSchemaDefinition + { + /// + [JsonPropertyName("type")] + public override string Type + { + get => "array"; + set + { + if (value is not "array") + { + throw new ArgumentException("Type must be 'array'.", nameof(value)); + } + } + } + + /// Gets or sets the minimum number of items that can be selected. + [JsonPropertyName("minItems")] + public int? MinItems { get; set; } + + /// Gets or sets the maximum number of items that can be selected. + [JsonPropertyName("maxItems")] + public int? MaxItems { get; set; } + + /// Gets or sets the schema for items in the array. + [JsonPropertyName("items")] + public required UntitledEnumItemsSchema Items { get; set; } + + /// Gets or sets the default values for the enum. + [JsonPropertyName("default")] + public IList? Default { get; set; } + } + + /// + /// Represents a schema for multiple-selection enumeration with display titles for each option. + /// + public sealed class TitledMultiSelectEnumSchema : PrimitiveSchemaDefinition + { + /// + [JsonPropertyName("type")] + public override string Type + { + get => "array"; + set + { + if (value is not "array") + { + throw new ArgumentException("Type must be 'array'.", nameof(value)); + } + } + } + + /// Gets or sets the minimum number of items that can be selected. + [JsonPropertyName("minItems")] + public int? MinItems { get; set; } + + /// Gets or sets the maximum number of items that can be selected. + [JsonPropertyName("maxItems")] + public int? MaxItems { get; set; } + + /// Gets or sets the schema for items in the array. + [JsonPropertyName("items")] + public required TitledEnumItemsSchema Items { get; set; } + + /// Gets or sets the default values for the enum. + [JsonPropertyName("default")] + public IList? Default { get; set; } + } + + /// + /// Represents a schema for an enum type. This is a compatibility alias for . + /// + [Obsolete("Use UntitledSingleSelectEnumSchema or TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")] + public sealed class EnumSchema : LegacyTitledEnumSchema + { + } } diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs index 1515be25c..5be40cf10 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs @@ -51,7 +51,7 @@ public sealed class ElicitResult : Result /// /// /// Values in the dictionary should be of types , , - /// , or . + /// , , or (for multi-select enums). /// /// [JsonPropertyName("content")] diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs index fa1773e42..361179831 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs @@ -1,7 +1,4 @@ -using System.ComponentModel; using System.Text.Json.Serialization; -using Microsoft.Extensions.AI; -using ModelContextProtocol.Client; namespace ModelContextProtocol.Protocol; @@ -14,14 +11,23 @@ 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. -/// -/// -/// This class is intentionally empty as the Model Context Protocol specification does not -/// currently define additional properties for sampling capabilities. Future versions of the -/// specification may extend this capability with additional configuration options. +/// using an AI model. The client must set a to process these requests. /// /// public sealed class SamplingCapability { + /// + /// Gets or sets whether the client supports context inclusion via includeContext parameter. + /// + /// + /// If not declared, servers should only use includeContext: "none". + /// + [JsonPropertyName("context")] + public SamplingContextCapability? Context { get; set; } + + /// + /// Gets or sets whether the client supports tool use via tools and toolChoice parameters. + /// + [JsonPropertyName("tools")] + public SamplingToolsCapability? Tools { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs new file mode 100644 index 000000000..bae960f3a --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs @@ -0,0 +1,6 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the sampling context capability. +/// +public sealed class SamplingContextCapability; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs index 60db179cc..093824e47 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -8,7 +9,8 @@ namespace ModelContextProtocol.Protocol; /// /// /// A encapsulates content sent to or received from AI models in the Model Context Protocol. -/// Each message has a specific role ( or ) and contains content which can be text or images. +/// The message has a role ( or ) and content which can be text, images, +/// audio, tool uses, or tool results. /// /// /// objects are typically used in collections within @@ -16,8 +18,9 @@ namespace ModelContextProtocol.Protocol; /// within the Model Context Protocol. /// /// -/// While similar to , the is focused on direct LLM sampling -/// operations rather than the enhanced resource embedding capabilities provided by . +/// If content contains any , then all content items +/// must be . Tool results cannot be mixed with text, image, or +/// audio content in the same message. /// /// /// See the schema for details. @@ -29,11 +32,21 @@ public sealed class SamplingMessage /// Gets or sets the content of the message. /// [JsonPropertyName("content")] - public required ContentBlock Content { get; set; } + [JsonConverter(typeof(SingleItemOrListConverter))] + public required IList Content { get; set; } /// - /// Gets or sets the role of the message sender, indicating whether it's from a "user" or an "assistant". + /// Gets or sets the role of the message sender. /// [JsonPropertyName("role")] - public required Role Role { get; set; } + public Role Role { get; set; } = Role.User; + + /// + /// Gets or sets metadata reserved by MCP for protocol-level metadata. + /// + /// + /// Implementations must not make assumptions about its contents. + /// + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs new file mode 100644 index 000000000..f93b79725 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs @@ -0,0 +1,6 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the sampling tools capability. +/// +public sealed class SamplingToolsCapability; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs b/src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs new file mode 100644 index 000000000..497062e7e --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs @@ -0,0 +1,67 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol; + +/// +/// JSON converter for that handles both array and single object representations. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class SingleItemOrListConverter : JsonConverter> + where T : class +{ + /// + public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType == JsonTokenType.StartArray) + { + List list = []; + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + if (JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(T))) is T item) + { + list.Add(item); + } + } + + return list; + } + + if (reader.TokenType == JsonTokenType.StartObject) + { + return JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(T))) is T item ? [item] : []; + } + + throw new JsonException($"Unexpected token type: {reader.TokenType}. Expected StartArray or StartObject."); + } + + /// + public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) + { + switch (value) + { + case null: + writer.WriteNullValue(); + return; + + case { Count: 1 }: + JsonSerializer.Serialize(writer, value[0], options.GetTypeInfo(typeof(object))); + return; + + default: + writer.WriteStartArray(); + foreach (var item in value) + { + JsonSerializer.Serialize(writer, item, options.GetTypeInfo(typeof(object))); + } + writer.WriteEndArray(); + return; + } + } +} diff --git a/src/ModelContextProtocol.Core/Protocol/ToolChoice.cs b/src/ModelContextProtocol.Core/Protocol/ToolChoice.cs new file mode 100644 index 000000000..41b387494 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/ToolChoice.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; + +namespace ModelContextProtocol.Protocol; + +/// +/// Controls tool selection behavior for sampling requests. +/// +public sealed class ToolChoice +{ + /// + /// Gets or sets the mode controlling which tools the model can call. + /// + /// + /// + /// "auto"Model decides whether to call tools (default) + /// "required"Model must call at least one tool + /// "none"Model must not call any tools + /// + /// + [JsonPropertyName("mode")] + public string? Mode { get; set; } + + /// The mode value "auto". + internal const string ModeAuto = "auto"; + + /// The mode value "required". + internal const string ModeRequired = "required"; + + /// The mode value "none". + internal const string ModeNone = "none"; +} + diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index e3d1271d0..f714bb184 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -249,7 +249,7 @@ public override async ValueTask InvokeAsync( { AIContent aiContent => new() { - Content = [aiContent.ToContent()], + Content = [aiContent.ToContentBlock()], StructuredContent = structuredContent, IsError = aiContent is ErrorContent }, @@ -491,7 +491,7 @@ private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumer foreach (var item in contentItems) { - contentList.Add(item.ToContent()); + contentList.Add(item.ToContentBlock()); hasAny = true; if (allErrorContent && item is not ErrorContent) diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index 877b9cda7..abccf2abb 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -104,42 +104,26 @@ public async Task SampleAsync( continue; } - if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) - { - Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; + Role role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User; - foreach (var content in message.Contents) + // Group all content blocks from this message into a single SamplingMessage + List contentBlocks = []; + foreach (var content in message.Contents) + { + if (content.ToContentBlock() is { } contentBlock) { - switch (content) - { - case TextContent textContent: - samplingMessages.Add(new() - { - Role = role, - Content = new TextContentBlock { Text = textContent.Text }, - }); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): - samplingMessages.Add(new() - { - Role = role, - Content = dataContent.HasTopLevelMediaType("image") ? - new ImageContentBlock - { - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - } : - new AudioContentBlock - { - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - }, - }); - break; - } + contentBlocks.Add(contentBlock); } } + + if (contentBlocks.Count > 0) + { + samplingMessages.Add(new() + { + Role = role, + Content = contentBlocks, + }); + } } ModelPreferences? modelPreferences = null; @@ -148,25 +132,63 @@ public async Task SampleAsync( modelPreferences = new() { Hints = [new() { Name = modelId }] }; } + IList? tools = null; + if (options?.Tools is { Count: > 0 }) + { + foreach (var tool in options.Tools) + { + if (tool is AIFunctionDeclaration af) + { + (tools ??= []).Add(new() + { + Name = af.Name, + Description = af.Description, + InputSchema = af.JsonSchema, + Meta = af.AdditionalProperties.ToJsonObject(), + }); + } + } + } + + ToolChoice? toolChoice = options?.ToolMode switch + { + NoneChatToolMode => new() { Mode = ToolChoice.ModeNone }, + AutoChatToolMode => new() { Mode = ToolChoice.ModeAuto }, + RequiredChatToolMode => new() { Mode = ToolChoice.ModeRequired }, + _ => null, + }; + var result = await SampleAsync(new() { - Messages = samplingMessages, MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, + Messages = samplingMessages, + ModelPreferences = modelPreferences, StopSequences = options?.StopSequences?.ToArray(), SystemPrompt = systemPrompt?.ToString(), Temperature = options?.Temperature, - ModelPreferences = modelPreferences, + ToolChoice = toolChoice, + Tools = tools, + Meta = options?.AdditionalProperties?.ToJsonObject(), }, cancellationToken).ConfigureAwait(false); - AIContent? responseContent = result.Content.ToAIContent(); + List responseContents = []; + foreach (var block in result.Content) + { + if (block.ToAIContent() is { } content) + { + responseContents.Add(content); + } + } - return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) + return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContents)) { ModelId = result.Model, FinishReason = result.StopReason switch { - "maxTokens" => ChatFinishReason.Length, - "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, + CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length, + CreateMessageResult.StopReasonToolUse => ChatFinishReason.ToolCalls, + CreateMessageResult.StopReasonEndTurn or CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop, + _ => null, } }; } diff --git a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs index dafdd7f7c..18272ffe6 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs @@ -46,7 +46,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are bound directly to the instance associated -/// with this request's . Such parameters may be used to understand +/// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs index 7d9f877a9..1dcf7bcf2 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs @@ -37,7 +37,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are bound directly to the instance associated -/// with this request's . Such parameters may be used to understand +/// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs index 3281bef16..752af4983 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs @@ -26,7 +26,7 @@ public sealed class McpServerPromptCreateOptions /// Gets or sets optional services used in the construction of the . /// /// - /// These services will be used to determine which parameters should be satisifed from dependency injection. As such, + /// These services will be used to determine which parameters should be satisfied from dependency injection. As such, /// what services are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. /// public IServiceProvider? Services { get; set; } @@ -46,7 +46,7 @@ public sealed class McpServerPromptCreateOptions public string? Title { get; set; } /// - /// Gets or set the description to use for the . + /// Gets or sets the description to use for the . /// /// /// If , but a is applied to the method, diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs index c52af48f0..34a08a18d 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs @@ -26,7 +26,7 @@ public sealed class McpServerResourceCreateOptions /// Gets or sets optional services used in the construction of the . /// /// - /// These services will be used to determine which parameters should be satisifed from dependency injection. As such, + /// These services will be used to determine which parameters should be satisfied from dependency injection. As such, /// what services are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. /// public IServiceProvider? Services { get; set; } @@ -51,12 +51,12 @@ public sealed class McpServerResourceCreateOptions public string? Name { get; set; } /// - /// Gets or sets the title to use for the . + /// Gets or sets the title to use for the . /// public string? Title { get; set; } /// - /// Gets or set the description to use for the . + /// Gets or sets the description to use for the . /// /// /// If , but a is applied to the member, diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index 6948ea912..987424b0d 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -99,7 +99,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// @@ -111,7 +111,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs index 9e71e0eab..cf5455587 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs @@ -90,7 +90,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// @@ -106,7 +106,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs index e4e1d9330..b029e5d43 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -41,7 +41,7 @@ public sealed class McpServerToolCreateOptions public string? Name { get; set; } /// - /// Gets or set the description to use for the . + /// Gets or sets the description to use for the . /// /// /// If , but a is applied to the method, diff --git a/tests/Common/Utils/TestServerTransport.cs b/tests/Common/Utils/TestServerTransport.cs index f875fe504..51682ba60 100644 --- a/tests/Common/Utils/TestServerTransport.cs +++ b/tests/Common/Utils/TestServerTransport.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Protocol; +using ModelContextProtocol.Protocol; using System.Text.Json; using System.Threading.Channels; @@ -74,7 +74,7 @@ private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cance await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), + Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model"}, McpJsonUtilities.DefaultOptions), }, cancellationToken); } diff --git a/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj b/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj new file mode 100644 index 000000000..430db1b02 --- /dev/null +++ b/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj @@ -0,0 +1,36 @@ + + + + net9.0 + enable + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs new file mode 100644 index 000000000..2439f894f --- /dev/null +++ b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs @@ -0,0 +1,1545 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Diagnostics.CodeAnalysis; +using Xunit; + +namespace ModelContextProtocol.Analyzers.Tests; + +public partial class XmlToDescriptionGeneratorTests +{ + [Fact] + public void Generator_WithSummaryOnly_GeneratesMethodDescription() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test tool description + /// + [McpServerTool] + public static partial string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test tool description")] + public static partial string TestMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithSummaryAndRemarks_CombinesInMethodDescription() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test tool summary + /// + /// + /// Additional remarks + /// + [McpServerTool] + public static partial string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test tool summary\nAdditional remarks")] + public static partial string TestMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithParameterDocs_GeneratesParameterDescriptions() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test tool + /// + /// Input parameter description + /// Count parameter description + [McpServerTool] + public static partial string TestMethod(string input, int count) + { + return input; + } + } + """); + + Assert.True(result.Success); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test tool")] + public static partial string TestMethod([Description("Input parameter description")] string input, [Description("Count parameter description")] int count); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithReturnDocs_GeneratesReturnDescription() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test tool + /// + /// The result of the operation + [McpServerTool] + public static partial string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test tool")] + [return: Description("The result of the operation")] + public static partial string TestMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithExistingMethodDescription_DoesNotGenerateMethodDescription() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test tool summary + /// + /// Result + [McpServerTool] + [Description("Already has description")] + public static partial string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [return: Description("Result")] + public static partial string TestMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithExistingParameterDescription_SkipsThatParameter() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test tool + /// + /// Input description + /// Count description + [McpServerTool] + public static partial string TestMethod(string input, [Description("Already has")] int count) + { + return input; + } + } + """); + + Assert.True(result.Success); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test tool")] + public static partial string TestMethod([Description("Input description")] string input, int count); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithoutMcpServerToolAttribute_DoesNotGenerate() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + public partial class TestTools + { + /// + /// Test tool + /// + public static partial string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + } + + [Fact] + public void Generator_WithoutPartialKeyword_DoesNotGenerate() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// + /// Test tool + /// + [McpServerTool] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + } + + [Fact] + public void Generator_NonPartialMethodWithXmlDocs_ReportsMCP002Diagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// + /// Test tool with documentation + /// + [McpServerTool] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should report MCP002 diagnostic + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("TestMethod", diagnostic.GetMessage()); + Assert.Contains("partial", diagnostic.GetMessage()); + } + + [Fact] + public void Generator_NonPartialMethodWithParameterDocs_ReportsMCP002Diagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// Input parameter + [McpServerTool] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should report MCP002 diagnostic because parameter has documentation + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [Fact] + public void Generator_NonPartialMethodWithReturnDocs_ReportsMCP002Diagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// Return value + [McpServerTool] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should report MCP002 diagnostic because return has documentation + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [Fact] + public void Generator_NonPartialMethodWithoutXmlDocs_DoesNotReportDiagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + [McpServerTool] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should NOT report MCP002 diagnostic because there's no XML documentation + Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); + } + + [Fact] + public void Generator_NonPartialMethodWithEmptyXmlDocs_DoesNotReportDiagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// + [McpServerTool] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should NOT report MCP002 diagnostic because XML documentation is empty + Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); + } + + [Fact] + public void Generator_NonPartialMethodWithExistingDescriptions_DoesNotReportDiagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// Test tool + /// Input param + /// Return value + [McpServerTool] + [Description("Already has method description")] + [return: Description("Already has return description")] + public static string TestMethod([Description("Already has param description")] string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should NOT report MCP002 diagnostic because all descriptions already exist + Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); + } + + [Fact] + public void Generator_NonPartialMethodWithPartialExistingDescriptions_ReportsMCP002Diagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public class TestTools + { + /// Test tool + /// Input param + [McpServerTool] + [Description("Already has method description")] + public static string TestMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should report MCP002 diagnostic because parameter description would be generated + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [Fact] + public void Generator_NonPartialPromptWithXmlDocs_ReportsMCP002Diagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerPromptType] + public class TestPrompts + { + /// + /// Test prompt + /// + [McpServerPrompt] + public static string TestPrompt(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should report MCP002 diagnostic for prompts too + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [Fact] + public void Generator_NonPartialResourceWithXmlDocs_ReportsMCP002Diagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerResourceType] + public class TestResources + { + /// + /// Test resource + /// + [McpServerResource("test://resource")] + public static string TestResource(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Empty(result.GeneratedSources); + + // Should report MCP002 diagnostic for resources too + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [Fact] + public void Generator_WithSpecialCharacters_EscapesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test with "quotes", \backslash, newline + /// and tab characters. + /// + /// Parameter with "quotes" + [McpServerTool] + public static partial string TestEscaping(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test with \"quotes\", \\backslash, newline and tab characters.")] + public static partial string TestEscaping([Description("Parameter with \"quotes\"")] string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithInvalidXml_GeneratesPartialAndReportsDiagnostic() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test with + [McpServerTool] + public static partial string TestInvalidXml(string input) + { + return input; + } + } + """); + + // Should not throw, generates partial implementation without Description attributes + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + public static partial string TestInvalidXml(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + + // Should report a warning diagnostic + var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP001"); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + Assert.Contains("invalid", diagnostic.GetMessage(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Generator_WithGenericType_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Test generic + /// + [McpServerTool] + public static partial string TestGeneric(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Test generic")] + public static partial string TestGeneric(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithEmptyXmlComments_GeneratesPartialWithoutDescription() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// + [McpServerTool] + public static partial string TestEmpty(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + public static partial string TestEmpty(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithMultilineComments_CombinesIntoSingleLine() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// First line + /// Second line + /// Third line + /// + [McpServerTool] + public static partial string TestMultiline(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("First line Second line Third line")] + public static partial string TestMultiline(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithParametersOnly_GeneratesParameterDescriptions() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// Input parameter + /// Count parameter + [McpServerTool] + public static partial string TestMethod(string input, int count) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + public static partial string TestMethod([Description("Input parameter")] string input, [Description("Count parameter")] int count); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithNestedType_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + public partial class OuterClass + { + [McpServerToolType] + public partial class InnerClass + { + /// + /// Nested tool + /// + [McpServerTool] + public static partial string NestedMethod(string input) + { + return input; + } + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class OuterClass + { + partial class InnerClass + { + [Description("Nested tool")] + public static partial string NestedMethod(string input); + } + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithManyToolsAcrossMultipleNestedTypes_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test.Outer; + + [McpServerToolType] + public partial class RootTools + { + /// Root level tool 1 + [McpServerTool] + public static partial string RootTool1(string input) => input; + + /// Root level tool 2 + /// The count + [McpServerTool] + public static partial string RootTool2(string input, int count) => input; + } + + public partial class OuterContainer + { + [McpServerToolType] + public partial class Level2A + { + /// Level 2A tool + [McpServerTool] + public static partial string Level2ATool(string input) => input; + + public partial class Level3 + { + [McpServerToolType] + public partial class Level4 + { + /// Deep nested tool + /// The result + [McpServerTool] + public static partial string DeepTool(string input) => input; + } + } + } + + [McpServerToolType] + public partial class Level2B + { + /// Level 2B tool 1 + [McpServerTool] + public static partial string Level2BTool1(string input) => input; + + /// Level 2B tool 2 + [McpServerTool] + public static partial string Level2BTool2(string input) => input; + } + } + + namespace Test.Resources; + + [McpServerResourceType] + public partial class ResourceProviders + { + /// Test resource 1 + /// The path + [McpServerResource("test:///{path}")] + public static partial string Resource1(string path) => path; + + /// Test resource 2 + [McpServerResource("test2:///{id}")] + public static partial string Resource2(string id) => id; + } + + [McpServerPromptType] + public partial class GlobalPrompts + { + /// Global prompt + [McpServerPrompt] + public static partial string GlobalPrompt(string input) => input; + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test.Outer + { + partial class RootTools + { + [Description("Root level tool 1")] + public static partial string RootTool1(string input); + + [Description("Root level tool 2")] + public static partial string RootTool2(string input, [Description("The count")] int count); + } + + partial class OuterContainer + { + partial class Level2A + { + [Description("Level 2A tool")] + public static partial string Level2ATool(string input); + } + } + + partial class OuterContainer + { + partial class Level2A + { + partial class Level3 + { + partial class Level4 + { + [Description("Deep nested tool")] + [return: Description("The result")] + public static partial string DeepTool(string input); + } + } + } + } + + partial class OuterContainer + { + partial class Level2B + { + [Description("Level 2B tool 1")] + public static partial string Level2BTool1(string input); + + [Description("Level 2B tool 2")] + public static partial string Level2BTool2(string input); + } + } + } + + namespace Test.Outer.Test.Resources + { + partial class GlobalPrompts + { + [Description("Global prompt")] + public static partial string GlobalPrompt(string input); + } + + partial class ResourceProviders + { + [Description("Test resource 1")] + public static partial string Resource1([Description("The path")] string path); + + [Description("Test resource 2")] + public static partial string Resource2(string id); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithRecordClass_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial record TestTools + { + /// + /// Record tool + /// + [McpServerTool] + public static partial string RecordMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial record class TestTools + { + [Description("Record tool")] + public static partial string RecordMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithRecordStruct_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial record struct TestTools + { + /// + /// Record struct tool + /// + [McpServerTool] + public static partial string RecordStructMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial record struct TestTools + { + [Description("Record struct tool")] + public static partial string RecordStructMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithVirtualMethod_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public partial class TestTools + { + /// + /// Virtual tool + /// + [McpServerTool] + public virtual partial string VirtualMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Virtual tool")] + public virtual partial string VirtualMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithAbstractMethod_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerToolType] + public abstract partial class TestTools + { + /// + /// Abstract tool + /// + [McpServerTool] + public abstract partial string AbstractMethod(string input); + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestTools + { + [Description("Abstract tool")] + public abstract partial string AbstractMethod(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithMcpServerPrompt_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerPromptType] + public partial class TestPrompts + { + /// + /// Test prompt + /// + [McpServerPrompt] + public static partial string TestPrompt(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestPrompts + { + [Description("Test prompt")] + public static partial string TestPrompt(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithMcpServerResource_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + namespace Test; + + [McpServerResourceType] + public partial class TestResources + { + /// + /// Test resource + /// + [McpServerResource("test://resource")] + public static partial string TestResource(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + namespace Test + { + partial class TestResources + { + [Description("Test resource")] + public static partial string TestResource(string input); + } + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + [Fact] + public void Generator_WithGlobalNamespace_GeneratesCorrectly() + { + var result = RunGenerator(""" + using ModelContextProtocol.Server; + using System.ComponentModel; + + [McpServerToolType] + public partial class GlobalTools + { + /// + /// Tool in global namespace + /// + [McpServerTool] + public static partial string GlobalMethod(string input) + { + return input; + } + } + """); + + Assert.True(result.Success); + Assert.Single(result.GeneratedSources); + + var expected = $$""" + // + // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} + + #pragma warning disable + + using System.ComponentModel; + using ModelContextProtocol.Server; + + partial class GlobalTools + { + [Description("Tool in global namespace")] + public static partial string GlobalMethod(string input); + } + """; + + AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); + } + + private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source); + + // Get reference assemblies - we need to include all the basic runtime types + List referenceList = + [ + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).Assembly.Location), + ]; + + // Add all necessary runtime assemblies + var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll"))); + referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll"))); + + // Try to find and add ModelContextProtocol.Core + try + { + var coreAssemblyPath = Path.Combine(AppContext.BaseDirectory, "ModelContextProtocol.Core.dll"); + if (File.Exists(coreAssemblyPath)) + { + referenceList.Add(MetadataReference.CreateFromFile(coreAssemblyPath)); + } + } + catch + { + // If we can't find it, the compilation will fail with appropriate errors + } + + var compilation = CSharpCompilation.Create( + "TestAssembly", + [syntaxTree], + referenceList, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = (CSharpGeneratorDriver)CSharpGeneratorDriver + .Create(new XmlToDescriptionGenerator()) + .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + + var runResult = driver.GetRunResult(); + + return new GeneratorRunResult + { + Success = !diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), + GeneratedSources = runResult.GeneratedTrees.Select(t => (t.FilePath, t.GetText())).ToList(), + Diagnostics = diagnostics.ToList(), + Compilation = outputCompilation + }; + } + + private static void AssertGeneratedSourceEquals( + [StringSyntax("C#-test")] string expected, + [StringSyntax("C#-test")] string actual) + { + // Normalize line endings to \n, remove trailing whitespace from each line, and trim the end + static string Normalize(string s) + { + var lines = s.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); + for (int i = 0; i < lines.Length; i++) + { + lines[i] = lines[i].TrimEnd(); + } + return string.Join('\n', lines).TrimEnd(); + } + + var normalizedExpected = Normalize(expected); + var normalizedActual = Normalize(actual); + + Assert.Equal(normalizedExpected, normalizedActual); + } + + private class GeneratorRunResult + { + public bool Success { get; set; } + public List<(string FilePath, Microsoft.CodeAnalysis.Text.SourceText SourceText)> GeneratedSources { get; set; } = []; + public List Diagnostics { get; set; } = []; + public Compilation? Compilation { get; set; } + } +} diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 78acaeb5e..acf5d469e 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Client; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; @@ -259,8 +259,7 @@ public async Task Sampling_Sse_TestServer() return new CreateMessageResult { Model = "test-model", - Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, + Content = [new TextContentBlock { Text = "Test response" }], }; }; await using var client = await GetClientAsync(options); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 0c71e56e3..2899851ef 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -143,7 +143,7 @@ public async Task ClaimsPrincipal_CanBeInjected_IntoToolMethod() } [Fact] - public async Task Sampling_DoesNotCloseStream_Prematurely() + public async Task Sampling_DoesNotCloseStreamPrematurely() { Assert.SkipWhen(Stateless, "Sampling is not supported in stateless mode."); @@ -172,14 +172,14 @@ public async Task Sampling_DoesNotCloseStream_Prematurely() 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); + Assert.Equal("Test prompt for sampling", Assert.IsType(Assert.Single(message.Content)).Text); sampleCount++; return new CreateMessageResult { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Sampling response from client" }, + Content = [new TextContentBlock { Text = "Sampling response from client" }], }; } } @@ -285,7 +285,7 @@ public static async Task SamplingToolAsync(McpServer server, string prom new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = prompt }, + Content = [new TextContentBlock { Text = prompt }], } ], MaxTokens = 1000 @@ -294,7 +294,7 @@ public static async Task SamplingToolAsync(McpServer server, string prom await server.SampleAsync(samplingRequest, cancellationToken); var samplingResult = await server.SampleAsync(samplingRequest, cancellationToken); - return $"Sampling completed successfully. Client responded: {Assert.IsType(samplingResult.Content).Text}"; + return $"Sampling completed successfully. Client responded: {Assert.IsType(Assert.Single(samplingResult.Content)).Text}"; } } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj index 19f4e4149..5d21d0a0a 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj @@ -4,7 +4,8 @@ net10.0;net9.0;net8.0 enable enable - Exe + false + true ModelContextProtocol.AspNetCore.Tests @@ -13,25 +14,41 @@ false + + + true + + - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index 9a54ed71d..e4b797ee6 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -197,7 +197,7 @@ private static void ConfigureTools(McpServerOptions options, string? cliArg) return new CallToolResult { - Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] + Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}" }] }; } else if (request.Params?.Name == "echoCliArg") @@ -521,7 +521,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, + Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index 183a64e7e..53537c2b8 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Serilog; @@ -46,7 +46,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, + Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, @@ -191,7 +191,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st return new CallToolResult { - Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] + Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}" }] }; } else @@ -339,7 +339,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st }); messages.Add(new PromptMessage { - Role = Role.Assistant, + Role = Role.User, 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 diff --git a/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs b/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs index ec603c63f..3a57a07c6 100644 --- a/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs @@ -25,4 +25,127 @@ public void CallToolResult_ToChatMessage_ProducesExpectedAIContent() JsonElement result = Assert.IsType(frc.Result); Assert.Contains("This is a test message.", result.ToString()); } + + [Fact] + public void ToAIContent_ConvertsToolUseContentBlock() + { + Dictionary inputDict = new() { ["city"] = "Paris", ["units"] = "metric" }; + ToolUseContentBlock toolUse = new() + { + Id = "call_abc123", + Name = "get_weather", + Input = JsonSerializer.SerializeToElement(inputDict, McpJsonUtilities.DefaultOptions) + }; + + AIContent? aiContent = toolUse.ToAIContent(); + + var functionCall = Assert.IsType(aiContent); + Assert.Equal("call_abc123", functionCall.CallId); + Assert.Equal("get_weather", functionCall.Name); + Assert.NotNull(functionCall.Arguments); + + var cityArg = Assert.IsType(functionCall.Arguments["city"]); + Assert.Equal("Paris", cityArg.GetString()); + var unitsArg = Assert.IsType(functionCall.Arguments["units"]); + Assert.Equal("metric", unitsArg.GetString()); + } + + [Fact] + public void ToAIContent_ConvertsToolResultContentBlock() + { + ToolResultContentBlock toolResult = new() + { + ToolUseId = "call_abc123", + Content = [new TextContentBlock { Text = "Weather: 18°C" }], + IsError = false + }; + + AIContent? aiContent = toolResult.ToAIContent(); + + var functionResult = Assert.IsType(aiContent); + Assert.Equal("call_abc123", functionResult.CallId); + Assert.Null(functionResult.Exception); + Assert.NotNull(functionResult.Result); + } + + [Fact] + public void ToAIContent_ConvertsToolResultContentBlockWithError() + { + ToolResultContentBlock toolResult = new() + { + ToolUseId = "call_abc123", + Content = [new TextContentBlock { Text = "Error: Invalid city" }], + IsError = true + }; + + AIContent? aiContent = toolResult.ToAIContent(); + + var functionResult = Assert.IsType(aiContent); + Assert.Equal("call_abc123", functionResult.CallId); + Assert.NotNull(functionResult.Exception); + } + + [Fact] + public void ToAIContent_ConvertsToolResultWithMultipleContent() + { + ToolResultContentBlock toolResult = new() + { + ToolUseId = "call_123", + Content = + [ + new TextContentBlock { Text = "Text result" }, + new ImageContentBlock { Data = Convert.ToBase64String([1, 2, 3]), MimeType = "image/png" } + ] + }; + + AIContent? aiContent = toolResult.ToAIContent(); + + var functionResult = Assert.IsType(aiContent); + Assert.Equal("call_123", functionResult.CallId); + + var resultList = Assert.IsAssignableFrom>(functionResult.Result); + Assert.Equal(2, resultList.Count); + Assert.IsType(resultList[0]); + Assert.IsType(resultList[1]); + } + + [Fact] + public void ToAIContent_ToolUseToFunctionCallRoundTrip() + { + Dictionary inputDict = new() { ["param1"] = "value1", ["param2"] = 42 }; + ToolUseContentBlock original = new() + { + Id = "call_123", + Name = "test_tool", + Input = JsonSerializer.SerializeToElement(inputDict, McpJsonUtilities.DefaultOptions) + }; + + var functionCall = Assert.IsType(original.ToAIContent()); + + Assert.Equal("call_123", functionCall.CallId); + Assert.Equal("test_tool", functionCall.Name); + Assert.NotNull(functionCall.Arguments); + + var param1 = Assert.IsType(functionCall.Arguments["param1"]); + Assert.Equal("value1", param1.GetString()); + var param2 = Assert.IsType(functionCall.Arguments["param2"]); + Assert.Equal(42, param2.GetInt32()); + } + + [Fact] + public void ToAIContent_ToolResultToFunctionResultRoundTrip() + { + ToolResultContentBlock original = new() + { + ToolUseId = "call_123", + Content = [new TextContentBlock { Text = "Result" }, new TextContentBlock { Text = "More data" }], + IsError = false + }; + + var functionResult = Assert.IsType(original.ToAIContent()); + + Assert.Equal("call_123", functionResult.CallId); + Assert.False(functionResult.Exception != null); + Assert.NotNull(functionResult.Result); + } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs index 0eb84262b..504b52e21 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs @@ -73,10 +73,10 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }, SamplingHandler = async (c, p, t) => new CreateMessageResult { - Content = new TextContentBlock { Text = "result" }, + Content = [new TextContentBlock { Text = "result" }], Model = "test-model", Role = Role.User, - StopReason = "endTurn" + StopReason = "endTurn", } } }; diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index 09be7385e..604c31b8b 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -5,6 +5,7 @@ using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Moq; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading.Channels; @@ -106,10 +107,10 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat { Messages = [ - new SamplingMessage + new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = "Hello" } + Content = [new TextContentBlock { Text = "Hello" }] } ], Temperature = temperature, @@ -134,14 +135,14 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) .Returns(expectedResponse); - var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); + var handler = mockChatClient.Object.CreateSamplingHandler(); // Act var result = await handler(requestParams, Mock.Of>(), cancellationToken); // Assert Assert.NotNull(result); - Assert.Equal("Hello, World!", (result.Content as TextContentBlock)?.Text); + Assert.Equal("Hello, World!", result.Content.OfType().FirstOrDefault()?.Text); Assert.Equal("test-model", result.Model); Assert.Equal(Role.Assistant, result.Role); Assert.Equal("endTurn", result.StopReason); @@ -156,14 +157,14 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages() { Messages = [ - new SamplingMessage + new SamplingMessage { Role = Role.User, - Content = new ImageContentBlock + Content = [new ImageContentBlock { MimeType = "image/png", Data = Convert.ToBase64String(new byte[] { 1, 2, 3 }) - } + }], } ], MaxTokens = 100 @@ -188,14 +189,14 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages() .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) .Returns(expectedResponse); - var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); + var handler = mockChatClient.Object.CreateSamplingHandler(); // Act var result = await handler(requestParams, Mock.Of>(), cancellationToken); // Assert Assert.NotNull(result); - Assert.Equal(expectedData, (result.Content as ImageContentBlock)?.Data); + Assert.Equal(expectedData, result.Content.OfType().FirstOrDefault()?.Data); Assert.Equal("test-model", result.Model); Assert.Equal(Role.Assistant, result.Role); Assert.Equal("endTurn", result.StopReason); @@ -222,7 +223,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() new SamplingMessage { Role = Role.User, - Content = new EmbeddedResourceBlock { Resource = resource }, + Content = [new EmbeddedResourceBlock { Resource = resource }], } ], MaxTokens = 100 @@ -247,7 +248,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) .Returns(expectedResponse); - var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); + var handler = mockChatClient.Object.CreateSamplingHandler(); // Act var result = await handler(requestParams, Mock.Of>(), cancellationToken); @@ -542,4 +543,120 @@ public async Task ReturnsNegotiatedProtocolVersion(string? protocolVersion) await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = protocolVersion }); Assert.Equal(protocolVersion ?? "2025-06-18", client.NegotiatedProtocolVersion); } + + [Fact] + public async Task EndToEnd_SamplingWithTools_ServerUsesIChatClientWithFunctionInvocation_ClientHandlesSamplingWithIChatClient() + { + int getWeatherToolCallCount = 0; + int askClientToolCallCount = 0; + + Server.ServerOptions.ToolCollection?.Add(McpServerTool.Create( + async (McpServer server, string query, CancellationToken cancellationToken) => + { + askClientToolCallCount++; + + var weatherTool = AIFunctionFactory.Create( + (string location) => + { + getWeatherToolCallCount++; + return $"Weather in {location}: sunny, 22°C"; + }, + "get_weather", "Gets the weather for a location"); + + var response = await server + .AsSamplingChatClient() + .AsBuilder() + .UseFunctionInvocation() + .Build() + .GetResponseAsync(query, new ChatOptions { Tools = [weatherTool] }, cancellationToken); + + return response.Text ?? "No response"; + }, + new() { Name = "ask_client", Description = "Asks the client a question using sampling" })); + + int samplingCallCount = 0; + TestChatClient testChatClient = new((messages, options, ct) => + { + int currentCall = samplingCallCount++; + var lastMessage = messages.LastOrDefault(); + + // First call: Return a tool call request for get_weather + if (currentCall == 0) + { + return Task.FromResult(new([ + new ChatMessage(ChatRole.User, messages.First().Contents), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_weather_123", "get_weather", new Dictionary { ["location"] = "Paris" })]) + ]) + { + ModelId = "test-model", + FinishReason = ChatFinishReason.ToolCalls + }); + } + // Second call (after tool result): Return final text response + else + { + var toolResult = lastMessage?.Contents.OfType().FirstOrDefault(); + Assert.NotNull(toolResult); + Assert.Equal("call_weather_123", toolResult.CallId); + + string resultText = toolResult.Result?.ToString() ?? string.Empty; + Assert.Contains("Weather in Paris: sunny", resultText); + + return Task.FromResult(new([ + new ChatMessage(ChatRole.User, messages.First().Contents), + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_weather_123", "get_weather", new Dictionary { ["location"] = "Paris" })]), + new ChatMessage(ChatRole.User, [toolResult]), + new ChatMessage(ChatRole.Assistant, [new TextContent($"Based on the weather data: {resultText}")]) + ]) + { + ModelId = "test-model", + FinishReason = ChatFinishReason.Stop + }); + } + }); + + await using McpClient client = await CreateMcpClientForServer(new() + { + Handlers = new() { SamplingHandler = testChatClient.CreateSamplingHandler() }, + }); + + var result = await client.CallToolAsync( + "ask_client", + new Dictionary { ["query"] = "What's the weather in Paris?" }, + cancellationToken: TestContext.Current.CancellationToken); + Assert.NotNull(result); + Assert.Null(result.IsError); + + var textContent = result.Content.OfType().FirstOrDefault(); + Assert.NotNull(textContent); + Assert.Contains("Weather in Paris: sunny, 22", textContent.Text); + Assert.Equal(1, getWeatherToolCallCount); + Assert.Equal(1, askClientToolCallCount); + Assert.Equal(2, samplingCallCount); + } + + /// Simple test IChatClient implementation for testing. + private sealed class TestChatClient(Func, ChatOptions?, CancellationToken, Task> getResponse) : IChatClient + { + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) => + getResponse(messages, options, cancellationToken); + + async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + foreach (var update in response.ToChatResponseUpdates()) + { + yield return update; + } + } + + object? IChatClient.GetService(Type serviceType, object? serviceKey) => null; + void IDisposable.Dispose() { } + } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs index 45d8a467f..7f1cc3689 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs @@ -352,13 +352,10 @@ public async Task ResourceLinkTool_ReturnsJsonElement() Assert.IsType(result); var jsonElement = (JsonElement)result!; - Assert.True(jsonElement.TryGetProperty("content", out var contentArray)); - Assert.Equal(JsonValueKind.Array, contentArray.ValueKind); - Assert.Equal(1, contentArray.GetArrayLength()); + Assert.True(jsonElement.TryGetProperty("content", out var contentValue)); + Assert.Equal(JsonValueKind.Array, contentValue.ValueKind); - var firstContent = contentArray[0]; - Assert.True(firstContent.TryGetProperty("type", out var typeProperty)); - Assert.Equal("resource_link", typeProperty.GetString()); + Assert.Equal(1, contentValue.GetArrayLength()); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 16fad124a..ff6f56e24 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; @@ -383,7 +383,7 @@ public async Task Sampling_Stdio(string clientId) { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, + Content = [new TextContentBlock { Text = "Test response" }], }; } } diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index 2d5ef5f2d..31a8236f2 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Client; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; @@ -81,7 +81,7 @@ public async Task Sampling_Sse_EverythingServer() { Model = "test-model", Role = Role.Assistant, - Content = new TextContentBlock { Text = "Test response" }, + Content = [new TextContentBlock { Text = "Test response" }], }; } } diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj index 6bdca3438..425944624 100644 --- a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj +++ b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj @@ -6,6 +6,8 @@ enable enable + false + true ModelContextProtocol.Tests $(NoWarn);NU1903;NU1902 @@ -16,6 +18,13 @@ false + + + true + + @@ -26,20 +35,29 @@ - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs index 3d8d8ff18..0113b77f3 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; using System.Text.Json; @@ -125,4 +124,105 @@ public void Deserialize_IgnoresUnknownObjectProperties() var textBlock = Assert.IsType(contentBlock); Assert.Contains("Sample text", textBlock.Text); } + + [Fact] + public void ToolResultContentBlock_WithError_SerializationRoundtrips() + { + ToolResultContentBlock toolResult = new() + { + ToolUseId = "call_123", + Content = [new TextContentBlock { Text = "Error: City not found" }], + IsError = true + }; + + var json = JsonSerializer.Serialize(toolResult, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + var result = Assert.IsType(deserialized); + Assert.Equal("call_123", result.ToolUseId); + Assert.True(result.IsError); + Assert.Single(result.Content); + var textBlock = Assert.IsType(result.Content[0]); + Assert.Equal("Error: City not found", textBlock.Text); + } + + [Fact] + public void ToolResultContentBlock_WithStructuredContent_SerializationRoundtrips() + { + ToolResultContentBlock toolResult = new() + { + ToolUseId = "call_123", + Content = + [ + new TextContentBlock { Text = "Result data" } + ], + StructuredContent = JsonElement.Parse("""{"temperature":18,"condition":"cloudy"}"""), + IsError = false + }; + + var json = JsonSerializer.Serialize(toolResult, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + var result = Assert.IsType(deserialized); + Assert.Equal("call_123", result.ToolUseId); + Assert.Single(result.Content); + var textBlock = Assert.IsType(result.Content[0]); + Assert.Equal("Result data", textBlock.Text); + Assert.NotNull(result.StructuredContent); + Assert.Equal(18, result.StructuredContent.Value.GetProperty("temperature").GetInt32()); + Assert.Equal("cloudy", result.StructuredContent.Value.GetProperty("condition").GetString()); + Assert.False(result.IsError); + } + + [Fact] + public void ToolResultContentBlock_SerializationRoundTrip() + { + ToolResultContentBlock toolResult = new() + { + ToolUseId = "call_123", + Content = + [ + new TextContentBlock { Text = "Result data" }, + new ImageContentBlock { Data = "base64data", MimeType = "image/png" } + ], + StructuredContent = JsonElement.Parse("""{"temperature":18,"condition":"cloudy"}"""), + IsError = false + }; + + var json = JsonSerializer.Serialize(toolResult, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + var result = Assert.IsType(deserialized); + Assert.Equal("call_123", result.ToolUseId); + Assert.Equal(2, result.Content.Count); + var textBlock = Assert.IsType(result.Content[0]); + Assert.Equal("Result data", textBlock.Text); + var imageBlock = Assert.IsType(result.Content[1]); + Assert.Equal("base64data", imageBlock.Data); + Assert.Equal("image/png", imageBlock.MimeType); + Assert.NotNull(result.StructuredContent); + Assert.Equal(18, result.StructuredContent.Value.GetProperty("temperature").GetInt32()); + Assert.Equal("cloudy", result.StructuredContent.Value.GetProperty("condition").GetString()); + Assert.False(result.IsError); + } + + [Fact] + public void ToolUseContentBlock_SerializationRoundTrip() + { + ToolUseContentBlock toolUse = new() + { + Id = "call_abc123", + Name = "get_weather", + Input = JsonElement.Parse("""{"city":"Paris","units":"metric"}""") + }; + + var json = JsonSerializer.Serialize(toolUse, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + var result = Assert.IsType(deserialized); + Assert.Equal("call_abc123", result.Id); + Assert.Equal("get_weather", result.Name); + Assert.Equal("Paris", result.Input.GetProperty("city").GetString()); + Assert.Equal("metric", result.Input.GetProperty("units").GetString()); + } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs new file mode 100644 index 000000000..f57faf1d8 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs @@ -0,0 +1,174 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public class CreateMessageRequestParamsTests +{ + [Fact] + public void WithTools_SerializationRoundtrips() + { + CreateMessageRequestParams requestParams = new() + { + MaxTokens = 1000, + Messages = + [ + new SamplingMessage + { + Role = Role.User, + Content = [new TextContentBlock { Text = "What's the weather in Paris?" }] + } + ], + Tools = + [ + new Tool + { + Name = "get_weather", + Description = "Get weather for a city", + InputSchema = JsonElement.Parse(""" + { + "type": "object", + "properties": { + "city": { "type": "string" } + }, + "required": ["city"] + } + """) + } + ], + ToolChoice = new ToolChoice { Mode = "auto" } + }; + + var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(1000, deserialized.MaxTokens); + Assert.NotNull(deserialized.Messages); + Assert.Single(deserialized.Messages); + Assert.Equal(Role.User, deserialized.Messages[0].Role); + Assert.Single(deserialized.Messages[0].Content); + var textContent = Assert.IsType(deserialized.Messages[0].Content[0]); + Assert.Equal("What's the weather in Paris?", textContent.Text); + Assert.NotNull(deserialized.Tools); + Assert.Single(deserialized.Tools); + Assert.Equal("get_weather", deserialized.Tools[0].Name); + Assert.Equal("Get weather for a city", deserialized.Tools[0].Description); + Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString()); + Assert.True(deserialized.Tools[0].InputSchema.GetProperty("properties").TryGetProperty("city", out var cityProp)); + Assert.Equal("string", cityProp.GetProperty("type").GetString()); + Assert.Single(deserialized.Tools[0].InputSchema.GetProperty("required").EnumerateArray()); + Assert.Equal("city", deserialized.Tools[0].InputSchema.GetProperty("required")[0].GetString()); + Assert.NotNull(deserialized.ToolChoice); + Assert.Equal("auto", deserialized.ToolChoice.Mode); + } + + [Fact] + public void WithToolChoiceRequired_SerializationRoundtrips() + { + CreateMessageRequestParams requestParams = new() + { + MaxTokens = 1000, + Messages = + [ + new SamplingMessage + { + Role = Role.User, + Content = [new TextContentBlock { Text = "What's the weather?" }] + } + ], + Tools = + [ + new Tool + { + Name = "get_weather", + Description = "Get weather for a city", + InputSchema = JsonElement.Parse(""" + { + "type": "object", + "properties": { "city": { "type": "string" } }, + "required": ["city"] + } + """) + } + ], + ToolChoice = new ToolChoice { Mode = "required" } + }; + + var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(1000, deserialized.MaxTokens); + Assert.NotNull(deserialized.Messages); + Assert.Single(deserialized.Messages); + Assert.Equal(Role.User, deserialized.Messages[0].Role); + Assert.Single(deserialized.Messages[0].Content); + var textContent = Assert.IsType(deserialized.Messages[0].Content[0]); + Assert.Equal("What's the weather?", textContent.Text); + Assert.NotNull(deserialized.Tools); + Assert.Single(deserialized.Tools); + Assert.Equal("get_weather", deserialized.Tools[0].Name); + Assert.Equal("Get weather for a city", deserialized.Tools[0].Description); + Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString()); + Assert.NotNull(deserialized.ToolChoice); + Assert.Equal("required", deserialized.ToolChoice.Mode); + } + + [Fact] + public void WithToolChoiceNone_SerializationRoundtrips() + { + CreateMessageRequestParams requestParams = new() + { + MaxTokens = 1000, + Messages = + [ + new SamplingMessage + { + Role = Role.User, + Content = [new TextContentBlock { Text = "What's the weather in Paris?" }] + } + ], + Tools = + [ + new Tool + { + Name = "get_weather", + Description = "Get weather for a city", + InputSchema = JsonElement.Parse(""" + { + "type": "object", + "properties": { "city": { "type": "string" } }, + "required": ["city"] + } + """) + } + ], + ToolChoice = new ToolChoice { Mode = "none" } + }; + + var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(1000, deserialized.MaxTokens); + Assert.NotNull(deserialized.Messages); + Assert.Single(deserialized.Messages); + Assert.Equal(Role.User, deserialized.Messages[0].Role); + Assert.Single(deserialized.Messages[0].Content); + var textContent = Assert.IsType(deserialized.Messages[0].Content[0]); + Assert.Equal("What's the weather in Paris?", textContent.Text); + Assert.NotNull(deserialized.Tools); + Assert.Single(deserialized.Tools); + Assert.Equal("get_weather", deserialized.Tools[0].Name); + Assert.Equal("Get weather for a city", deserialized.Tools[0].Description); + Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString()); + Assert.NotNull(deserialized.ToolChoice); + Assert.Equal("none", deserialized.ToolChoice.Mode); + } +} + + + + + diff --git a/tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs new file mode 100644 index 000000000..67ab5f4f9 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs @@ -0,0 +1,247 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace ModelContextProtocol.Tests.Protocol; + +public class CreateMessageResultTests +{ + [Fact] + public void CreateMessageResult_WithSingleContent_Serializes() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = [new TextContentBlock { Text = "Hello" }], + StopReason = "endTurn" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Single(deserialized.Content); + Assert.IsType(deserialized.Content[0]); + } + + [Fact] + public void CreateMessageResult_WithMultipleToolUses_Serializes() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = + [ + new ToolUseContentBlock + { + Id = "call_1", + Name = "tool1", + Input = JsonElement.Parse("""{}""") + }, + new ToolUseContentBlock + { + Id = "call_2", + Name = "tool2", + Input = JsonElement.Parse("""{}""") + } + ], + StopReason = "toolUse" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Content.Count); + Assert.All(deserialized.Content, c => Assert.IsType(c)); + Assert.Equal("call_1", ((ToolUseContentBlock)deserialized.Content[0]).Id); + Assert.Equal("call_2", ((ToolUseContentBlock)deserialized.Content[1]).Id); + } + + [Fact] + public void CreateMessageResult_WithMixedContent_Serializes() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = + [ + new TextContentBlock { Text = "Let me check that." }, + new ToolUseContentBlock + { + Id = "call_1", + Name = "tool1", + Input = JsonElement.Parse("""{}""") + } + ], + StopReason = "toolUse" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Content.Count); + Assert.IsType(deserialized.Content[0]); + Assert.IsType(deserialized.Content[1]); + } + + [Fact] + public void CreateMessageResult_EmptyContent_AllowedButUnusual() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = [], + StopReason = "endTurn" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Empty(deserialized.Content); + } + + [Fact] + public void CreateMessageResult_WithImageContent_Serializes() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = + [ + new ImageContentBlock + { + Data = Convert.ToBase64String([1, 2, 3, 4, 5]), + MimeType = "image/png" + } + ], + StopReason = "endTurn" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Single(deserialized.Content); + var imageBlock = Assert.IsType(deserialized.Content[0]); + Assert.Equal("image/png", imageBlock.MimeType); + } + + [Fact] + public void CreateMessageResult_RoundTripWithAllFields() + { + CreateMessageResult original = new() + { + Role = Role.Assistant, + Model = "claude-3-sonnet", + Content = + [ + new TextContentBlock { Text = "I'll help you with that." }, + new ToolUseContentBlock + { + Id = "call_xyz", + Name = "calculator", + Input = JsonElement.Parse("""{"operation":"add","a":5,"b":3}""") + } + ], + StopReason = "toolUse", + Meta = (JsonObject)JsonNode.Parse("""{"custom":"metadata"}""")! + }; + + var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(Role.Assistant, deserialized.Role); + Assert.Equal("claude-3-sonnet", deserialized.Model); + Assert.Equal(2, deserialized.Content.Count); + Assert.Equal("toolUse", deserialized.StopReason); + Assert.NotNull(deserialized.Meta); + } + + [Fact] + public void CreateMessageResult_WithToolUse_SerializationRoundtrips() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = + [ + new ToolUseContentBlock + { + Id = "call_123", + Name = "get_weather", + Input = JsonElement.Parse("""{"city":"Paris"}""") + } + ], + StopReason = "toolUse" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(Role.Assistant, deserialized.Role); + Assert.Equal("test-model", deserialized.Model); + Assert.Equal("toolUse", deserialized.StopReason); + Assert.Single(deserialized.Content); + + var toolUse = Assert.IsType(deserialized.Content[0]); + Assert.Equal("call_123", toolUse.Id); + Assert.Equal("get_weather", toolUse.Name); + Assert.Equal("Paris", toolUse.Input.GetProperty("city").GetString()); + } + + [Fact] + public void CreateMessageResult_WithParallelToolUses_SerializationRoundtrips() + { + CreateMessageResult result = new() + { + Role = Role.Assistant, + Model = "test-model", + Content = + [ + new ToolUseContentBlock + { + Id = "call_abc123", + Name = "get_weather", + Input = JsonElement.Parse("""{"city":"Paris"}""") + }, + new ToolUseContentBlock + { + Id = "call_def456", + Name = "get_weather", + Input = JsonElement.Parse("""{"city":"London"}""") + } + ], + StopReason = "toolUse" + }; + + var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(Role.Assistant, deserialized.Role); + Assert.Equal("test-model", deserialized.Model); + Assert.Equal("toolUse", deserialized.StopReason); + Assert.Equal(2, deserialized.Content.Count); + + var toolUse1 = Assert.IsType(deserialized.Content[0]); + Assert.Equal("call_abc123", toolUse1.Id); + Assert.Equal("get_weather", toolUse1.Name); + Assert.Equal("Paris", toolUse1.Input.GetProperty("city").GetString()); + + var toolUse2 = Assert.IsType(deserialized.Content[1]); + Assert.Equal("call_def456", toolUse2.Id); + Assert.Equal("get_weather", toolUse2.Name); + Assert.Equal("London", toolUse2.Input.GetProperty("city").GetString()); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs index 420c35f95..c29f7d033 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs @@ -1,6 +1,8 @@ using ModelContextProtocol.Protocol; using System.Text.Json; +#pragma warning disable CS0618 // Type or member is obsolete + namespace ModelContextProtocol.Tests.Protocol; public class ElicitationDefaultValuesTests @@ -259,7 +261,8 @@ public void PrimitiveSchemaDefinition_EnumSchema_WithDefault_RoundTrips() // Assert Assert.NotNull(deserialized); - var enumSchema = Assert.IsType(deserialized); + // EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema + var enumSchema = Assert.IsType(deserialized); Assert.Equal("draft", enumSchema.Default); Assert.Equal(["draft", "published", "archived"], enumSchema.Enum); } @@ -326,7 +329,8 @@ public void RequestSchema_WithAllDefaultTypes_Serializes_Correctly() var activeSchema = Assert.IsType(deserialized.RequestedSchema.Properties["active"]); Assert.True(activeSchema.Default); - var statusSchema = Assert.IsType(deserialized.RequestedSchema.Properties["status"]); + // EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema + var statusSchema = Assert.IsType(deserialized.RequestedSchema.Properties["status"]); Assert.Equal("active", statusSchema.Default); } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index 76f967bed..fc4dacb78 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -3,6 +3,8 @@ using ModelContextProtocol.Protocol; using System.Text.Json; +#pragma warning disable CS0618 // Type or member is obsolete + namespace ModelContextProtocol.Tests.Configuration; public partial class ElicitationTests : ClientServerTestBase diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs index b4afb1af3..b0cdbe3d9 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs @@ -4,6 +4,8 @@ using System.Text.Json; using System.Text.Json.Serialization; +#pragma warning disable CS0618 // Type or member is obsolete + namespace ModelContextProtocol.Tests.Configuration; public partial class ElicitationTypedTests : ClientServerTestBase @@ -147,7 +149,7 @@ public async Task Can_Elicit_Typed_Information() break; case nameof(SampleForm.Role): - var enumSchema = Assert.IsType(value); + var enumSchema = Assert.IsType(value); Assert.Equal("string", enumSchema.Type); Assert.Equal([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum); break; diff --git a/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs b/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs new file mode 100644 index 000000000..86f0be591 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs @@ -0,0 +1,309 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +#pragma warning disable CS0618 // Type or member is obsolete + +namespace ModelContextProtocol.Tests.Protocol; + +public class EnumSchemaTests +{ + [Fact] + public void UntitledSingleSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.UntitledSingleSelectEnumSchema + { + Title = "Priority", + Description = "Task priority level", + Enum = ["low", "medium", "high"], + Default = "medium" + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Priority", result.Title); + Assert.Equal("Task priority level", result.Description); + Assert.Equal(["low", "medium", "high"], result.Enum); + Assert.Equal("medium", result.Default); + Assert.Contains("\"type\":\"string\"", json); + Assert.Contains("\"enum\":[\"low\",\"medium\",\"high\"]", json); + Assert.DoesNotContain("enumNames", json); + Assert.DoesNotContain("oneOf", json); + } + + [Fact] + public void TitledSingleSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.TitledSingleSelectEnumSchema + { + Title = "Severity", + Description = "Issue severity", + OneOf = + [ + new ElicitRequestParams.EnumSchemaOption { Const = "critical", Title = "Critical" }, + new ElicitRequestParams.EnumSchemaOption { Const = "high", Title = "High Priority" }, + new ElicitRequestParams.EnumSchemaOption { Const = "low", Title = "Low Priority" } + ], + Default = "high" + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Severity", result.Title); + Assert.Equal("Issue severity", result.Description); + Assert.Equal(3, result.OneOf.Count); + Assert.Equal("critical", result.OneOf[0].Const); + Assert.Equal("Critical", result.OneOf[0].Title); + Assert.Equal("high", result.Default); + Assert.Contains("\"oneOf\":", json); + Assert.Contains("\"const\":\"critical\"", json); + Assert.Contains("\"title\":\"Critical\"", json); + Assert.DoesNotContain("enum\":", json); + Assert.DoesNotContain("enumNames", json); + } + + [Fact] + public void UntitledMultiSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.UntitledMultiSelectEnumSchema + { + Title = "Tags", + Description = "Select multiple tags", + MinItems = 1, + MaxItems = 3, + Items = new ElicitRequestParams.UntitledEnumItemsSchema + { + Type = "string", + Enum = ["bug", "feature", "documentation", "enhancement"] + }, + Default = ["bug", "feature"] + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Tags", result.Title); + Assert.Equal("Select multiple tags", result.Description); + Assert.Equal(1, result.MinItems); + Assert.Equal(3, result.MaxItems); + Assert.NotNull(result.Items); + Assert.Equal("string", result.Items.Type); + Assert.Equal(["bug", "feature", "documentation", "enhancement"], result.Items.Enum); + Assert.Equal(["bug", "feature"], result.Default); + Assert.Contains("\"type\":\"array\"", json); + Assert.Contains("\"minItems\":1", json); + Assert.Contains("\"maxItems\":3", json); + Assert.Contains("\"items\":", json); + Assert.DoesNotContain("anyOf", json); + } + + [Fact] + public void TitledMultiSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.TitledMultiSelectEnumSchema + { + Title = "Features", + Description = "Select desired features", + MinItems = 2, + Items = new ElicitRequestParams.TitledEnumItemsSchema + { + AnyOf = + [ + new ElicitRequestParams.EnumSchemaOption { Const = "auth", Title = "Authentication" }, + new ElicitRequestParams.EnumSchemaOption { Const = "api", Title = "REST API" }, + new ElicitRequestParams.EnumSchemaOption { Const = "ui", Title = "User Interface" } + ] + }, + Default = ["auth", "api"] + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Features", result.Title); + Assert.Equal("Select desired features", result.Description); + Assert.Equal(2, result.MinItems); + Assert.NotNull(result.Items); + Assert.NotNull(result.Items.AnyOf); + Assert.Equal(3, result.Items.AnyOf.Count); + Assert.Equal("auth", result.Items.AnyOf[0].Const); + Assert.Equal("Authentication", result.Items.AnyOf[0].Title); + Assert.Equal(["auth", "api"], result.Default); + Assert.Contains("\"type\":\"array\"", json); + Assert.Contains("\"anyOf\":", json); + Assert.Contains("\"const\":\"auth\"", json); + Assert.Contains("\"title\":\"Authentication\"", json); + } + + [Fact] + public void LegacyTitledEnumSchema_WithEnumNames_Deserializes_As_EnumSchema() + { + // Arrange - JSON with enumNames should deserialize as EnumSchema for backward compatibility + string json = """ + { + "type": "string", + "title": "Status", + "enum": ["active", "inactive", "pending"], + "enumNames": ["Active", "Inactive", "Pending"], + "default": "active" + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Status", result.Title); + Assert.Equal(["active", "inactive", "pending"], result.Enum); + Assert.Equal(["Active", "Inactive", "Pending"], result.EnumNames); + Assert.Equal("active", result.Default); + } + + [Fact] + public void EnumSchema_WithoutEnumNames_Deserializes_As_UntitledSingleSelect() + { + // Arrange - JSON without enumNames should deserialize as UntitledSingleSelectEnumSchema + string json = """ + { + "type": "string", + "title": "Status", + "enum": ["draft", "published", "archived"], + "default": "draft" + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Status", result.Title); + Assert.Equal(["draft", "published", "archived"], result.Enum); + Assert.Equal("draft", result.Default); + } + + [Fact] + public void EnumSchema_WithOneOf_Deserializes_As_TitledSingleSelect() + { + // Arrange - JSON with oneOf should deserialize as TitledSingleSelectEnumSchema + string json = """ + { + "type": "string", + "title": "Priority", + "oneOf": [ + { "const": "p0", "title": "P0 - Critical" }, + { "const": "p1", "title": "P1 - High" }, + { "const": "p2", "title": "P2 - Medium" } + ], + "default": "p1" + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Priority", result.Title); + Assert.Equal(3, result.OneOf.Count); + Assert.Equal("p0", result.OneOf[0].Const); + Assert.Equal("P0 - Critical", result.OneOf[0].Title); + Assert.Equal("p1", result.Default); + } + + [Fact] + public void MultiSelectEnum_WithEnum_Deserializes_As_UntitledMultiSelect() + { + // Arrange + string json = """ + { + "type": "array", + "title": "Categories", + "items": { + "type": "string", + "enum": ["tech", "business", "lifestyle"] + }, + "default": ["tech"] + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Categories", result.Title); + Assert.NotNull(result.Items); + Assert.Equal(["tech", "business", "lifestyle"], result.Items.Enum); + Assert.Equal(["tech"], result.Default); + } + + [Fact] + public void MultiSelectEnum_WithAnyOf_Deserializes_As_TitledMultiSelect() + { + // Arrange + string json = """ + { + "type": "array", + "title": "Roles", + "items": { + "anyOf": [ + { "const": "admin", "title": "Administrator" }, + { "const": "user", "title": "User" }, + { "const": "guest", "title": "Guest" } + ] + }, + "default": ["user"] + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Roles", result.Title); + Assert.NotNull(result.Items); + Assert.NotNull(result.Items.AnyOf); + Assert.Equal(3, result.Items.AnyOf.Count); + Assert.Equal("admin", result.Items.AnyOf[0].Const); + Assert.Equal("Administrator", result.Items.AnyOf[0].Title); + Assert.Equal(["user"], result.Default); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs index bb5ab1c67..1e577f965 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs @@ -1,6 +1,8 @@ using ModelContextProtocol.Protocol; using System.Text.Json; +#pragma warning disable CS0618 // Type or member is obsolete + namespace ModelContextProtocol.Tests.Protocol; public static class PrimitiveSchemaDefinitionTests diff --git a/tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs b/tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs new file mode 100644 index 000000000..9765d1be3 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs @@ -0,0 +1,111 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public class SamplingMessageTests +{ + [Fact] + public void WithToolResults_SerializationRoundtrips() + { + SamplingMessage message = new() + { + Role = Role.User, + Content = + [ + new ToolResultContentBlock + { + ToolUseId = "call_123", + Content = + [ + new TextContentBlock { Text = "Weather in Paris: 18°C, partly cloudy" } + ] + } + ] + }; + + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(Role.User, deserialized.Role); + Assert.Single(deserialized.Content); + + var toolResult = Assert.IsType(deserialized.Content[0]); + Assert.Equal("call_123", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + + var textBlock = Assert.IsType(toolResult.Content[0]); + Assert.Equal("Weather in Paris: 18°C, partly cloudy", textBlock.Text); + } + + [Fact] + public void WithMultipleToolResults_SerializationRoundtrips() + { + SamplingMessage message = new() + { + Role = Role.User, + Content = + [ + new ToolResultContentBlock + { + ToolUseId = "call_abc123", + Content = [new TextContentBlock { Text = "Weather in Paris: 18°C, partly cloudy" }] + }, + new ToolResultContentBlock + { + ToolUseId = "call_def456", + Content = [new TextContentBlock { Text = "Weather in London: 15°C, rainy" }] + } + ] + }; + + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(Role.User, deserialized.Role); + Assert.Equal(2, deserialized.Content.Count); + + var toolResult1 = Assert.IsType(deserialized.Content[0]); + Assert.Equal("call_abc123", toolResult1.ToolUseId); + Assert.Single(toolResult1.Content); + var textBlock1 = Assert.IsType(toolResult1.Content[0]); + Assert.Equal("Weather in Paris: 18°C, partly cloudy", textBlock1.Text); + + var toolResult2 = Assert.IsType(deserialized.Content[1]); + Assert.Equal("call_def456", toolResult2.ToolUseId); + Assert.Single(toolResult2.Content); + var textBlock2 = Assert.IsType(toolResult2.Content[0]); + Assert.Equal("Weather in London: 15°C, rainy", textBlock2.Text); + } + + [Fact] + public void WithToolResultOnly_SerializationRoundtrips() + { + SamplingMessage message = new() + { + Role = Role.User, + Content = + [ + new ToolResultContentBlock + { + ToolUseId = "call_123", + Content = [new TextContentBlock { Text = "Result" }] + } + ] + }; + + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(Role.User, deserialized.Role); + Assert.Single(deserialized.Content); + var toolResult = Assert.IsType(deserialized.Content[0]); + Assert.Equal("call_123", toolResult.ToolUseId); + Assert.Single(toolResult.Content); + var textBlock = Assert.IsType(toolResult.Content[0]); + Assert.Equal("Result", textBlock.Text); + } +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs new file mode 100644 index 000000000..548579d50 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs @@ -0,0 +1,30 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public class ToolChoiceTests +{ + [Fact] + public void DefaultModeIsNull() + { + Assert.Null(new ToolChoice().Mode); + } + + [Theory] + [InlineData(null)] + [InlineData("none")] + [InlineData("required")] + [InlineData("auto")] + [InlineData("something_custom")] + public void SerializesWithMode(string? mode) + { + ToolChoice toolChoice = new() { Mode = mode }; + + var json = JsonSerializer.Serialize(toolChoice, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + Assert.Equal(mode, deserialized.Mode); + } +} diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 810bcef48..ab2537b6b 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; @@ -802,12 +802,12 @@ public override Task SendRequestAsync(JsonRpcRequest request, C Assert.Equal($"You are a helpful assistant.{Environment.NewLine}More system stuff.", rp.SystemPrompt); Assert.Equal(2, rp.Messages.Count); - Assert.Equal("I am going to France.", Assert.IsType(rp.Messages[0].Content).Text); - Assert.Equal("What is the most famous tower in Paris?", Assert.IsType(rp.Messages[1].Content).Text); + Assert.Equal("I am going to France.", Assert.IsType(Assert.Single(rp.Messages[0].Content)).Text); + Assert.Equal("What is the most famous tower in Paris?", Assert.IsType(Assert.Single(rp.Messages[1].Content)).Text); CreateMessageResult result = new() { - Content = new TextContentBlock { Text = "The Eiffel Tower." }, + Content = [new TextContentBlock { Text = "The Eiffel Tower." }], Model = "amazingmodel", Role = Role.Assistant, StopReason = "endTurn", diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs index 5394ba30e..7f9cd61a5 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs @@ -22,8 +22,7 @@ public async Task CreateAsync_ValidProcessInvalidServer_Throws() await Assert.ThrowsAsync(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); } - // [Fact(Skip = "Platform not supported by this test.", SkipUnless = nameof(IsStdErrCallbackSupported))] - [Fact] + [Fact(Skip = "Platform not supported by this test.", SkipUnless = nameof(IsStdErrCallbackSupported))] public async Task CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked() { string id = Guid.NewGuid().ToString("N"); From 44083aad80c4084930466d72028f8e291055eea8 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 20 Nov 2025 13:50:39 -0800 Subject: [PATCH 10/12] Revert "Merge main branch and resolve conflicts (PR #976 integration)" This reverts commit 4443cfcb6ec07179fa801d3eebd9cef04e32c137. --- .github/dependabot.yml | 1 + Directory.Build.props | 5 +- Directory.Packages.props | 30 +- Makefile | 19 +- ModelContextProtocol.slnx | 2 - README.md | 10 +- docs/concepts/elicitation/elicitation.md | 9 +- .../samples/server/Tools/InteractiveTools.cs | 148 -- docs/index.md | 6 - docs/toc.yml | 6 +- docs/versioning.md | 61 - global.json | 3 + samples/ChatWithTools/Program.cs | 1 - .../EverythingServer/Tools/SampleLlmTool.cs | 4 +- .../Tools/SampleLlmTool.cs | 4 +- .../Diagnostics.cs | 31 - .../ModelContextProtocol.Analyzers.csproj | 20 - .../XmlToDescriptionGenerator.cs | 414 ----- .../AIContentExtensions.cs | 212 +-- .../Client/McpClientExtensions.cs | 143 ++ .../Client/McpClientHandlers.cs | 2 +- src/ModelContextProtocol.Core/Diagnostics.cs | 2 +- .../ModelContextProtocol.Core.csproj | 17 - .../Protocol/ContentBlock.cs | 248 +-- .../Protocol/ContextInclusion.cs | 15 - .../Protocol/CreateMessageRequestParams.cs | 20 - .../Protocol/CreateMessageResult.cs | 26 +- .../Protocol/ElicitRequestParams.cs | 499 +----- .../Protocol/ElicitResult.cs | 2 +- .../Protocol/SamplingCapability.cs | 24 +- .../Protocol/SamplingContextCapability.cs | 6 - .../Protocol/SamplingMessage.cs | 25 +- .../Protocol/SamplingToolsCapability.cs | 6 - .../Protocol/SingleItemOrListConverter.cs | 67 - .../Protocol/ToolChoice.cs | 32 - .../Server/AIFunctionMcpServerTool.cs | 4 +- .../Server/McpServer.Methods.cs | 98 +- .../Server/McpServerPrompt.cs | 2 +- .../Server/McpServerPromptAttribute.cs | 2 +- .../Server/McpServerPromptCreateOptions.cs | 4 +- .../Server/McpServerResourceCreateOptions.cs | 6 +- .../Server/McpServerTool.cs | 4 +- .../Server/McpServerToolAttribute.cs | 4 +- .../Server/McpServerToolCreateOptions.cs | 2 +- tests/Common/Utils/TestServerTransport.cs | 4 +- ...odelContextProtocol.Analyzers.Tests.csproj | 36 - .../XmlToDescriptionGeneratorTests.cs | 1545 ----------------- .../HttpServerIntegrationTests.cs | 5 +- .../MapMcpTests.cs | 12 +- ...delContextProtocol.AspNetCore.Tests.csproj | 29 +- .../Program.cs | 6 +- .../Program.cs | 8 +- .../AIContentExtensionsTests.cs | 123 -- .../Client/McpClientCreationTests.cs | 4 +- .../Client/McpClientTests.cs | 139 +- .../Client/McpClientToolTests.cs | 9 +- .../ClientIntegrationTests.cs | 4 +- .../DockerEverythingServerTests.cs | 4 +- .../ModelContextProtocol.Tests.csproj | 28 +- .../Protocol/ContentBlockTests.cs | 102 +- .../CreateMessageRequestParamsTests.cs | 174 -- .../Protocol/CreateMessageResultTests.cs | 247 --- .../Protocol/ElicitationDefaultValuesTests.cs | 8 +- .../Protocol/ElicitationTests.cs | 2 - .../Protocol/ElicitationTypedTests.cs | 4 +- .../Protocol/EnumSchemaTests.cs | 309 ---- .../PrimitiveSchemaDefinitionTests.cs | 2 - .../Protocol/SamplingMessageTests.cs | 111 -- .../Protocol/ToolChoiceTests.cs | 30 - .../Server/McpServerTests.cs | 8 +- .../Transport/StdioClientTransportTests.cs | 3 +- 71 files changed, 399 insertions(+), 4803 deletions(-) delete mode 100644 docs/versioning.md delete mode 100644 src/ModelContextProtocol.Analyzers/Diagnostics.cs delete mode 100644 src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj delete mode 100644 src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs create mode 100644 src/ModelContextProtocol.Core/Client/McpClientExtensions.cs delete mode 100644 src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs delete mode 100644 src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs delete mode 100644 src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs delete mode 100644 src/ModelContextProtocol.Core/Protocol/ToolChoice.cs delete mode 100644 tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj delete mode 100644 tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7ffd6a269..57236c8cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,6 +17,7 @@ updates: patterns: - "xunit.*" - "Microsoft.NET.Test.Sdk" + - "Microsoft.Testing.*" - "coverlet.*" - "GitHubActionsTestLogger" - "Moq" diff --git a/Directory.Build.props b/Directory.Build.props index bd2aed325..1fad98569 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,8 +29,9 @@ - trx%3bLogFileName=$(MSBuildProjectName).$(TargetFramework).$(OS).trx - $(ArtifactsTestResultsDir) + true + <_MTPResultsDirectory>$(ArtifactsTestResultsDir) + $(TestingPlatformCommandLineArguments) --results-directory $(_MTPResultsDirectory) --report-trx --report-trx-filename $(MSBuildProjectName).$(TargetFramework).$(OS).trx diff --git a/Directory.Packages.props b/Directory.Packages.props index 65f62f16e..f21988562 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,7 @@ 8.0.22 9.0.11 10.0.0 + 2.0.2 @@ -46,25 +47,16 @@ - - - - - - - + - - - + - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + @@ -72,7 +64,6 @@ - @@ -81,14 +72,13 @@ - - + + - - + diff --git a/Makefile b/Makefile index f300ca158..8fd72b1e1 100644 --- a/Makefile +++ b/Makefile @@ -18,18 +18,15 @@ build: restore test: build dotnet test \ --no-build \ + --no-progress \ --configuration $(CONFIGURATION) \ - --filter '(Execution!=Manual)' \ - --blame \ - --blame-crash \ - --blame-hang-timeout 7m \ - --diag "$(ARTIFACT_PATH)/diag.txt" \ - --logger "trx" \ - --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" \ - --collect "XPlat Code Coverage" \ - --results-directory $(ARTIFACT_PATH)/testresults \ - -- \ - RunConfiguration.CollectSourceInformation=true + --filter-not-trait 'Execution=Manual' \ + --crashdump \ + --hangdump \ + --hangdump-timeout 7m \ + --coverage \ + --coverage-output-format cobertura \ + -p:_MTPResultsDirectory=$(ARTIFACT_PATH)/testresults \ pack: restore dotnet pack --no-restore --configuration $(CONFIGURATION) diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 1f6dce1ed..a70e3e310 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -62,13 +62,11 @@ - - diff --git a/README.md b/README.md index 73c71bd2b..3099dfcd3 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,6 @@ To get started writing a client, the `McpClient.CreateAsync` method is used to i to a server. Once you have an `McpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. ```csharp -using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; - var clientTransport = new StdioClientTransport(new StdioClientTransportOptions { Name = "Everything", @@ -66,7 +63,7 @@ var result = await client.CallToolAsync( cancellationToken:CancellationToken.None); // echo always returns one and only one text content object -Console.WriteLine(result.Content.OfType().First().Text); +Console.WriteLine(result.Content.First(c => c.Type == "text").Text); ``` You can find samples demonstrating how to use ModelContextProtocol with an LLM SDK in the [samples](samples) directory, and also refer to the [tests](tests/ModelContextProtocol.Tests) project for more examples. Additional examples and documentation will be added as in the near future. @@ -228,11 +225,6 @@ await using McpServer server = McpServer.Create(new StdioServerTransport("MyServ await server.RunAsync(); ``` -Descriptions can be added to tools, prompts, and resources in a variety of ways, including via the `[Description]` attribute from `System.ComponentModel`. -This attribute may be placed on a method to provide for the tool, prompt, or resource, or on individual parameters to describe each's purpose. -XML comments may also be used; if an `[McpServerTool]`, `[McpServerPrompt]`, or `[McpServerResource]`-attributed method is marked as `partial`, -XML comments placed on the method will be used automatically to generate `[Description]` attributes for the method and its parameters. - ## Acknowledgements The starting point for this library was a project called [mcpdotnet](https://github.com/PederHP/mcpdotnet), initiated by [Peder Holdgaard Pedersen](https://github.com/PederHP). We are grateful for the work done by Peder and other contributors to that repository, which created a solid foundation for this library. diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index bc1fc7ee2..2d9d43c9f 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -16,16 +16,9 @@ The C# SDK registers an instance of so tools can simply add a parameter of type to their method signature to access it. The MCP Server must specify the schema of each input value it is requesting from the user. -Primitive types (string, number, boolean) and enum types are supported for elicitation requests. +Only primitive types (string, number, boolean) are supported for elicitation requests. The schema may include a description to help the user understand what is being requested. -For enum types, the SDK supports several schema formats: -- **UntitledSingleSelectEnumSchema**: A single-select enum where the enum values serve as both the value and display text -- **TitledSingleSelectEnumSchema**: A single-select enum with separate display titles for each option (using JSON Schema `oneOf` with `const` and `title`) -- **UntitledMultiSelectEnumSchema**: A multi-select enum allowing multiple values to be selected -- **TitledMultiSelectEnumSchema**: A multi-select enum with display titles for each option -- **LegacyTitledEnumSchema** (deprecated): The legacy enum schema using `enumNames` for backward compatibility - The server can request a single input or multiple inputs at once. To help distinguish multiple inputs, each input has a unique name. diff --git a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs index 1dfcf9ac4..1528fa5a6 100644 --- a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs +++ b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs @@ -123,152 +123,4 @@ CancellationToken token } } } - - // - [McpServerTool, Description("Example tool demonstrating various enum schema types")] - public async Task EnumExamples( - McpServer server, - CancellationToken token - ) - { - // Example 1: UntitledSingleSelectEnumSchema - Simple enum without display titles - var prioritySchema = new RequestSchema - { - Properties = - { - ["Priority"] = new UntitledSingleSelectEnumSchema - { - Title = "Priority Level", - Description = "Select the priority level", - Enum = ["low", "medium", "high", "critical"], - Default = "medium" - } - } - }; - - var priorityResponse = await server.ElicitAsync(new ElicitRequestParams - { - Message = "Select a priority level:", - RequestedSchema = prioritySchema - }, token); - - if (priorityResponse.Action != "accept") - { - return "Operation cancelled"; - } - - string? priority = priorityResponse.Content?["Priority"].GetString(); - - // Example 2: TitledSingleSelectEnumSchema - Enum with custom display titles - var severitySchema = new RequestSchema - { - Properties = - { - ["Severity"] = new TitledSingleSelectEnumSchema - { - Title = "Issue Severity", - Description = "Select the issue severity level", - OneOf = - [ - new EnumSchemaOption { Const = "p0", Title = "P0 - Critical (Immediate attention required)" }, - new EnumSchemaOption { Const = "p1", Title = "P1 - High (Urgent, within 24 hours)" }, - new EnumSchemaOption { Const = "p2", Title = "P2 - Medium (Within a week)" }, - new EnumSchemaOption { Const = "p3", Title = "P3 - Low (As time permits)" } - ], - Default = "p2" - } - } - }; - - var severityResponse = await server.ElicitAsync(new ElicitRequestParams - { - Message = "Select the issue severity:", - RequestedSchema = severitySchema - }, token); - - if (severityResponse.Action != "accept") - { - return "Operation cancelled"; - } - - string? severity = severityResponse.Content?["Severity"].GetString(); - - // Example 3: UntitledMultiSelectEnumSchema - Select multiple values - var tagsSchema = new RequestSchema - { - Properties = - { - ["Tags"] = new UntitledMultiSelectEnumSchema - { - Title = "Tags", - Description = "Select one or more tags", - MinItems = 1, - MaxItems = 3, - Items = new UntitledEnumItemsSchema - { - Type = "string", - Enum = ["bug", "feature", "documentation", "enhancement", "question"] - }, - Default = ["bug"] - } - } - }; - - var tagsResponse = await server.ElicitAsync(new ElicitRequestParams - { - Message = "Select up to 3 tags:", - RequestedSchema = tagsSchema - }, token); - - if (tagsResponse.Action != "accept") - { - return "Operation cancelled"; - } - - // For multi-select, the value is an array - var tags = tagsResponse.Content?["Tags"].EnumerateArray() - .Select(e => e.GetString()) - .ToArray(); - - // Example 4: TitledMultiSelectEnumSchema - Multi-select with custom titles - var featuresSchema = new RequestSchema - { - Properties = - { - ["Features"] = new TitledMultiSelectEnumSchema - { - Title = "Features", - Description = "Select desired features", - Items = new TitledEnumItemsSchema - { - AnyOf = - [ - new EnumSchemaOption { Const = "auth", Title = "Authentication & Authorization" }, - new EnumSchemaOption { Const = "api", Title = "RESTful API" }, - new EnumSchemaOption { Const = "ui", Title = "Modern UI Components" }, - new EnumSchemaOption { Const = "db", Title = "Database Integration" } - ] - } - } - } - }; - - var featuresResponse = await server.ElicitAsync(new ElicitRequestParams - { - Message = "Select desired features:", - RequestedSchema = featuresSchema - }, token); - - if (featuresResponse.Action != "accept") - { - return "Operation cancelled"; - } - - var features = featuresResponse.Content?["Features"].EnumerateArray() - .Select(e => e.GetString()) - .ToArray(); - - return $"Selected: Priority={priority}, Severity={severity}, Tags=[{string.Join(", ", tags ?? [])}], Features=[{string.Join(", ", features ?? [])}]"; - } - // } \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index f2163f094..facf2093b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,12 +18,6 @@ For more information about MCP: For how-to guides, tutorials, and additional guidance, refer to the [official MCP documentation](https://modelcontextprotocol.io/). -## Official SDK packages - -The official C# SDK packages for stable and pre-release versions are published to the [NuGet Gallery](https://www.nuget.org) under the [ModelContextProtocolOfficial](https://www.nuget.org/profiles/ModelContextProtocolOfficial) profile. - -Continuous integration builds are published to the modelcontextprotocol organization's [GitHub NuGet package registry](https://github.com/orgs/modelcontextprotocol/packages?ecosystem=nuget). - ## License This project is licensed under the [MIT License](https://github.com/modelcontextprotocol/csharp-sdk/blob/main/LICENSE). diff --git a/docs/toc.yml b/docs/toc.yml index 84cf4de03..350a2ae3b 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -3,7 +3,5 @@ items: href: concepts/index.md - name: API Reference href: api/ModelContextProtocol.yml -- name: Versioning - href: versioning.md -- name: GitHub - href: https://github.com/ModelContextProtocol/csharp-sdk +- name: Github + href: https://github.com/ModelContextProtocol/csharp-sdk \ No newline at end of file diff --git a/docs/versioning.md b/docs/versioning.md deleted file mode 100644 index c9174a74e..000000000 --- a/docs/versioning.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: C# SDK Versioning -author: jeffhandley -description: ModelContextProtocol C# SDK approach to versioning, breaking changes, and support -uid: versioning ---- -The ModelContextProtocol specification continues to evolve rapidly, and it's important for the C# SDK to remain current with specification additions and updates. To enable this, all NuGet packages that compose the SDK will follow [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) with MAJOR.MINOR.PATCH version numbers, and optional pre-release versions. - -Given a version number MAJOR.MINOR.PATCH, the package versions will increment the: - -* MAJOR version when incompatible API changes are included -* MINOR version when functionality is added in a backward-compatible manner -* PATCH version when backward-compatible bug fixes are included - -*A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements.* - -## Supported versions - -Beginning with the 1.0.0 release, the following support policy will be applied for the official C# ModelContextProtocol SDK packages: - -1. New functionality and additive APIs will be introduced in MINOR releases within the current MAJOR version only - * New functionality will not be added to an earlier MAJOR version -2. Bugs will be fixed within either: - 1. A new PATCH release against the latest MAJOR.MINOR version - 2. A new MINOR release against the latest MAJOR version -3. Critical, blocking issues will be fixed against: - 1. The latest MINOR version within _the current_ MAJOR version - 2. The latest MINOR version within _one previous_ MAJOR version, until the latest MAJOR version has been published for 3 months - -## Experimental APIs - -MAJOR or MINOR version updates might introduce or alter APIs annotated as [`[Experimental]`](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute). This attribute indicates that an API is experimental and it may change at any time--including within PATCH or MINOR version updates. - -Experimental APIs require suppression of diagnostic codes specific to the MCP SDK APIs, using an `MCPEXP` prefix. - -## Breaking changes - -Prior to the release of a stable 1.0.0 set of NuGet packages, the SDK remains in preview and breaking changes can be introduced without prior notice. All versions beginning with the stable 1.0.0 release will follow semantic versioning, and breaking changes will require increments to the MAJOR version. - -If feasible, the SDK will support all versions of the MCP spec. However, if breaking changes to the spec make this infeasible, preference will be given to the most recent version of the MCP spec, and this would be considered a breaking change necessitating a new MAJOR version. - -All releases are posted to https://github.com/modelcontextprotocol/csharp-sdk/releases with release notes. Issues and pull requests labeled with `breaking-change` are highlighted in the corresponding release notes. - -### Specification schema changes - -If the MCP specification changes the schema for JSON payloads, the C# SDK may use the [`McpSession.NegotiatedProtocolVersion`](https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpSession.html#ModelContextProtocol_McpSession_NegotiatedProtocolVersion) to dynamically change the payload schema, potentially using internal data transfer objects (DTOs) to achieve the needed deserialization behavior. These techniques will be applied where feasible to maintain backward- and forward-compatibility between MCP specification versions. - -Refer to the following prototypes for illustrations of how this could be achieved: - -* [Support multiple contents in sampling results](https://github.com/eiriktsarpalis/csharp-sdk/pull/2) -* [Support multiple contents in sampling results (using DTOs)](https://github.com/eiriktsarpalis/csharp-sdk/pull/3) - -### Obsolete APIs - -If APIs within the SDK become obsolete due to changes in the MCP spec or other evolution of the SDK's APIs, the [`[Obsolete]`](https://learn.microsoft.com/dotnet/api/system.obsoleteattribute) attribute will be applied to the affected APIs. - -1. Within a MINOR version update, APIs may be marked as `[Obsolete]` to produce _build warnings_ while the API remains functional. The build warnings will provide guidance specific to the affected APIs. -2. Within a MAJOR version update, APIs may be marked as `[Obsolete]` to produce _build errors_ indicating the API is no longer functional and always throws exceptions. The build errors will provide guidance specific to the affected APIs. -3. Within a MAJOR version update, obsolete APIs may be removed. API removals are expected to be rare and avoided wherever possible, and `[Obsolete]` attributes will be applied ahead of the API removal. - -Beginning with the 1.0.0 release, all obsoletions will use diagnostic codes specific to the MCP SDK APIs, using an `MCPOBS` prefix. diff --git a/global.json b/global.json index fcb4599c2..4ed7c32bc 100644 --- a/global.json +++ b/global.json @@ -2,5 +2,8 @@ "sdk": { "version": "10.0.100", "rollForward": "minor" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } diff --git a/samples/ChatWithTools/Program.cs b/samples/ChatWithTools/Program.cs index c5870cdc3..c6fca0493 100644 --- a/samples/ChatWithTools/Program.cs +++ b/samples/ChatWithTools/Program.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; -using ModelContextProtocol; using ModelContextProtocol.Client; using OpenAI; using OpenTelemetry; diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs index 48c5184b3..6bbe6e51d 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -17,7 +17,7 @@ public static async Task SampleLLM( var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await server.SampleAsync(samplingParams, cancellationToken); - return $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}"; + return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; } private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) @@ -27,7 +27,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage { Role = Role.User, - Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 7d4c61784..2c96b8c35 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -20,7 +20,7 @@ public static async Task SampleLLM( var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken); - return $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}"; + return $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}"; } private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) @@ -30,7 +30,7 @@ private static CreateMessageRequestParams CreateRequestSamplingParams(string con Messages = [new SamplingMessage { Role = Role.User, - Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/src/ModelContextProtocol.Analyzers/Diagnostics.cs b/src/ModelContextProtocol.Analyzers/Diagnostics.cs deleted file mode 100644 index e2c70412b..000000000 --- a/src/ModelContextProtocol.Analyzers/Diagnostics.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using System.Collections.Immutable; -using System.Text; -using System.Xml.Linq; - -namespace ModelContextProtocol.Analyzers; - -/// Provides the diagnostic descriptors used by the assembly. -internal static class Diagnostics -{ - public static DiagnosticDescriptor InvalidXmlDocumentation { get; } = new( - id: "MCP001", - title: "Invalid XML documentation for MCP method", - messageFormat: "XML comment for method '{0}' is invalid and cannot be processed to generate [Description] attributes.", - category: "mcp", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "The XML documentation comment contains invalid XML and cannot be processed to generate Description attributes."); - - public static DiagnosticDescriptor McpMethodMustBePartial { get; } = new( - id: "MCP002", - title: "MCP method must be partial to generate [Description] attributes", - messageFormat: "Method '{0}' has XML documentation that could be used to generate [Description] attributes, but the method is not declared as partial.", - category: "mcp", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: "Methods with MCP attributes should be declared as partial to allow the source generator to emit Description attributes from XML documentation comments."); -} diff --git a/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj b/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj deleted file mode 100644 index 5338bbb84..000000000 --- a/src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netstandard2.0 - true - false - true - - - - - - - - - - - - - diff --git a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs b/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs deleted file mode 100644 index a5dff0c70..000000000 --- a/src/ModelContextProtocol.Analyzers/XmlToDescriptionGenerator.cs +++ /dev/null @@ -1,414 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using System.CodeDom.Compiler; -using System.Collections.Immutable; -using System.Text; -using System.Xml.Linq; - -namespace ModelContextProtocol.Analyzers; - -/// -/// Source generator that creates [Description] attributes from XML comments -/// for partial methods tagged with MCP attributes. -/// -[Generator] -public sealed class XmlToDescriptionGenerator : IIncrementalGenerator -{ - private const string GeneratedFileName = "ModelContextProtocol.Descriptions.g.cs"; - private const string McpServerToolAttributeName = "ModelContextProtocol.Server.McpServerToolAttribute"; - private const string McpServerPromptAttributeName = "ModelContextProtocol.Server.McpServerPromptAttribute"; - private const string McpServerResourceAttributeName = "ModelContextProtocol.Server.McpServerResourceAttribute"; - private const string DescriptionAttributeName = "System.ComponentModel.DescriptionAttribute"; - - public void Initialize(IncrementalGeneratorInitializationContext context) - { - // Use ForAttributeWithMetadataName for each MCP attribute type - var toolMethods = CreateProviderForAttribute(context, McpServerToolAttributeName); - var promptMethods = CreateProviderForAttribute(context, McpServerPromptAttributeName); - var resourceMethods = CreateProviderForAttribute(context, McpServerResourceAttributeName); - - // Combine all three providers - var allMethods = toolMethods - .Collect() - .Combine(promptMethods.Collect()) - .Combine(resourceMethods.Collect()) - .Select(static (tuple, _) => - { - var ((tool, prompt), resource) = tuple; - return tool.AddRange(prompt).AddRange(resource); - }); - - // Combine with compilation to get well-known type symbols. - var compilationAndMethods = context.CompilationProvider.Combine(allMethods); - - // Write out the source for all methods. - context.RegisterSourceOutput(compilationAndMethods, static (spc, source) => Execute(source.Left, source.Right, spc)); - } - - private static IncrementalValuesProvider CreateProviderForAttribute( - IncrementalGeneratorInitializationContext context, - string attributeMetadataName) => - context.SyntaxProvider.ForAttributeWithMetadataName( - attributeMetadataName, - static (node, _) => node is MethodDeclarationSyntax, - static (ctx, ct) => - { - var methodDeclaration = (MethodDeclarationSyntax)ctx.TargetNode; - var methodSymbol = (IMethodSymbol)ctx.TargetSymbol; - return new MethodToGenerate(methodDeclaration, methodSymbol); - }); - - private static void Execute(Compilation compilation, ImmutableArray methods, SourceProductionContext context) - { - if (methods.IsDefaultOrEmpty || - compilation.GetTypeByMetadataName(DescriptionAttributeName) is not { } descriptionAttribute) - { - return; - } - - // Gather a list of all methods needing generation. - List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methodsToGenerate = new(methods.Length); - foreach (var methodModel in methods) - { - var xmlDocs = ExtractXmlDocumentation(methodModel.MethodSymbol, context); - - // Generate implementation for partial methods. - if (methodModel.MethodDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - methodsToGenerate.Add((methodModel.MethodSymbol, methodModel.MethodDeclaration, xmlDocs)); - } - else if (xmlDocs is not null && HasGeneratableContent(xmlDocs, methodModel.MethodSymbol, descriptionAttribute)) - { - // The method is not partial but has XML docs that would generate attributes; issue a diagnostic. - context.ReportDiagnostic(Diagnostic.Create( - Diagnostics.McpMethodMustBePartial, - methodModel.MethodDeclaration.Identifier.GetLocation(), - methodModel.MethodSymbol.Name)); - } - } - - // Generate a single file with all partial declarations. - if (methodsToGenerate.Count > 0) - { - string source = GenerateSourceFile(compilation, methodsToGenerate, descriptionAttribute); - context.AddSource(GeneratedFileName, SourceText.From(source, Encoding.UTF8)); - } - } - - private static XmlDocumentation? ExtractXmlDocumentation(IMethodSymbol methodSymbol, SourceProductionContext context) - { - string? xmlDoc = methodSymbol.GetDocumentationCommentXml(); - if (string.IsNullOrWhiteSpace(xmlDoc)) - { - return null; - } - - try - { - if (XDocument.Parse(xmlDoc).Element("member") is not { } memberElement) - { - return null; - } - - var summary = CleanXmlDocText(memberElement.Element("summary")?.Value); - var remarks = CleanXmlDocText(memberElement.Element("remarks")?.Value); - var returns = CleanXmlDocText(memberElement.Element("returns")?.Value); - - // Combine summary and remarks for method description. - var methodDescription = - string.IsNullOrWhiteSpace(remarks) ? summary : - string.IsNullOrWhiteSpace(summary) ? remarks : - $"{summary}\n{remarks}"; - - Dictionary paramDocs = new(StringComparer.Ordinal); - foreach (var paramElement in memberElement.Elements("param")) - { - var name = paramElement.Attribute("name")?.Value; - var value = CleanXmlDocText(paramElement.Value); - if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(value)) - { - paramDocs[name!] = value; - } - } - - // Return documentation even if empty - we'll still generate the partial implementation - return new(methodDescription ?? string.Empty, returns ?? string.Empty, paramDocs); - } - catch (System.Xml.XmlException) - { - // Emit warning for invalid XML - context.ReportDiagnostic(Diagnostic.Create( - Diagnostics.InvalidXmlDocumentation, - methodSymbol.Locations.FirstOrDefault(), - methodSymbol.Name)); - return null; - } - } - - private static string CleanXmlDocText(string? text) - { - if (string.IsNullOrWhiteSpace(text)) - { - return string.Empty; - } - - // Remove leading/trailing whitespace and normalize line breaks - var lines = text!.Split('\n') - .Select(line => line.Trim()) - .Where(line => !string.IsNullOrEmpty(line)); - - return string.Join(" ", lines).Trim(); - } - - private static string GenerateSourceFile( - Compilation compilation, - List<(IMethodSymbol MethodSymbol, MethodDeclarationSyntax MethodDeclaration, XmlDocumentation? XmlDocs)> methods, - INamedTypeSymbol descriptionAttribute) - { - StringWriter sw = new(); - IndentedTextWriter writer = new(sw); - - writer.WriteLine("// "); - writer.WriteLine($"// ModelContextProtocol.Analyzers {typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}"); - writer.WriteLine(); - writer.WriteLine("#pragma warning disable"); - writer.WriteLine(); - writer.WriteLine("using System.ComponentModel;"); - writer.WriteLine("using ModelContextProtocol.Server;"); - writer.WriteLine(); - - // Group methods by namespace and containing type - var groupedMethods = methods.GroupBy(m => - m.MethodSymbol.ContainingNamespace.Name == compilation.GlobalNamespace.Name ? "" : - m.MethodSymbol.ContainingNamespace?.ToDisplayString() ?? - ""); - - bool firstNamespace = true; - foreach (var namespaceGroup in groupedMethods) - { - if (!firstNamespace) - { - writer.WriteLine(); - } - firstNamespace = false; - - // Check if this is the global namespace (methods with null ContainingNamespace) - bool isGlobalNamespace = string.IsNullOrEmpty(namespaceGroup.Key); - if (!isGlobalNamespace) - { - writer.WriteLine($"namespace {namespaceGroup.Key}"); - writer.WriteLine("{"); - writer.Indent++; - } - - // Group by containing type within namespace - bool isFirstTypeInNamespace = true; - foreach (var typeGroup in namespaceGroup.GroupBy(m => m.MethodSymbol.ContainingType, SymbolEqualityComparer.Default)) - { - if (typeGroup.Key is not INamedTypeSymbol containingType) - { - continue; - } - - if (!isFirstTypeInNamespace) - { - writer.WriteLine(); - } - isFirstTypeInNamespace = false; - - // Write out the type, which could include parent types. - AppendNestedTypeDeclarations(writer, containingType, typeGroup, descriptionAttribute); - } - - if (!isGlobalNamespace) - { - writer.Indent--; - writer.WriteLine("}"); - } - } - - return sw.ToString(); - } - - private static void AppendNestedTypeDeclarations( - IndentedTextWriter writer, - INamedTypeSymbol typeSymbol, - IGrouping typeGroup, - INamedTypeSymbol descriptionAttribute) - { - // Build stack of nested types from innermost to outermost - Stack types = []; - for (var current = typeSymbol; current is not null; current = current.ContainingType) - { - types.Push(current); - } - - // Generate type declarations from outermost to innermost - int nestingCount = types.Count; - while (types.Count > 0) - { - // Get the type keyword and handle records - var type = types.Pop(); - var typeDecl = type.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as TypeDeclarationSyntax; - string typeKeyword; - if (typeDecl is RecordDeclarationSyntax rds) - { - string classOrStruct = rds.ClassOrStructKeyword.ValueText; - if (string.IsNullOrEmpty(classOrStruct)) - { - classOrStruct = "class"; - } - - typeKeyword = $"{typeDecl.Keyword.ValueText} {classOrStruct}"; - } - else - { - typeKeyword = typeDecl?.Keyword.ValueText ?? "class"; - } - - writer.WriteLine($"partial {typeKeyword} {type.Name}"); - writer.WriteLine("{"); - writer.Indent++; - } - - // Generate methods for this type. - bool firstMethodInType = true; - foreach (var (methodSymbol, methodDeclaration, xmlDocs) in typeGroup) - { - AppendMethodDeclaration(writer, methodSymbol, methodDeclaration, xmlDocs, descriptionAttribute, firstMethodInType); - firstMethodInType = false; - } - - // Close all type declarations. - for (int i = 0; i < nestingCount; i++) - { - writer.Indent--; - writer.WriteLine("}"); - } - } - - private static void AppendMethodDeclaration( - IndentedTextWriter writer, - IMethodSymbol methodSymbol, - MethodDeclarationSyntax methodDeclaration, - XmlDocumentation? xmlDocs, - INamedTypeSymbol descriptionAttribute, - bool firstMethodInType) - { - if (!firstMethodInType) - { - writer.WriteLine(); - } - - // Add the Description attribute for method if needed and documentation exists - if (xmlDocs is not null && - !string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) && - !HasAttribute(methodSymbol, descriptionAttribute)) - { - writer.WriteLine($"[Description(\"{EscapeString(xmlDocs.MethodDescription)}\")]"); - } - - // Add return: Description attribute if needed and documentation exists - if (xmlDocs is not null && - !string.IsNullOrWhiteSpace(xmlDocs.Returns) && - methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute))) - { - writer.WriteLine($"[return: Description(\"{EscapeString(xmlDocs.Returns)}\")]"); - } - - // Copy modifiers from original method syntax. - // Add return type (without nullable annotations). - // Add method name. - writer.Write(string.Join(" ", methodDeclaration.Modifiers.Select(m => m.Text))); - writer.Write(' '); - writer.Write(methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); - writer.Write(' '); - writer.Write(methodSymbol.Name); - - // Add parameters with their Description attributes. - writer.Write("("); - for (int i = 0; i < methodSymbol.Parameters.Length; i++) - { - IParameterSymbol param = methodSymbol.Parameters[i]; - - if (i > 0) - { - writer.Write(", "); - } - - if (xmlDocs is not null && - !HasAttribute(param, descriptionAttribute) && - xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) && - !string.IsNullOrWhiteSpace(paramDoc)) - { - writer.Write($"[Description(\"{EscapeString(paramDoc)}\")] "); - } - - writer.Write(param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)); - writer.Write(' '); - writer.Write(param.Name); - } - writer.WriteLine(");"); - } - - /// Checks if a symbol has a specific attribute applied. - private static bool HasAttribute(ISymbol symbol, INamedTypeSymbol attributeType) - { - foreach (var attr in symbol.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType)) - { - return true; - } - } - - return false; - } - - /// Escape special characters for C# string literals. - private static string EscapeString(string text) => - string.IsNullOrEmpty(text) ? text : - text.Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\r", "\\r") - .Replace("\n", "\\n") - .Replace("\t", "\\t"); - - /// Checks if XML documentation would generate any Description attributes for a method. - private static bool HasGeneratableContent(XmlDocumentation xmlDocs, IMethodSymbol methodSymbol, INamedTypeSymbol descriptionAttribute) - { - // Check if method description would be generated - if (!string.IsNullOrWhiteSpace(xmlDocs.MethodDescription) && !HasAttribute(methodSymbol, descriptionAttribute)) - { - return true; - } - - // Check if return description would be generated - if (!string.IsNullOrWhiteSpace(xmlDocs.Returns) && - methodSymbol.GetReturnTypeAttributes().All(attr => !SymbolEqualityComparer.Default.Equals(attr.AttributeClass, descriptionAttribute))) - { - return true; - } - - // Check if any parameter descriptions would be generated - foreach (var param in methodSymbol.Parameters) - { - if (!HasAttribute(param, descriptionAttribute) && - xmlDocs.Parameters.TryGetValue(param.Name, out var paramDoc) && - !string.IsNullOrWhiteSpace(paramDoc)) - { - return true; - } - } - - return false; - } - - /// Represents a method that may need Description attributes generated. - private readonly record struct MethodToGenerate(MethodDeclarationSyntax MethodDeclaration, IMethodSymbol MethodSymbol); - - /// Holds extracted XML documentation for a method. - private sealed record XmlDocumentation(string MethodDescription, string Returns, Dictionary Parameters); -} diff --git a/src/ModelContextProtocol.Core/AIContentExtensions.cs b/src/ModelContextProtocol.Core/AIContentExtensions.cs index 374f00555..8686b7b6a 100644 --- a/src/ModelContextProtocol.Core/AIContentExtensions.cs +++ b/src/ModelContextProtocol.Core/AIContentExtensions.cs @@ -1,11 +1,9 @@ using Microsoft.Extensions.AI; -using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; #if !NET using System.Runtime.InteropServices; #endif using System.Text.Json; -using System.Text.Json.Nodes; namespace ModelContextProtocol; @@ -18,140 +16,6 @@ namespace ModelContextProtocol; /// public static class AIContentExtensions { - /// - /// 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 . - /// - /// - /// This method creates a function that converts MCP message requests into chat client calls, enabling - /// an MCP client to generate text or other content using an actual AI model via the provided chat client. - /// - /// - /// The handler can process text messages, image messages, resource messages, and tool use/results as defined in the - /// Model Context Protocol. - /// - /// - /// is . - public static Func, CancellationToken, ValueTask> CreateSamplingHandler( - this IChatClient chatClient) - { - Throw.IfNull(chatClient); - - return async (requestParams, progress, cancellationToken) => - { - Throw.IfNull(requestParams); - - var (messages, options) = ToChatClientArguments(requestParams); - var progressToken = requestParams.ProgressToken; - - List updates = []; - await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) - { - updates.Add(update); - - if (progressToken is not null) - { - progress.Report(new() { Progress = updates.Count }); - } - } - - ChatResponse? chatResponse = updates.ToChatResponse(); - ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); - - IList? contents = lastMessage?.Contents.Select(c => c.ToContentBlock()).ToList(); - if (contents is not { Count: > 0 }) - { - (contents ??= []).Add(new TextContentBlock() { Text = "" }); - } - - return new() - { - Model = chatResponse.ModelId ?? "", - StopReason = - chatResponse.FinishReason == ChatFinishReason.Stop ? CreateMessageResult.StopReasonEndTurn : - chatResponse.FinishReason == ChatFinishReason.Length ? CreateMessageResult.StopReasonMaxTokens : - chatResponse.FinishReason == ChatFinishReason.ToolCalls ? CreateMessageResult.StopReasonToolUse : - chatResponse.FinishReason.ToString(), - Meta = chatResponse.AdditionalProperties?.ToJsonObject(), - Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, - Content = contents, - }; - - static (IList Messages, ChatOptions? Options) ToChatClientArguments(CreateMessageRequestParams requestParams) - { - ChatOptions? options = null; - - if (requestParams.MaxTokens is int maxTokens) - { - (options ??= new()).MaxOutputTokens = maxTokens; - } - - if (requestParams.Temperature is float temperature) - { - (options ??= new()).Temperature = temperature; - } - - if (requestParams.StopSequences is { } stopSequences) - { - (options ??= new()).StopSequences = stopSequences.ToArray(); - } - - if (requestParams.SystemPrompt is { } systemPrompt) - { - (options ??= new()).Instructions = systemPrompt; - } - - if (requestParams.Tools is { } tools) - { - foreach (var tool in tools) - { - ((options ??= new()).Tools ??= []).Add(new ToolAIFunctionDeclaration(tool)); - } - - if (options.Tools is { Count: > 0 } && requestParams.ToolChoice is { } toolChoice) - { - options.ToolMode = toolChoice.Mode switch - { - ToolChoice.ModeAuto => ChatToolMode.Auto, - ToolChoice.ModeRequired => ChatToolMode.RequireAny, - ToolChoice.ModeNone => ChatToolMode.None, - _ => null, - }; - } - } - - List messages = []; - foreach (var sm in requestParams.Messages) - { - if (sm.Content?.Select(b => b.ToAIContent()).OfType().ToList() is { Count: > 0 } aiContents) - { - messages.Add(new ChatMessage(sm.Role is Role.Assistant ? ChatRole.Assistant : ChatRole.User, aiContents)); - } - } - - return (messages, options); - } - }; - } - - /// Converts the specified dictionary to a . - internal static JsonObject? ToJsonObject(this IReadOnlyDictionary properties) => - JsonSerializer.SerializeToNode(properties, McpJsonUtilities.JsonContext.Default.IReadOnlyDictionaryStringObject) as JsonObject; - - internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj) - { - AdditionalPropertiesDictionary d = []; - foreach (var kvp in obj) - { - d.Add(kvp.Key, kvp.Value); - } - - return d; - } - /// /// Converts a to a object. /// @@ -235,7 +99,7 @@ public static IList ToPromptMessages(this ChatMessage chatMessage { if (content is TextContent or DataContent) { - messages.Add(new PromptMessage { Role = r, Content = content.ToContentBlock() }); + messages.Add(new PromptMessage { Role = r, Content = content.ToContent() }); } } @@ -258,31 +122,13 @@ public static IList ToPromptMessages(this ChatMessage chatMessage AIContent? ac = content switch { TextContentBlock textContent => new TextContent(textContent.Text), - ImageContentBlock imageContent => new DataContent(Convert.FromBase64String(imageContent.Data), imageContent.MimeType), - AudioContentBlock audioContent => new DataContent(Convert.FromBase64String(audioContent.Data), audioContent.MimeType), - EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(), - - ToolUseContentBlock toolUse => FunctionCallContent.CreateFromParsedArguments(toolUse.Input, toolUse.Id, toolUse.Name, - static json => JsonSerializer.Deserialize(json, McpJsonUtilities.JsonContext.Default.IDictionaryStringObject)), - - ToolResultContentBlock toolResult => new FunctionResultContent( - toolResult.ToolUseId, - toolResult.Content.Count == 1 ? toolResult.Content[0].ToAIContent() : toolResult.Content.Select(c => c.ToAIContent()).OfType().ToList()) - { - Exception = toolResult.IsError is true ? new() : null, - }, - _ => null, }; - if (ac is not null) - { - ac.RawRepresentation = content; - ac.AdditionalProperties = content.Meta?.ToAdditionalProperties(); - } + ac?.RawRepresentation = content; return ac; } @@ -354,12 +200,8 @@ public static IList ToAIContents(this IEnumerable c return [.. contents.Select(ToAIContent)]; } - /// Creates a new from the content of an . - /// The to convert. - /// The created . - public static ContentBlock ToContentBlock(this AIContent content) - { - ContentBlock contentBlock = content switch + internal static ContentBlock ToContent(this AIContent content) => + content switch { TextContent textContent => new TextContentBlock { @@ -388,55 +230,9 @@ public static ContentBlock ToContentBlock(this AIContent content) } }, - FunctionCallContent callContent => new ToolUseContentBlock() - { - Id = callContent.CallId, - Name = callContent.Name, - Input = JsonSerializer.SerializeToElement(callContent.Arguments, McpJsonUtilities.DefaultOptions.GetTypeInfo>()!), - }, - - FunctionResultContent resultContent => new ToolResultContentBlock() - { - ToolUseId = resultContent.CallId, - IsError = resultContent.Exception is not null, - Content = - resultContent.Result is AIContent c ? [c.ToContentBlock()] : - resultContent.Result is IEnumerable ec ? [.. ec.Select(c => c.ToContentBlock())] : - [new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo()) }], - StructuredContent = resultContent.Result is JsonElement je ? je : null, - }, - _ => new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), } }; - - contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(); - - return contentBlock; - } - - private sealed class ToolAIFunctionDeclaration(Tool tool) : AIFunctionDeclaration - { - public override string Name => tool.Name; - - public override string Description => tool.Description ?? ""; - - public override IReadOnlyDictionary AdditionalProperties => - field ??= tool.Meta is { } meta ? meta.ToDictionary(p => p.Key, p => (object?)p.Value) : []; - - public override JsonElement JsonSchema => tool.InputSchema; - - public override JsonElement? ReturnJsonSchema => tool.OutputSchema; - - public override object? GetService(Type serviceType, object? serviceKey = null) - { - Throw.IfNull(serviceType); - - return - serviceKey is null && serviceType.IsInstanceOfType(tool) ? tool : - base.GetService(serviceType, serviceKey); - } - } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs new file mode 100644 index 000000000..86f77a900 --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -0,0 +1,143 @@ +using Microsoft.Extensions.AI; +using ModelContextProtocol.Protocol; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace ModelContextProtocol.Client; + +/// +/// Provides extension methods for interacting with an . +/// +/// +/// +/// This class contains extension methods that simplify common operations with an MCP client, +/// such as pinging a server, listing and working with tools, prompts, and resources, and +/// managing subscriptions to resources. +/// +/// +public static class McpClientExtensions +{ + /// + /// 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 . + /// + /// + /// This method creates a function that converts MCP message requests into chat client calls, enabling + /// an MCP client to generate text or other content using an actual AI model via the provided chat client. + /// + /// + /// The handler can process text messages, image messages, and resource messages as defined in the + /// Model Context Protocol. + /// + /// + /// is . + public static Func, CancellationToken, ValueTask> CreateSamplingHandler( + this IChatClient chatClient) + { + Throw.IfNull(chatClient); + + return async (requestParams, progress, cancellationToken) => + { + Throw.IfNull(requestParams); + + var (messages, options) = requestParams.ToChatClientArguments(); + var progressToken = requestParams.ProgressToken; + + List updates = []; + await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + updates.Add(update); + + if (progressToken is not null) + { + progress.Report(new() + { + Progress = updates.Count, + }); + } + } + + return updates.ToChatResponse().ToCreateMessageResult(); + }; + } + + /// + /// Converts the contents of a into a pair of + /// and instances to use + /// as inputs into a operation. + /// + /// + /// The created pair of messages and options. + /// is . + internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( + this CreateMessageRequestParams requestParams) + { + Throw.IfNull(requestParams); + + ChatOptions? options = null; + + if (requestParams.MaxTokens is int maxTokens) + { + (options ??= new()).MaxOutputTokens = maxTokens; + } + + if (requestParams.Temperature is float temperature) + { + (options ??= new()).Temperature = temperature; + } + + if (requestParams.StopSequences is { } stopSequences) + { + (options ??= new()).StopSequences = stopSequences.ToArray(); + } + + List messages = + (from sm in requestParams.Messages + let aiContent = sm.Content.ToAIContent() + where aiContent is not null + select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) + .ToList(); + + return (messages, options); + } + + /// Converts the contents of a into a . + /// The whose contents should be extracted. + /// The created . + /// is . + internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chatResponse) + { + Throw.IfNull(chatResponse); + + // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports + // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one + // in any of the response messages, or we'll use all the text from them concatenated, otherwise. + + ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); + + ContentBlock? content = null; + if (lastMessage is not null) + { + foreach (var lmc in lastMessage.Contents) + { + if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) + { + content = dc.ToContent(); + } + } + } + + return new() + { + Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, + Model = chatResponse.ModelId ?? "unknown", + Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, + StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", + }; + } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs index c21ec7b48..6200cede3 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientHandlers.cs @@ -80,7 +80,7 @@ public class McpClientHandlers /// generated content. /// /// - /// You can create a handler using the extension + /// You can create a handler using the extension /// method with any implementation of . /// /// diff --git a/src/ModelContextProtocol.Core/Diagnostics.cs b/src/ModelContextProtocol.Core/Diagnostics.cs index 083422d9c..bed648868 100644 --- a/src/ModelContextProtocol.Core/Diagnostics.cs +++ b/src/ModelContextProtocol.Core/Diagnostics.cs @@ -13,7 +13,7 @@ internal static class Diagnostics internal static Meter Meter { get; } = new("Experimental.ModelContextProtocol"); internal static Histogram CreateDurationHistogram(string name, string description, bool longBuckets) => - Meter.CreateHistogram(name, "s", description, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries); + Meter.CreateHistogram(name, "s", description, advice: longBuckets ? LongSecondsBucketBoundaries : ShortSecondsBucketBoundaries); /// /// Follows boundaries from http.server.request.duration/http.client.request.duration diff --git a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj index cdbe25a2d..d39c008eb 100644 --- a/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj +++ b/src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj @@ -43,23 +43,6 @@ - - - - - - - - - - - diff --git a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs index af8efdba6..012038f1d 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContentBlock.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.AI; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -36,7 +37,7 @@ private protected ContentBlock() /// When overridden in a derived class, gets the type of content. /// /// - /// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", "resource_link", "tool_use", and "tool_result". + /// This determines the structure of the content object. Valid values include "image", "audio", "text", "resource", and "resource_link". /// [JsonPropertyName("type")] public abstract string Type { get; } @@ -51,15 +52,6 @@ private protected ContentBlock() [JsonPropertyName("annotations")] public Annotations? Annotations { get; set; } - /// - /// Gets or sets metadata reserved by MCP for protocol-level metadata. - /// - /// - /// Implementations must not make assumptions about its contents. - /// - [JsonPropertyName("_meta")] - public JsonObject? Meta { get; set; } - /// /// Provides a for . /// @@ -92,12 +84,6 @@ public class Converter : JsonConverter ResourceContents? resource = null; Annotations? annotations = null; JsonObject? meta = null; - string? id = null; - JsonElement? input = null; - string? toolUseId = null; - List? content = null; - JsonElement? structuredContent = null; - bool? isError = null; while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { @@ -156,71 +142,42 @@ public class Converter : JsonConverter meta = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonObject); break; - case "id": - id = reader.GetString(); - break; - - case "input": - input = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement); - break; - - case "toolUseId": - toolUseId = reader.GetString(); - break; - - case "content": - if (reader.TokenType == JsonTokenType.StartArray) - { - content = []; - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - content.Add(Read(ref reader, typeof(ContentBlock), options) ?? - throw new JsonException("Unexpected null item in content array.")); - } - } - else - { - content = [Read(ref reader, typeof(ContentBlock), options) ?? - throw new JsonException("Unexpected null content item.")]; - } - break; - - case "structuredContent": - structuredContent = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.JsonElement); - break; - - case "isError": - isError = reader.GetBoolean(); - break; - default: reader.Skip(); break; } } - ContentBlock block = type switch + return type switch { "text" => new TextContentBlock { Text = text ?? throw new JsonException("Text contents must be provided for 'text' type."), + Annotations = annotations, + Meta = meta, }, "image" => new ImageContentBlock { Data = data ?? throw new JsonException("Image data must be provided for 'image' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'image' type."), + Annotations = annotations, + Meta = meta, }, "audio" => new AudioContentBlock { Data = data ?? throw new JsonException("Audio data must be provided for 'audio' type."), MimeType = mimeType ?? throw new JsonException("MIME type must be provided for 'audio' type."), + Annotations = annotations, + Meta = meta, }, "resource" => new EmbeddedResourceBlock { Resource = resource ?? throw new JsonException("Resource contents must be provided for 'resource' type."), + Annotations = annotations, + Meta = meta, }, "resource_link" => new ResourceLinkBlock @@ -230,30 +187,11 @@ public class Converter : JsonConverter Description = description, MimeType = mimeType, Size = size, - }, - - "tool_use" => new ToolUseContentBlock - { - Id = id ?? throw new JsonException("ID must be provided for 'tool_use' type."), - Name = name ?? throw new JsonException("Name must be provided for 'tool_use' type."), - Input = input ?? throw new JsonException("Input must be provided for 'tool_use' type."), - }, - - "tool_result" => new ToolResultContentBlock - { - ToolUseId = toolUseId ?? throw new JsonException("ToolUseId must be provided for 'tool_result' type."), - Content = content ?? throw new JsonException("Content must be provided for 'tool_result' type."), - StructuredContent = structuredContent, - IsError = isError, + Annotations = annotations, }, _ => throw new JsonException($"Unknown content type: '{type}'"), }; - - block.Annotations = annotations; - block.Meta = meta; - - return block; } /// @@ -273,21 +211,41 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial { case TextContentBlock textContent: writer.WriteString("text", textContent.Text); + if (textContent.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, textContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case ImageContentBlock imageContent: writer.WriteString("data", imageContent.Data); writer.WriteString("mimeType", imageContent.MimeType); + if (imageContent.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, imageContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case AudioContentBlock audioContent: writer.WriteString("data", audioContent.Data); writer.WriteString("mimeType", audioContent.MimeType); + if (audioContent.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, audioContent.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case EmbeddedResourceBlock embeddedResource: writer.WritePropertyName("resource"); JsonSerializer.Serialize(writer, embeddedResource.Resource, McpJsonUtilities.JsonContext.Default.ResourceContents); + if (embeddedResource.Meta is not null) + { + writer.WritePropertyName("_meta"); + JsonSerializer.Serialize(writer, embeddedResource.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); + } break; case ResourceLinkBlock resourceLink: @@ -306,33 +264,6 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial writer.WriteNumber("size", resourceLink.Size.Value); } break; - - case ToolUseContentBlock toolUse: - writer.WriteString("id", toolUse.Id); - writer.WriteString("name", toolUse.Name); - writer.WritePropertyName("input"); - JsonSerializer.Serialize(writer, toolUse.Input, McpJsonUtilities.JsonContext.Default.JsonElement); - break; - - case ToolResultContentBlock toolResult: - writer.WriteString("toolUseId", toolResult.ToolUseId); - writer.WritePropertyName("content"); - writer.WriteStartArray(); - foreach (var item in toolResult.Content) - { - Write(writer, item, options); - } - writer.WriteEndArray(); - if (toolResult.StructuredContent.HasValue) - { - writer.WritePropertyName("structuredContent"); - JsonSerializer.Serialize(writer, toolResult.StructuredContent.Value, McpJsonUtilities.JsonContext.Default.JsonElement); - } - if (toolResult.IsError.HasValue) - { - writer.WriteBoolean("isError", toolResult.IsError.Value); - } - break; } if (value.Annotations is { } annotations) @@ -341,12 +272,6 @@ public override void Write(Utf8JsonWriter writer, ContentBlock value, JsonSerial JsonSerializer.Serialize(writer, annotations, McpJsonUtilities.JsonContext.Default.Annotations); } - if (value.Meta is not null) - { - writer.WritePropertyName("_meta"); - JsonSerializer.Serialize(writer, value.Meta, McpJsonUtilities.JsonContext.Default.JsonObject); - } - writer.WriteEndObject(); } } @@ -363,6 +288,15 @@ public sealed class TextContentBlock : ContentBlock /// [JsonPropertyName("text")] public required string Text { get; set; } + + /// + /// Gets or sets metadata reserved by MCP for protocol-level metadata. + /// + /// + /// Implementations must not make assumptions about its contents. + /// + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } } /// Represents an image provided to or from an LLM. @@ -387,6 +321,15 @@ public sealed class ImageContentBlock : ContentBlock /// [JsonPropertyName("mimeType")] public required string MimeType { get; set; } + + /// + /// Gets or sets metadata reserved by MCP for protocol-level metadata. + /// + /// + /// Implementations must not make assumptions about its contents. + /// + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } } /// Represents audio provided to or from an LLM. @@ -411,6 +354,15 @@ public sealed class AudioContentBlock : ContentBlock /// [JsonPropertyName("mimeType")] public required string MimeType { get; set; } + + /// + /// Gets or sets metadata reserved by MCP for protocol-level metadata. + /// + /// + /// Implementations must not make assumptions about its contents. + /// + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } } /// Represents the contents of a resource, embedded into a prompt or tool call result. @@ -434,6 +386,15 @@ public sealed class EmbeddedResourceBlock : ContentBlock /// [JsonPropertyName("resource")] public required ResourceContents Resource { get; set; } + + /// + /// Gets or sets metadata reserved by MCP for protocol-level metadata. + /// + /// + /// Implementations must not make assumptions about its contents. + /// + [JsonPropertyName("_meta")] + public JsonObject? Meta { get; set; } } /// Represents a resource that the server is capable of reading, included in a prompt or tool call result. @@ -502,76 +463,3 @@ public sealed class ResourceLinkBlock : ContentBlock [JsonPropertyName("size")] public long? Size { get; set; } } - -/// Represents a request from the assistant to call a tool. -public sealed class ToolUseContentBlock : ContentBlock -{ - /// - public override string Type => "tool_use"; - - /// - /// Gets or sets a unique identifier for this tool use. - /// - /// - /// This ID is used to match tool results to their corresponding tool uses. - /// - [JsonPropertyName("id")] - public required string Id { get; set; } - - /// - /// Gets or sets the name of the tool to call. - /// - [JsonPropertyName("name")] - public required string Name { get; set; } - - /// - /// Gets or sets the arguments to pass to the tool, conforming to the tool's input schema. - /// - [JsonPropertyName("input")] - public required JsonElement Input { get; set; } -} - -/// Represents the result of a tool use, provided by the user back to the assistant. -public sealed class ToolResultContentBlock : ContentBlock -{ - /// - public override string Type => "tool_result"; - - /// - /// Gets or sets the ID of the tool use this result corresponds to. - /// - /// - /// This must match the ID from a previous . - /// - [JsonPropertyName("toolUseId")] - public required string ToolUseId { get; set; } - - /// - /// Gets or sets the unstructured result content of the tool use. - /// - /// - /// This has the same format as CallToolResult.Content and can include text, images, - /// audio, resource links, and embedded resources. - /// - [JsonPropertyName("content")] - public required List Content { get; set; } - - /// - /// Gets or sets an optional structured result object. - /// - /// - /// If the tool defined an outputSchema, this should conform to that schema. - /// - [JsonPropertyName("structuredContent")] - public JsonElement? StructuredContent { get; set; } - - /// - /// Gets or sets whether the tool use resulted in an error. - /// - /// - /// If true, the content typically describes the error that occurred. - /// Default: false - /// - [JsonPropertyName("isError")] - public bool? IsError { get; set; } -} diff --git a/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs b/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs index ec1bc2977..8894a3b3a 100644 --- a/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs +++ b/src/ModelContextProtocol.Core/Protocol/ContextInclusion.cs @@ -6,14 +6,7 @@ namespace ModelContextProtocol.Protocol; /// Specifies the context inclusion options for a request in the Model Context Protocol (MCP). /// /// -/// /// See the schema for details. -/// -/// -/// , and in particular and , are deprecated. -/// Servers should only use these values if the client declares with -/// set. These values may be removed in future spec releases. -/// /// [JsonConverter(typeof(JsonStringEnumConverter))] public enum ContextInclusion @@ -27,20 +20,12 @@ public enum ContextInclusion /// /// Indicates that context from the server that sent the request should be included. /// - /// - /// This value is soft-deprecated. Servers should only use this value if the client - /// declares ClientCapabilities.Sampling.Context. - /// [JsonStringEnumMemberName("thisServer")] ThisServer, /// /// Indicates that context from all servers that the client is connected to should be included. /// - /// - /// This value is soft-deprecated. Servers should only use this value if the client - /// declares ClientCapabilities.Sampling.Context. - /// [JsonStringEnumMemberName("allServers")] AllServers } diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs index c910053fb..d3086c0be 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageRequestParams.cs @@ -16,15 +16,7 @@ public sealed class CreateMessageRequestParams : RequestParams /// Gets or sets an indication as to which server contexts should be included in the prompt. /// /// - /// /// The client may ignore this request. - /// - /// - /// , and in particular and - /// , are deprecated. Servers should only use these values if the client - /// declares with set. - /// These values may be removed in future spec releases. - /// /// [JsonPropertyName("includeContext")] public ContextInclusion? IncludeContext { get; set; } @@ -108,16 +100,4 @@ public sealed class CreateMessageRequestParams : RequestParams /// [JsonPropertyName("temperature")] public float? Temperature { get; set; } - - /// - /// Gets or sets tools that the model may use during generation. - /// - [JsonPropertyName("tools")] - public IList? Tools { get; set; } - - /// - /// Gets or sets controls for how the model uses tools. - /// - [JsonPropertyName("toolChoice")] - public ToolChoice? ToolChoice { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs index d6891c747..7fada6399 100644 --- a/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/CreateMessageResult.cs @@ -11,14 +11,10 @@ namespace ModelContextProtocol.Protocol; public sealed class CreateMessageResult : Result { /// - /// Gets or sets the content of the assistant's response. + /// Gets or sets the content of the message. /// - /// - /// In the corresponding JSON, this may be a single content block or an array of content blocks. - /// [JsonPropertyName("content")] - [JsonConverter(typeof(SingleItemOrListConverter))] - public required IList Content { get; set; } + public required ContentBlock Content { get; set; } /// /// Gets or sets the name of the model that generated the message. @@ -39,14 +35,12 @@ public sealed class CreateMessageResult : Result /// Gets or sets the reason why message generation (sampling) stopped, if known. /// /// - /// Standard values include: + /// Common values include: /// /// endTurnThe model naturally completed its response. /// maxTokensThe response was truncated due to reaching token limits. /// stopSequenceA specific stop sequence was encountered during generation. - /// toolUseThe model wants to use one or more tools. /// - /// This field is an open string to allow for provider-specific stop reasons. /// [JsonPropertyName("stopReason")] public string? StopReason { get; set; } @@ -55,17 +49,5 @@ public sealed class CreateMessageResult : Result /// Gets or sets the role of the user who generated the message. /// [JsonPropertyName("role")] - public Role Role { get; set; } = Role.Assistant; - - /// The stop reason "endTurn". - internal const string StopReasonEndTurn = "endTurn"; - - /// The stop reason "maxTokens". - internal const string StopReasonMaxTokens = "maxTokens"; - - /// The stop reason "stopSequence". - internal const string StopReasonStopSequence = "stopSequence"; - - /// The stop reason "toolUse". - internal const string StopReasonToolUse = "toolUse"; + public required Role Role { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index 74d0fd8a9..50f9d4d51 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -21,10 +21,7 @@ public sealed class ElicitRequestParams /// Gets or sets the requested schema. /// /// - /// May be one of , , , - /// , , - /// , , - /// or (deprecated). + /// May be one of , , , or . /// [JsonPropertyName("requestedSchema")] [field: MaybeNull] @@ -62,10 +59,7 @@ public IDictionary Properties /// /// Represents restricted subset of JSON Schema: - /// , , , - /// , , - /// , , - /// or (deprecated). + /// , , , or . /// [JsonConverter(typeof(Converter))] public abstract class PrimitiveSchemaDefinition @@ -119,13 +113,8 @@ public class Converter : JsonConverter bool? defaultBool = null; double? defaultNumber = null; string? defaultString = null; - IList? defaultStringArray = null; IList? enumValues = null; IList? enumNames = null; - IList? oneOf = null; - int? minItems = null; - int? maxItems = null; - object? items = null; // Can be UntitledEnumItemsSchema or TitledEnumItemsSchema while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { @@ -172,14 +161,6 @@ public class Converter : JsonConverter maximum = reader.GetDouble(); break; - case "minItems": - minItems = reader.GetInt32(); - break; - - case "maxItems": - maxItems = reader.GetInt32(); - break; - case "default": // We need to handle different types for default values // Store the value based on the JSON token type @@ -197,9 +178,6 @@ public class Converter : JsonConverter case JsonTokenType.String: defaultString = reader.GetString(); break; - case JsonTokenType.StartArray: - defaultStringArray = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); - break; } break; @@ -211,14 +189,6 @@ public class Converter : JsonConverter enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); break; - case "oneOf": - oneOf = DeserializeEnumOptions(ref reader); - break; - - case "items": - items = DeserializeEnumItemsSchema(ref reader); - break; - default: reader.Skip(); break; @@ -234,39 +204,15 @@ public class Converter : JsonConverter switch (type) { case "string": - if (oneOf is not null) + if (enumValues is not null) { - // TitledSingleSelectEnumSchema - psd = new TitledSingleSelectEnumSchema + psd = new EnumSchema { - OneOf = oneOf, + Enum = enumValues, + EnumNames = enumNames, Default = defaultString, }; } - else if (enumValues is not null) - { - if (enumNames is not null) - { - // EnumSchema for backward compatibility -#pragma warning disable CS0618 // Type or member is obsolete - psd = new EnumSchema -#pragma warning restore CS0618 // Type or member is obsolete - { - Enum = enumValues, - EnumNames = enumNames, - Default = defaultString, - }; - } - else - { - // UntitledSingleSelectEnumSchema - psd = new UntitledSingleSelectEnumSchema - { - Enum = enumValues, - Default = defaultString, - }; - } - } else { psd = new StringSchema @@ -279,31 +225,6 @@ public class Converter : JsonConverter } break; - case "array": - if (items is TitledEnumItemsSchema titledItems) - { - // TitledMultiSelectEnumSchema - psd = new TitledMultiSelectEnumSchema - { - MinItems = minItems, - MaxItems = maxItems, - Items = titledItems, - Default = defaultStringArray, - }; - } - else if (items is UntitledEnumItemsSchema untitledItems) - { - // UntitledMultiSelectEnumSchema - psd = new UntitledMultiSelectEnumSchema - { - MinItems = minItems, - MaxItems = maxItems, - Items = untitledItems, - Default = defaultStringArray, - }; - } - break; - case "integer": case "number": psd = new NumberSchema @@ -332,108 +253,6 @@ public class Converter : JsonConverter return psd; } - private static IList DeserializeEnumOptions(ref Utf8JsonReader reader) - { - if (reader.TokenType != JsonTokenType.StartArray) - { - throw new JsonException("Expected array for oneOf property."); - } - - var options = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Expected object in oneOf array."); - } - - string? constValue = null; - string? titleValue = null; - - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType == JsonTokenType.PropertyName) - { - string? propertyName = reader.GetString(); - reader.Read(); - - switch (propertyName) - { - case "const": - constValue = reader.GetString(); - break; - case "title": - titleValue = reader.GetString(); - break; - default: - reader.Skip(); - break; - } - } - } - - if (constValue is null || titleValue is null) - { - throw new JsonException("Each option in oneOf must have both 'const' and 'title' properties."); - } - - options.Add(new EnumSchemaOption { Const = constValue, Title = titleValue }); - } - - return options; - } - - private static object DeserializeEnumItemsSchema(ref Utf8JsonReader reader) - { - if (reader.TokenType != JsonTokenType.StartObject) - { - throw new JsonException("Expected object for items property."); - } - - string? type = null; - IList? enumValues = null; - IList? anyOf = null; - - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType == JsonTokenType.PropertyName) - { - string? propertyName = reader.GetString(); - reader.Read(); - - switch (propertyName) - { - case "type": - type = reader.GetString(); - break; - case "enum": - enumValues = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); - break; - case "anyOf": - anyOf = DeserializeEnumOptions(ref reader); - break; - default: - reader.Skip(); - break; - } - } - } - - // Determine which type to create based on the properties - if (anyOf is not null) - { - return new TitledEnumItemsSchema { AnyOf = anyOf }; - } - else if (enumValues is not null) - { - return new UntitledEnumItemsSchema { Type = type ?? "string", Enum = enumValues }; - } - else - { - throw new JsonException("Items schema must have either 'enum' or 'anyOf' property."); - } - } - /// public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition value, JsonSerializerOptions options) { @@ -498,82 +317,20 @@ public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition valu } break; - case UntitledSingleSelectEnumSchema untitledSingleSelect: - if (untitledSingleSelect.Enum is not null) - { - writer.WritePropertyName("enum"); - JsonSerializer.Serialize(writer, untitledSingleSelect.Enum, McpJsonUtilities.JsonContext.Default.IListString); - } - if (untitledSingleSelect.Default is not null) - { - writer.WriteString("default", untitledSingleSelect.Default); - } - break; - - case TitledSingleSelectEnumSchema titledSingleSelect: - if (titledSingleSelect.OneOf is not null && titledSingleSelect.OneOf.Count > 0) - { - writer.WritePropertyName("oneOf"); - SerializeEnumOptions(writer, titledSingleSelect.OneOf); - } - if (titledSingleSelect.Default is not null) - { - writer.WriteString("default", titledSingleSelect.Default); - } - break; - - case UntitledMultiSelectEnumSchema untitledMultiSelect: - if (untitledMultiSelect.MinItems.HasValue) - { - writer.WriteNumber("minItems", untitledMultiSelect.MinItems.Value); - } - if (untitledMultiSelect.MaxItems.HasValue) - { - writer.WriteNumber("maxItems", untitledMultiSelect.MaxItems.Value); - } - writer.WritePropertyName("items"); - SerializeUntitledEnumItemsSchema(writer, untitledMultiSelect.Items); - if (untitledMultiSelect.Default is not null) - { - writer.WritePropertyName("default"); - JsonSerializer.Serialize(writer, untitledMultiSelect.Default, McpJsonUtilities.JsonContext.Default.IListString); - } - break; - - case TitledMultiSelectEnumSchema titledMultiSelect: - if (titledMultiSelect.MinItems.HasValue) - { - writer.WriteNumber("minItems", titledMultiSelect.MinItems.Value); - } - if (titledMultiSelect.MaxItems.HasValue) - { - writer.WriteNumber("maxItems", titledMultiSelect.MaxItems.Value); - } - writer.WritePropertyName("items"); - SerializeTitledEnumItemsSchema(writer, titledMultiSelect.Items); - if (titledMultiSelect.Default is not null) - { - writer.WritePropertyName("default"); - JsonSerializer.Serialize(writer, titledMultiSelect.Default, McpJsonUtilities.JsonContext.Default.IListString); - } - break; - -#pragma warning disable CS0618 // Type or member is obsolete - case LegacyTitledEnumSchema legacyEnum: -#pragma warning restore CS0618 // Type or member is obsolete - if (legacyEnum.Enum is not null) + case EnumSchema enumSchema: + if (enumSchema.Enum is not null) { writer.WritePropertyName("enum"); - JsonSerializer.Serialize(writer, legacyEnum.Enum, McpJsonUtilities.JsonContext.Default.IListString); + JsonSerializer.Serialize(writer, enumSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString); } - if (legacyEnum.EnumNames is not null) + if (enumSchema.EnumNames is not null) { writer.WritePropertyName("enumNames"); - JsonSerializer.Serialize(writer, legacyEnum.EnumNames, McpJsonUtilities.JsonContext.Default.IListString); + JsonSerializer.Serialize(writer, enumSchema.EnumNames, McpJsonUtilities.JsonContext.Default.IListString); } - if (legacyEnum.Default is not null) + if (enumSchema.Default is not null) { - writer.WriteString("default", legacyEnum.Default); + writer.WriteString("default", enumSchema.Default); } break; @@ -583,36 +340,6 @@ public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition valu writer.WriteEndObject(); } - - private static void SerializeEnumOptions(Utf8JsonWriter writer, IList options) - { - writer.WriteStartArray(); - foreach (var option in options) - { - writer.WriteStartObject(); - writer.WriteString("const", option.Const); - writer.WriteString("title", option.Title); - writer.WriteEndObject(); - } - writer.WriteEndArray(); - } - - private static void SerializeUntitledEnumItemsSchema(Utf8JsonWriter writer, UntitledEnumItemsSchema itemsSchema) - { - writer.WriteStartObject(); - writer.WriteString("type", itemsSchema.Type); - writer.WritePropertyName("enum"); - JsonSerializer.Serialize(writer, itemsSchema.Enum, McpJsonUtilities.JsonContext.Default.IListString); - writer.WriteEndObject(); - } - - private static void SerializeTitledEnumItemsSchema(Utf8JsonWriter writer, TitledEnumItemsSchema itemsSchema) - { - writer.WriteStartObject(); - writer.WritePropertyName("anyOf"); - SerializeEnumOptions(writer, itemsSchema.AnyOf); - writer.WriteEndObject(); - } } } @@ -740,12 +467,8 @@ public override string Type public bool? Default { get; set; } } - /// - /// Represents a legacy schema for an enum type with enumNames. - /// This schema is deprecated in favor of . - /// - [Obsolete("Use TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")] - public class LegacyTitledEnumSchema : PrimitiveSchemaDefinition + /// Represents a schema for an enum type. + public sealed class EnumSchema : PrimitiveSchemaDefinition { /// [JsonPropertyName("type")] @@ -782,196 +505,4 @@ public IList Enum [JsonPropertyName("default")] public string? Default { get; set; } } - - /// - /// Represents a schema for single-selection enumeration without display titles for options. - /// - public sealed class UntitledSingleSelectEnumSchema : PrimitiveSchemaDefinition - { - /// - [JsonPropertyName("type")] - public override string Type - { - get => "string"; - set - { - if (value is not "string") - { - throw new ArgumentException("Type must be 'string'.", nameof(value)); - } - } - } - - /// Gets or sets the list of allowed string values for the enum. - [JsonPropertyName("enum")] - [field: MaybeNull] - public IList Enum - { - get => field ??= []; - set - { - Throw.IfNull(value); - field = value; - } - } - - /// Gets or sets the default value for the enum. - [JsonPropertyName("default")] - public string? Default { get; set; } - } - - /// - /// Represents a single option in a titled enum schema with a constant value and display title. - /// - public sealed class EnumSchemaOption - { - /// Gets or sets the constant value for this option. - [JsonPropertyName("const")] - public required string Const { get; set; } - - /// Gets or sets the display title for this option. - [JsonPropertyName("title")] - public required string Title { get; set; } - } - - /// - /// Represents a schema for single-selection enumeration with display titles for each option. - /// - public sealed class TitledSingleSelectEnumSchema : PrimitiveSchemaDefinition - { - /// - [JsonPropertyName("type")] - public override string Type - { - get => "string"; - set - { - if (value is not "string") - { - throw new ArgumentException("Type must be 'string'.", nameof(value)); - } - } - } - - /// Gets or sets the list of enum options with their constant values and display titles. - [JsonPropertyName("oneOf")] - [field: MaybeNull] - public IList OneOf - { - get => field ??= []; - set - { - Throw.IfNull(value); - field = value; - } - } - - /// Gets or sets the default value for the enum. - [JsonPropertyName("default")] - public string? Default { get; set; } - } - - /// - /// Represents the items schema for untitled multi-select enum arrays. - /// - public sealed class UntitledEnumItemsSchema - { - /// Gets or sets the type of the items. - [JsonPropertyName("type")] - public string Type { get; set; } = "string"; - - /// Gets or sets the list of allowed string values. - [JsonPropertyName("enum")] - public required IList Enum { get; set; } - } - - /// - /// Represents the items schema for titled multi-select enum arrays. - /// - public sealed class TitledEnumItemsSchema - { - /// Gets or sets the list of enum options with constant values and display titles. - [JsonPropertyName("anyOf")] - public required IList AnyOf { get; set; } - } - - /// - /// Represents a schema for multiple-selection enumeration without display titles for options. - /// - public sealed class UntitledMultiSelectEnumSchema : PrimitiveSchemaDefinition - { - /// - [JsonPropertyName("type")] - public override string Type - { - get => "array"; - set - { - if (value is not "array") - { - throw new ArgumentException("Type must be 'array'.", nameof(value)); - } - } - } - - /// Gets or sets the minimum number of items that can be selected. - [JsonPropertyName("minItems")] - public int? MinItems { get; set; } - - /// Gets or sets the maximum number of items that can be selected. - [JsonPropertyName("maxItems")] - public int? MaxItems { get; set; } - - /// Gets or sets the schema for items in the array. - [JsonPropertyName("items")] - public required UntitledEnumItemsSchema Items { get; set; } - - /// Gets or sets the default values for the enum. - [JsonPropertyName("default")] - public IList? Default { get; set; } - } - - /// - /// Represents a schema for multiple-selection enumeration with display titles for each option. - /// - public sealed class TitledMultiSelectEnumSchema : PrimitiveSchemaDefinition - { - /// - [JsonPropertyName("type")] - public override string Type - { - get => "array"; - set - { - if (value is not "array") - { - throw new ArgumentException("Type must be 'array'.", nameof(value)); - } - } - } - - /// Gets or sets the minimum number of items that can be selected. - [JsonPropertyName("minItems")] - public int? MinItems { get; set; } - - /// Gets or sets the maximum number of items that can be selected. - [JsonPropertyName("maxItems")] - public int? MaxItems { get; set; } - - /// Gets or sets the schema for items in the array. - [JsonPropertyName("items")] - public required TitledEnumItemsSchema Items { get; set; } - - /// Gets or sets the default values for the enum. - [JsonPropertyName("default")] - public IList? Default { get; set; } - } - - /// - /// Represents a schema for an enum type. This is a compatibility alias for . - /// - [Obsolete("Use UntitledSingleSelectEnumSchema or TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")] - public sealed class EnumSchema : LegacyTitledEnumSchema - { - } } diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs index 5be40cf10..1515be25c 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitResult.cs @@ -51,7 +51,7 @@ public sealed class ElicitResult : Result /// /// /// Values in the dictionary should be of types , , - /// , , or (for multi-select enums). + /// , or . /// /// [JsonPropertyName("content")] diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs index 361179831..fa1773e42 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs @@ -1,4 +1,7 @@ +using System.ComponentModel; using System.Text.Json.Serialization; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Client; namespace ModelContextProtocol.Protocol; @@ -11,23 +14,14 @@ 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. +/// +/// +/// This class is intentionally empty as the Model Context Protocol specification does not +/// currently define additional properties for sampling capabilities. Future versions of the +/// specification may extend this capability with additional configuration options. /// /// public sealed class SamplingCapability { - /// - /// Gets or sets whether the client supports context inclusion via includeContext parameter. - /// - /// - /// If not declared, servers should only use includeContext: "none". - /// - [JsonPropertyName("context")] - public SamplingContextCapability? Context { get; set; } - - /// - /// Gets or sets whether the client supports tool use via tools and toolChoice parameters. - /// - [JsonPropertyName("tools")] - public SamplingToolsCapability? Tools { get; set; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs deleted file mode 100644 index bae960f3a..000000000 --- a/src/ModelContextProtocol.Core/Protocol/SamplingContextCapability.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ModelContextProtocol.Protocol; - -/// -/// Represents the sampling context capability. -/// -public sealed class SamplingContextCapability; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs index 093824e47..60db179cc 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingMessage.cs @@ -1,4 +1,3 @@ -using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace ModelContextProtocol.Protocol; @@ -9,8 +8,7 @@ namespace ModelContextProtocol.Protocol; /// /// /// A encapsulates content sent to or received from AI models in the Model Context Protocol. -/// The message has a role ( or ) and content which can be text, images, -/// audio, tool uses, or tool results. +/// Each message has a specific role ( or ) and contains content which can be text or images. /// /// /// objects are typically used in collections within @@ -18,9 +16,8 @@ namespace ModelContextProtocol.Protocol; /// within the Model Context Protocol. /// /// -/// If content contains any , then all content items -/// must be . Tool results cannot be mixed with text, image, or -/// audio content in the same message. +/// While similar to , the is focused on direct LLM sampling +/// operations rather than the enhanced resource embedding capabilities provided by . /// /// /// See the schema for details. @@ -32,21 +29,11 @@ public sealed class SamplingMessage /// Gets or sets the content of the message. /// [JsonPropertyName("content")] - [JsonConverter(typeof(SingleItemOrListConverter))] - public required IList Content { get; set; } + public required ContentBlock Content { get; set; } /// - /// Gets or sets the role of the message sender. + /// Gets or sets the role of the message sender, indicating whether it's from a "user" or an "assistant". /// [JsonPropertyName("role")] - public Role Role { get; set; } = Role.User; - - /// - /// Gets or sets metadata reserved by MCP for protocol-level metadata. - /// - /// - /// Implementations must not make assumptions about its contents. - /// - [JsonPropertyName("_meta")] - public JsonObject? Meta { get; set; } + public required Role Role { get; set; } } diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs deleted file mode 100644 index f93b79725..000000000 --- a/src/ModelContextProtocol.Core/Protocol/SamplingToolsCapability.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ModelContextProtocol.Protocol; - -/// -/// Represents the sampling tools capability. -/// -public sealed class SamplingToolsCapability; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs b/src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs deleted file mode 100644 index 497062e7e..000000000 --- a/src/ModelContextProtocol.Core/Protocol/SingleItemOrListConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.ComponentModel; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace ModelContextProtocol.Protocol; - -/// -/// JSON converter for that handles both array and single object representations. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class SingleItemOrListConverter : JsonConverter> - where T : class -{ - /// - public override IList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - return null; - } - - if (reader.TokenType == JsonTokenType.StartArray) - { - List list = []; - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - if (JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(T))) is T item) - { - list.Add(item); - } - } - - return list; - } - - if (reader.TokenType == JsonTokenType.StartObject) - { - return JsonSerializer.Deserialize(ref reader, options.GetTypeInfo(typeof(T))) is T item ? [item] : []; - } - - throw new JsonException($"Unexpected token type: {reader.TokenType}. Expected StartArray or StartObject."); - } - - /// - public override void Write(Utf8JsonWriter writer, IList value, JsonSerializerOptions options) - { - switch (value) - { - case null: - writer.WriteNullValue(); - return; - - case { Count: 1 }: - JsonSerializer.Serialize(writer, value[0], options.GetTypeInfo(typeof(object))); - return; - - default: - writer.WriteStartArray(); - foreach (var item in value) - { - JsonSerializer.Serialize(writer, item, options.GetTypeInfo(typeof(object))); - } - writer.WriteEndArray(); - return; - } - } -} diff --git a/src/ModelContextProtocol.Core/Protocol/ToolChoice.cs b/src/ModelContextProtocol.Core/Protocol/ToolChoice.cs deleted file mode 100644 index 41b387494..000000000 --- a/src/ModelContextProtocol.Core/Protocol/ToolChoice.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ModelContextProtocol.Protocol; - -/// -/// Controls tool selection behavior for sampling requests. -/// -public sealed class ToolChoice -{ - /// - /// Gets or sets the mode controlling which tools the model can call. - /// - /// - /// - /// "auto"Model decides whether to call tools (default) - /// "required"Model must call at least one tool - /// "none"Model must not call any tools - /// - /// - [JsonPropertyName("mode")] - public string? Mode { get; set; } - - /// The mode value "auto". - internal const string ModeAuto = "auto"; - - /// The mode value "required". - internal const string ModeRequired = "required"; - - /// The mode value "none". - internal const string ModeNone = "none"; -} - diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index f714bb184..e3d1271d0 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -249,7 +249,7 @@ public override async ValueTask InvokeAsync( { AIContent aiContent => new() { - Content = [aiContent.ToContentBlock()], + Content = [aiContent.ToContent()], StructuredContent = structuredContent, IsError = aiContent is ErrorContent }, @@ -491,7 +491,7 @@ private static CallToolResult ConvertAIContentEnumerableToCallToolResult(IEnumer foreach (var item in contentItems) { - contentList.Add(item.ToContentBlock()); + contentList.Add(item.ToContent()); hasAny = true; if (allErrorContent && item is not ErrorContent) diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index abccf2abb..877b9cda7 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -104,25 +104,41 @@ public async Task SampleAsync( continue; } - Role role = message.Role == ChatRole.Assistant ? Role.Assistant : Role.User; - - // Group all content blocks from this message into a single SamplingMessage - List contentBlocks = []; - foreach (var content in message.Contents) + if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) { - if (content.ToContentBlock() is { } contentBlock) - { - contentBlocks.Add(contentBlock); - } - } + Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; - if (contentBlocks.Count > 0) - { - samplingMessages.Add(new() + foreach (var content in message.Contents) { - Role = role, - Content = contentBlocks, - }); + switch (content) + { + case TextContent textContent: + samplingMessages.Add(new() + { + Role = role, + Content = new TextContentBlock { Text = textContent.Text }, + }); + break; + + case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): + samplingMessages.Add(new() + { + Role = role, + Content = dataContent.HasTopLevelMediaType("image") ? + new ImageContentBlock + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + } : + new AudioContentBlock + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + }, + }); + break; + } + } } } @@ -132,63 +148,25 @@ public async Task SampleAsync( modelPreferences = new() { Hints = [new() { Name = modelId }] }; } - IList? tools = null; - if (options?.Tools is { Count: > 0 }) - { - foreach (var tool in options.Tools) - { - if (tool is AIFunctionDeclaration af) - { - (tools ??= []).Add(new() - { - Name = af.Name, - Description = af.Description, - InputSchema = af.JsonSchema, - Meta = af.AdditionalProperties.ToJsonObject(), - }); - } - } - } - - ToolChoice? toolChoice = options?.ToolMode switch - { - NoneChatToolMode => new() { Mode = ToolChoice.ModeNone }, - AutoChatToolMode => new() { Mode = ToolChoice.ModeAuto }, - RequiredChatToolMode => new() { Mode = ToolChoice.ModeRequired }, - _ => null, - }; - var result = await SampleAsync(new() { - MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, Messages = samplingMessages, - ModelPreferences = modelPreferences, + MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, StopSequences = options?.StopSequences?.ToArray(), SystemPrompt = systemPrompt?.ToString(), Temperature = options?.Temperature, - ToolChoice = toolChoice, - Tools = tools, - Meta = options?.AdditionalProperties?.ToJsonObject(), + ModelPreferences = modelPreferences, }, cancellationToken).ConfigureAwait(false); - List responseContents = []; - foreach (var block in result.Content) - { - if (block.ToAIContent() is { } content) - { - responseContents.Add(content); - } - } + AIContent? responseContent = result.Content.ToAIContent(); - return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContents)) + return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) { ModelId = result.Model, FinishReason = result.StopReason switch { - CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length, - CreateMessageResult.StopReasonToolUse => ChatFinishReason.ToolCalls, - CreateMessageResult.StopReasonEndTurn or CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop, - _ => null, + "maxTokens" => ChatFinishReason.Length, + "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, } }; } diff --git a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs index 18272ffe6..dafdd7f7c 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs @@ -46,7 +46,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are bound directly to the instance associated -/// with this request's . Such parameters may be used to understand +/// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs index 1dcf7bcf2..7d9f877a9 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs @@ -37,7 +37,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are bound directly to the instance associated -/// with this request's . Such parameters may be used to understand +/// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs index 752af4983..3281bef16 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptCreateOptions.cs @@ -26,7 +26,7 @@ public sealed class McpServerPromptCreateOptions /// Gets or sets optional services used in the construction of the . /// /// - /// These services will be used to determine which parameters should be satisfied from dependency injection. As such, + /// These services will be used to determine which parameters should be satisifed from dependency injection. As such, /// what services are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. /// public IServiceProvider? Services { get; set; } @@ -46,7 +46,7 @@ public sealed class McpServerPromptCreateOptions public string? Title { get; set; } /// - /// Gets or sets the description to use for the . + /// Gets or set the description to use for the . /// /// /// If , but a is applied to the method, diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs index 34a08a18d..c52af48f0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceCreateOptions.cs @@ -26,7 +26,7 @@ public sealed class McpServerResourceCreateOptions /// Gets or sets optional services used in the construction of the . /// /// - /// These services will be used to determine which parameters should be satisfied from dependency injection. As such, + /// These services will be used to determine which parameters should be satisifed from dependency injection. As such, /// what services are satisfied via this provider should match what's satisfied via the provider passed in at invocation time. /// public IServiceProvider? Services { get; set; } @@ -51,12 +51,12 @@ public sealed class McpServerResourceCreateOptions public string? Name { get; set; } /// - /// Gets or sets the title to use for the . + /// Gets or sets the title to use for the . /// public string? Title { get; set; } /// - /// Gets or sets the description to use for the . + /// Gets or set the description to use for the . /// /// /// If , but a is applied to the member, diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index 987424b0d..6948ea912 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -99,7 +99,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// @@ -111,7 +111,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs index cf5455587..9e71e0eab 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs @@ -90,7 +90,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// Converted to a single object using . +/// Converted to a single object using . /// /// /// @@ -106,7 +106,7 @@ namespace ModelContextProtocol.Server; /// /// /// of -/// Each is converted to a object using . +/// Each is converted to a object using . /// /// /// of diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs index b029e5d43..e4e1d9330 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -41,7 +41,7 @@ public sealed class McpServerToolCreateOptions public string? Name { get; set; } /// - /// Gets or sets the description to use for the . + /// Gets or set the description to use for the . /// /// /// If , but a is applied to the method, diff --git a/tests/Common/Utils/TestServerTransport.cs b/tests/Common/Utils/TestServerTransport.cs index 51682ba60..f875fe504 100644 --- a/tests/Common/Utils/TestServerTransport.cs +++ b/tests/Common/Utils/TestServerTransport.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Protocol; +using ModelContextProtocol.Protocol; using System.Text.Json; using System.Threading.Channels; @@ -74,7 +74,7 @@ private async Task SamplingAsync(JsonRpcRequest request, CancellationToken cance await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model"}, McpJsonUtilities.DefaultOptions), + Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new TextContentBlock { Text = "" }, Model = "model", Role = Role.User }, McpJsonUtilities.DefaultOptions), }, cancellationToken); } diff --git a/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj b/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj deleted file mode 100644 index 430db1b02..000000000 --- a/tests/ModelContextProtocol.Analyzers.Tests/ModelContextProtocol.Analyzers.Tests.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - net9.0 - enable - true - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs b/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs deleted file mode 100644 index 2439f894f..000000000 --- a/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs +++ /dev/null @@ -1,1545 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using System.Diagnostics.CodeAnalysis; -using Xunit; - -namespace ModelContextProtocol.Analyzers.Tests; - -public partial class XmlToDescriptionGeneratorTests -{ - [Fact] - public void Generator_WithSummaryOnly_GeneratesMethodDescription() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test tool description - /// - [McpServerTool] - public static partial string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test tool description")] - public static partial string TestMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithSummaryAndRemarks_CombinesInMethodDescription() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test tool summary - /// - /// - /// Additional remarks - /// - [McpServerTool] - public static partial string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test tool summary\nAdditional remarks")] - public static partial string TestMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithParameterDocs_GeneratesParameterDescriptions() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test tool - /// - /// Input parameter description - /// Count parameter description - [McpServerTool] - public static partial string TestMethod(string input, int count) - { - return input; - } - } - """); - - Assert.True(result.Success); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test tool")] - public static partial string TestMethod([Description("Input parameter description")] string input, [Description("Count parameter description")] int count); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithReturnDocs_GeneratesReturnDescription() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test tool - /// - /// The result of the operation - [McpServerTool] - public static partial string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test tool")] - [return: Description("The result of the operation")] - public static partial string TestMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithExistingMethodDescription_DoesNotGenerateMethodDescription() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test tool summary - /// - /// Result - [McpServerTool] - [Description("Already has description")] - public static partial string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [return: Description("Result")] - public static partial string TestMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithExistingParameterDescription_SkipsThatParameter() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test tool - /// - /// Input description - /// Count description - [McpServerTool] - public static partial string TestMethod(string input, [Description("Already has")] int count) - { - return input; - } - } - """); - - Assert.True(result.Success); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test tool")] - public static partial string TestMethod([Description("Input description")] string input, int count); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithoutMcpServerToolAttribute_DoesNotGenerate() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - public partial class TestTools - { - /// - /// Test tool - /// - public static partial string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - } - - [Fact] - public void Generator_WithoutPartialKeyword_DoesNotGenerate() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// - /// Test tool - /// - [McpServerTool] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - } - - [Fact] - public void Generator_NonPartialMethodWithXmlDocs_ReportsMCP002Diagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// - /// Test tool with documentation - /// - [McpServerTool] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should report MCP002 diagnostic - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - Assert.Contains("TestMethod", diagnostic.GetMessage()); - Assert.Contains("partial", diagnostic.GetMessage()); - } - - [Fact] - public void Generator_NonPartialMethodWithParameterDocs_ReportsMCP002Diagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// Input parameter - [McpServerTool] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should report MCP002 diagnostic because parameter has documentation - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - } - - [Fact] - public void Generator_NonPartialMethodWithReturnDocs_ReportsMCP002Diagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// Return value - [McpServerTool] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should report MCP002 diagnostic because return has documentation - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - } - - [Fact] - public void Generator_NonPartialMethodWithoutXmlDocs_DoesNotReportDiagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - [McpServerTool] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should NOT report MCP002 diagnostic because there's no XML documentation - Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); - } - - [Fact] - public void Generator_NonPartialMethodWithEmptyXmlDocs_DoesNotReportDiagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// - [McpServerTool] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should NOT report MCP002 diagnostic because XML documentation is empty - Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); - } - - [Fact] - public void Generator_NonPartialMethodWithExistingDescriptions_DoesNotReportDiagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// Test tool - /// Input param - /// Return value - [McpServerTool] - [Description("Already has method description")] - [return: Description("Already has return description")] - public static string TestMethod([Description("Already has param description")] string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should NOT report MCP002 diagnostic because all descriptions already exist - Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002"); - } - - [Fact] - public void Generator_NonPartialMethodWithPartialExistingDescriptions_ReportsMCP002Diagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public class TestTools - { - /// Test tool - /// Input param - [McpServerTool] - [Description("Already has method description")] - public static string TestMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should report MCP002 diagnostic because parameter description would be generated - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - } - - [Fact] - public void Generator_NonPartialPromptWithXmlDocs_ReportsMCP002Diagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerPromptType] - public class TestPrompts - { - /// - /// Test prompt - /// - [McpServerPrompt] - public static string TestPrompt(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should report MCP002 diagnostic for prompts too - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - } - - [Fact] - public void Generator_NonPartialResourceWithXmlDocs_ReportsMCP002Diagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerResourceType] - public class TestResources - { - /// - /// Test resource - /// - [McpServerResource("test://resource")] - public static string TestResource(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Empty(result.GeneratedSources); - - // Should report MCP002 diagnostic for resources too - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - } - - [Fact] - public void Generator_WithSpecialCharacters_EscapesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test with "quotes", \backslash, newline - /// and tab characters. - /// - /// Parameter with "quotes" - [McpServerTool] - public static partial string TestEscaping(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test with \"quotes\", \\backslash, newline and tab characters.")] - public static partial string TestEscaping([Description("Parameter with \"quotes\"")] string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithInvalidXml_GeneratesPartialAndReportsDiagnostic() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test with - [McpServerTool] - public static partial string TestInvalidXml(string input) - { - return input; - } - } - """); - - // Should not throw, generates partial implementation without Description attributes - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - public static partial string TestInvalidXml(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - - // Should report a warning diagnostic - var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP001"); - Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); - Assert.Contains("invalid", diagnostic.GetMessage(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void Generator_WithGenericType_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Test generic - /// - [McpServerTool] - public static partial string TestGeneric(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Test generic")] - public static partial string TestGeneric(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithEmptyXmlComments_GeneratesPartialWithoutDescription() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// - [McpServerTool] - public static partial string TestEmpty(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - public static partial string TestEmpty(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithMultilineComments_CombinesIntoSingleLine() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// First line - /// Second line - /// Third line - /// - [McpServerTool] - public static partial string TestMultiline(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("First line Second line Third line")] - public static partial string TestMultiline(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithParametersOnly_GeneratesParameterDescriptions() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// Input parameter - /// Count parameter - [McpServerTool] - public static partial string TestMethod(string input, int count) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - public static partial string TestMethod([Description("Input parameter")] string input, [Description("Count parameter")] int count); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithNestedType_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - public partial class OuterClass - { - [McpServerToolType] - public partial class InnerClass - { - /// - /// Nested tool - /// - [McpServerTool] - public static partial string NestedMethod(string input) - { - return input; - } - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class OuterClass - { - partial class InnerClass - { - [Description("Nested tool")] - public static partial string NestedMethod(string input); - } - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithManyToolsAcrossMultipleNestedTypes_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test.Outer; - - [McpServerToolType] - public partial class RootTools - { - /// Root level tool 1 - [McpServerTool] - public static partial string RootTool1(string input) => input; - - /// Root level tool 2 - /// The count - [McpServerTool] - public static partial string RootTool2(string input, int count) => input; - } - - public partial class OuterContainer - { - [McpServerToolType] - public partial class Level2A - { - /// Level 2A tool - [McpServerTool] - public static partial string Level2ATool(string input) => input; - - public partial class Level3 - { - [McpServerToolType] - public partial class Level4 - { - /// Deep nested tool - /// The result - [McpServerTool] - public static partial string DeepTool(string input) => input; - } - } - } - - [McpServerToolType] - public partial class Level2B - { - /// Level 2B tool 1 - [McpServerTool] - public static partial string Level2BTool1(string input) => input; - - /// Level 2B tool 2 - [McpServerTool] - public static partial string Level2BTool2(string input) => input; - } - } - - namespace Test.Resources; - - [McpServerResourceType] - public partial class ResourceProviders - { - /// Test resource 1 - /// The path - [McpServerResource("test:///{path}")] - public static partial string Resource1(string path) => path; - - /// Test resource 2 - [McpServerResource("test2:///{id}")] - public static partial string Resource2(string id) => id; - } - - [McpServerPromptType] - public partial class GlobalPrompts - { - /// Global prompt - [McpServerPrompt] - public static partial string GlobalPrompt(string input) => input; - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test.Outer - { - partial class RootTools - { - [Description("Root level tool 1")] - public static partial string RootTool1(string input); - - [Description("Root level tool 2")] - public static partial string RootTool2(string input, [Description("The count")] int count); - } - - partial class OuterContainer - { - partial class Level2A - { - [Description("Level 2A tool")] - public static partial string Level2ATool(string input); - } - } - - partial class OuterContainer - { - partial class Level2A - { - partial class Level3 - { - partial class Level4 - { - [Description("Deep nested tool")] - [return: Description("The result")] - public static partial string DeepTool(string input); - } - } - } - } - - partial class OuterContainer - { - partial class Level2B - { - [Description("Level 2B tool 1")] - public static partial string Level2BTool1(string input); - - [Description("Level 2B tool 2")] - public static partial string Level2BTool2(string input); - } - } - } - - namespace Test.Outer.Test.Resources - { - partial class GlobalPrompts - { - [Description("Global prompt")] - public static partial string GlobalPrompt(string input); - } - - partial class ResourceProviders - { - [Description("Test resource 1")] - public static partial string Resource1([Description("The path")] string path); - - [Description("Test resource 2")] - public static partial string Resource2(string id); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithRecordClass_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial record TestTools - { - /// - /// Record tool - /// - [McpServerTool] - public static partial string RecordMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial record class TestTools - { - [Description("Record tool")] - public static partial string RecordMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithRecordStruct_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial record struct TestTools - { - /// - /// Record struct tool - /// - [McpServerTool] - public static partial string RecordStructMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial record struct TestTools - { - [Description("Record struct tool")] - public static partial string RecordStructMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithVirtualMethod_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public partial class TestTools - { - /// - /// Virtual tool - /// - [McpServerTool] - public virtual partial string VirtualMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Virtual tool")] - public virtual partial string VirtualMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithAbstractMethod_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerToolType] - public abstract partial class TestTools - { - /// - /// Abstract tool - /// - [McpServerTool] - public abstract partial string AbstractMethod(string input); - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestTools - { - [Description("Abstract tool")] - public abstract partial string AbstractMethod(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithMcpServerPrompt_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerPromptType] - public partial class TestPrompts - { - /// - /// Test prompt - /// - [McpServerPrompt] - public static partial string TestPrompt(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestPrompts - { - [Description("Test prompt")] - public static partial string TestPrompt(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithMcpServerResource_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - namespace Test; - - [McpServerResourceType] - public partial class TestResources - { - /// - /// Test resource - /// - [McpServerResource("test://resource")] - public static partial string TestResource(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - namespace Test - { - partial class TestResources - { - [Description("Test resource")] - public static partial string TestResource(string input); - } - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - [Fact] - public void Generator_WithGlobalNamespace_GeneratesCorrectly() - { - var result = RunGenerator(""" - using ModelContextProtocol.Server; - using System.ComponentModel; - - [McpServerToolType] - public partial class GlobalTools - { - /// - /// Tool in global namespace - /// - [McpServerTool] - public static partial string GlobalMethod(string input) - { - return input; - } - } - """); - - Assert.True(result.Success); - Assert.Single(result.GeneratedSources); - - var expected = $$""" - // - // ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}} - - #pragma warning disable - - using System.ComponentModel; - using ModelContextProtocol.Server; - - partial class GlobalTools - { - [Description("Tool in global namespace")] - public static partial string GlobalMethod(string input); - } - """; - - AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString()); - } - - private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source) - { - var syntaxTree = CSharpSyntaxTree.ParseText(source); - - // Get reference assemblies - we need to include all the basic runtime types - List referenceList = - [ - MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).Assembly.Location), - ]; - - // Add all necessary runtime assemblies - var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll"))); - referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll"))); - - // Try to find and add ModelContextProtocol.Core - try - { - var coreAssemblyPath = Path.Combine(AppContext.BaseDirectory, "ModelContextProtocol.Core.dll"); - if (File.Exists(coreAssemblyPath)) - { - referenceList.Add(MetadataReference.CreateFromFile(coreAssemblyPath)); - } - } - catch - { - // If we can't find it, the compilation will fail with appropriate errors - } - - var compilation = CSharpCompilation.Create( - "TestAssembly", - [syntaxTree], - referenceList, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - var driver = (CSharpGeneratorDriver)CSharpGeneratorDriver - .Create(new XmlToDescriptionGenerator()) - .RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); - - var runResult = driver.GetRunResult(); - - return new GeneratorRunResult - { - Success = !diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error), - GeneratedSources = runResult.GeneratedTrees.Select(t => (t.FilePath, t.GetText())).ToList(), - Diagnostics = diagnostics.ToList(), - Compilation = outputCompilation - }; - } - - private static void AssertGeneratedSourceEquals( - [StringSyntax("C#-test")] string expected, - [StringSyntax("C#-test")] string actual) - { - // Normalize line endings to \n, remove trailing whitespace from each line, and trim the end - static string Normalize(string s) - { - var lines = s.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); - for (int i = 0; i < lines.Length; i++) - { - lines[i] = lines[i].TrimEnd(); - } - return string.Join('\n', lines).TrimEnd(); - } - - var normalizedExpected = Normalize(expected); - var normalizedActual = Normalize(actual); - - Assert.Equal(normalizedExpected, normalizedActual); - } - - private class GeneratorRunResult - { - public bool Success { get; set; } - public List<(string FilePath, Microsoft.CodeAnalysis.Text.SourceText SourceText)> GeneratedSources { get; set; } = []; - public List Diagnostics { get; set; } = []; - public Compilation? Compilation { get; set; } - } -} diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index acf5d469e..78acaeb5e 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Client; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; @@ -259,7 +259,8 @@ public async Task Sampling_Sse_TestServer() return new CreateMessageResult { Model = "test-model", - Content = [new TextContentBlock { Text = "Test response" }], + Role = Role.Assistant, + Content = new TextContentBlock { Text = "Test response" }, }; }; await using var client = await GetClientAsync(options); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 2899851ef..0c71e56e3 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -143,7 +143,7 @@ public async Task ClaimsPrincipal_CanBeInjected_IntoToolMethod() } [Fact] - public async Task Sampling_DoesNotCloseStreamPrematurely() + public async Task Sampling_DoesNotCloseStream_Prematurely() { Assert.SkipWhen(Stateless, "Sampling is not supported in stateless mode."); @@ -172,14 +172,14 @@ public async Task Sampling_DoesNotCloseStreamPrematurely() Assert.NotNull(parameters?.Messages); var message = Assert.Single(parameters.Messages); Assert.Equal(Role.User, message.Role); - Assert.Equal("Test prompt for sampling", Assert.IsType(Assert.Single(message.Content)).Text); + 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" }], + Content = new TextContentBlock { Text = "Sampling response from client" }, }; } } @@ -285,7 +285,7 @@ public static async Task SamplingToolAsync(McpServer server, string prom new SamplingMessage { Role = Role.User, - Content = [new TextContentBlock { Text = prompt }], + Content = new TextContentBlock { Text = prompt }, } ], MaxTokens = 1000 @@ -294,7 +294,7 @@ public static async Task SamplingToolAsync(McpServer server, string prom await server.SampleAsync(samplingRequest, cancellationToken); var samplingResult = await server.SampleAsync(samplingRequest, cancellationToken); - return $"Sampling completed successfully. Client responded: {Assert.IsType(Assert.Single(samplingResult.Content)).Text}"; + return $"Sampling completed successfully. Client responded: {Assert.IsType(samplingResult.Content).Text}"; } } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj index 5d21d0a0a..19f4e4149 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj @@ -4,8 +4,7 @@ net10.0;net9.0;net8.0 enable enable - false - true + Exe ModelContextProtocol.AspNetCore.Tests @@ -14,41 +13,25 @@ false - - - true - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index e4b797ee6..9a54ed71d 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; @@ -197,7 +197,7 @@ private static void ConfigureTools(McpServerOptions options, string? cliArg) return new CallToolResult { - Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}" }] + Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] }; } else if (request.Params?.Name == "echoCliArg") @@ -521,7 +521,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage { Role = Role.User, - Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index 53537c2b8..183a64e7e 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Serilog; @@ -46,7 +46,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st Messages = [new SamplingMessage { Role = Role.User, - Content = [new TextContentBlock { Text = $"Resource {uri} context: {context}" }], + Content = new TextContentBlock { Text = $"Resource {uri} context: {context}" }, }], SystemPrompt = "You are a helpful test server.", MaxTokens = maxTokens, @@ -191,7 +191,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st return new CallToolResult { - Content = [new TextContentBlock { Text = $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}" }] + Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }] }; } else @@ -339,7 +339,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st }); messages.Add(new PromptMessage { - Role = Role.User, + 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 diff --git a/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs b/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs index 3a57a07c6..ec603c63f 100644 --- a/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/AIContentExtensionsTests.cs @@ -25,127 +25,4 @@ public void CallToolResult_ToChatMessage_ProducesExpectedAIContent() JsonElement result = Assert.IsType(frc.Result); Assert.Contains("This is a test message.", result.ToString()); } - - [Fact] - public void ToAIContent_ConvertsToolUseContentBlock() - { - Dictionary inputDict = new() { ["city"] = "Paris", ["units"] = "metric" }; - ToolUseContentBlock toolUse = new() - { - Id = "call_abc123", - Name = "get_weather", - Input = JsonSerializer.SerializeToElement(inputDict, McpJsonUtilities.DefaultOptions) - }; - - AIContent? aiContent = toolUse.ToAIContent(); - - var functionCall = Assert.IsType(aiContent); - Assert.Equal("call_abc123", functionCall.CallId); - Assert.Equal("get_weather", functionCall.Name); - Assert.NotNull(functionCall.Arguments); - - var cityArg = Assert.IsType(functionCall.Arguments["city"]); - Assert.Equal("Paris", cityArg.GetString()); - var unitsArg = Assert.IsType(functionCall.Arguments["units"]); - Assert.Equal("metric", unitsArg.GetString()); - } - - [Fact] - public void ToAIContent_ConvertsToolResultContentBlock() - { - ToolResultContentBlock toolResult = new() - { - ToolUseId = "call_abc123", - Content = [new TextContentBlock { Text = "Weather: 18°C" }], - IsError = false - }; - - AIContent? aiContent = toolResult.ToAIContent(); - - var functionResult = Assert.IsType(aiContent); - Assert.Equal("call_abc123", functionResult.CallId); - Assert.Null(functionResult.Exception); - Assert.NotNull(functionResult.Result); - } - - [Fact] - public void ToAIContent_ConvertsToolResultContentBlockWithError() - { - ToolResultContentBlock toolResult = new() - { - ToolUseId = "call_abc123", - Content = [new TextContentBlock { Text = "Error: Invalid city" }], - IsError = true - }; - - AIContent? aiContent = toolResult.ToAIContent(); - - var functionResult = Assert.IsType(aiContent); - Assert.Equal("call_abc123", functionResult.CallId); - Assert.NotNull(functionResult.Exception); - } - - [Fact] - public void ToAIContent_ConvertsToolResultWithMultipleContent() - { - ToolResultContentBlock toolResult = new() - { - ToolUseId = "call_123", - Content = - [ - new TextContentBlock { Text = "Text result" }, - new ImageContentBlock { Data = Convert.ToBase64String([1, 2, 3]), MimeType = "image/png" } - ] - }; - - AIContent? aiContent = toolResult.ToAIContent(); - - var functionResult = Assert.IsType(aiContent); - Assert.Equal("call_123", functionResult.CallId); - - var resultList = Assert.IsAssignableFrom>(functionResult.Result); - Assert.Equal(2, resultList.Count); - Assert.IsType(resultList[0]); - Assert.IsType(resultList[1]); - } - - [Fact] - public void ToAIContent_ToolUseToFunctionCallRoundTrip() - { - Dictionary inputDict = new() { ["param1"] = "value1", ["param2"] = 42 }; - ToolUseContentBlock original = new() - { - Id = "call_123", - Name = "test_tool", - Input = JsonSerializer.SerializeToElement(inputDict, McpJsonUtilities.DefaultOptions) - }; - - var functionCall = Assert.IsType(original.ToAIContent()); - - Assert.Equal("call_123", functionCall.CallId); - Assert.Equal("test_tool", functionCall.Name); - Assert.NotNull(functionCall.Arguments); - - var param1 = Assert.IsType(functionCall.Arguments["param1"]); - Assert.Equal("value1", param1.GetString()); - var param2 = Assert.IsType(functionCall.Arguments["param2"]); - Assert.Equal(42, param2.GetInt32()); - } - - [Fact] - public void ToAIContent_ToolResultToFunctionResultRoundTrip() - { - ToolResultContentBlock original = new() - { - ToolUseId = "call_123", - Content = [new TextContentBlock { Text = "Result" }, new TextContentBlock { Text = "More data" }], - IsError = false - }; - - var functionResult = Assert.IsType(original.ToAIContent()); - - Assert.Equal("call_123", functionResult.CallId); - Assert.False(functionResult.Exception != null); - Assert.NotNull(functionResult.Result); - } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs index 504b52e21..0eb84262b 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs @@ -73,10 +73,10 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }, SamplingHandler = async (c, p, t) => new CreateMessageResult { - Content = [new TextContentBlock { Text = "result" }], + Content = new TextContentBlock { Text = "result" }, Model = "test-model", Role = Role.User, - StopReason = "endTurn", + StopReason = "endTurn" } } }; diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index 604c31b8b..09be7385e 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -5,7 +5,6 @@ using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Moq; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading.Channels; @@ -107,10 +106,10 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat { Messages = [ - new SamplingMessage + new SamplingMessage { Role = Role.User, - Content = [new TextContentBlock { Text = "Hello" }] + Content = new TextContentBlock { Text = "Hello" } } ], Temperature = temperature, @@ -135,14 +134,14 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) .Returns(expectedResponse); - var handler = mockChatClient.Object.CreateSamplingHandler(); + var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); // Act var result = await handler(requestParams, Mock.Of>(), cancellationToken); // Assert Assert.NotNull(result); - Assert.Equal("Hello, World!", result.Content.OfType().FirstOrDefault()?.Text); + Assert.Equal("Hello, World!", (result.Content as TextContentBlock)?.Text); Assert.Equal("test-model", result.Model); Assert.Equal(Role.Assistant, result.Role); Assert.Equal("endTurn", result.StopReason); @@ -157,14 +156,14 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages() { Messages = [ - new SamplingMessage + new SamplingMessage { Role = Role.User, - Content = [new ImageContentBlock + Content = new ImageContentBlock { MimeType = "image/png", Data = Convert.ToBase64String(new byte[] { 1, 2, 3 }) - }], + } } ], MaxTokens = 100 @@ -189,14 +188,14 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages() .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) .Returns(expectedResponse); - var handler = mockChatClient.Object.CreateSamplingHandler(); + var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); // Act var result = await handler(requestParams, Mock.Of>(), cancellationToken); // Assert Assert.NotNull(result); - Assert.Equal(expectedData, result.Content.OfType().FirstOrDefault()?.Data); + Assert.Equal(expectedData, (result.Content as ImageContentBlock)?.Data); Assert.Equal("test-model", result.Model); Assert.Equal(Role.Assistant, result.Role); Assert.Equal("endTurn", result.StopReason); @@ -223,7 +222,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() new SamplingMessage { Role = Role.User, - Content = [new EmbeddedResourceBlock { Resource = resource }], + Content = new EmbeddedResourceBlock { Resource = resource }, } ], MaxTokens = 100 @@ -248,7 +247,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) .Returns(expectedResponse); - var handler = mockChatClient.Object.CreateSamplingHandler(); + var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); // Act var result = await handler(requestParams, Mock.Of>(), cancellationToken); @@ -543,120 +542,4 @@ public async Task ReturnsNegotiatedProtocolVersion(string? protocolVersion) await using McpClient client = await CreateMcpClientForServer(new() { ProtocolVersion = protocolVersion }); Assert.Equal(protocolVersion ?? "2025-06-18", client.NegotiatedProtocolVersion); } - - [Fact] - public async Task EndToEnd_SamplingWithTools_ServerUsesIChatClientWithFunctionInvocation_ClientHandlesSamplingWithIChatClient() - { - int getWeatherToolCallCount = 0; - int askClientToolCallCount = 0; - - Server.ServerOptions.ToolCollection?.Add(McpServerTool.Create( - async (McpServer server, string query, CancellationToken cancellationToken) => - { - askClientToolCallCount++; - - var weatherTool = AIFunctionFactory.Create( - (string location) => - { - getWeatherToolCallCount++; - return $"Weather in {location}: sunny, 22°C"; - }, - "get_weather", "Gets the weather for a location"); - - var response = await server - .AsSamplingChatClient() - .AsBuilder() - .UseFunctionInvocation() - .Build() - .GetResponseAsync(query, new ChatOptions { Tools = [weatherTool] }, cancellationToken); - - return response.Text ?? "No response"; - }, - new() { Name = "ask_client", Description = "Asks the client a question using sampling" })); - - int samplingCallCount = 0; - TestChatClient testChatClient = new((messages, options, ct) => - { - int currentCall = samplingCallCount++; - var lastMessage = messages.LastOrDefault(); - - // First call: Return a tool call request for get_weather - if (currentCall == 0) - { - return Task.FromResult(new([ - new ChatMessage(ChatRole.User, messages.First().Contents), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_weather_123", "get_weather", new Dictionary { ["location"] = "Paris" })]) - ]) - { - ModelId = "test-model", - FinishReason = ChatFinishReason.ToolCalls - }); - } - // Second call (after tool result): Return final text response - else - { - var toolResult = lastMessage?.Contents.OfType().FirstOrDefault(); - Assert.NotNull(toolResult); - Assert.Equal("call_weather_123", toolResult.CallId); - - string resultText = toolResult.Result?.ToString() ?? string.Empty; - Assert.Contains("Weather in Paris: sunny", resultText); - - return Task.FromResult(new([ - new ChatMessage(ChatRole.User, messages.First().Contents), - new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("call_weather_123", "get_weather", new Dictionary { ["location"] = "Paris" })]), - new ChatMessage(ChatRole.User, [toolResult]), - new ChatMessage(ChatRole.Assistant, [new TextContent($"Based on the weather data: {resultText}")]) - ]) - { - ModelId = "test-model", - FinishReason = ChatFinishReason.Stop - }); - } - }); - - await using McpClient client = await CreateMcpClientForServer(new() - { - Handlers = new() { SamplingHandler = testChatClient.CreateSamplingHandler() }, - }); - - var result = await client.CallToolAsync( - "ask_client", - new Dictionary { ["query"] = "What's the weather in Paris?" }, - cancellationToken: TestContext.Current.CancellationToken); - Assert.NotNull(result); - Assert.Null(result.IsError); - - var textContent = result.Content.OfType().FirstOrDefault(); - Assert.NotNull(textContent); - Assert.Contains("Weather in Paris: sunny, 22", textContent.Text); - Assert.Equal(1, getWeatherToolCallCount); - Assert.Equal(1, askClientToolCallCount); - Assert.Equal(2, samplingCallCount); - } - - /// Simple test IChatClient implementation for testing. - private sealed class TestChatClient(Func, ChatOptions?, CancellationToken, Task> getResponse) : IChatClient - { - public Task GetResponseAsync( - IEnumerable messages, - ChatOptions? options = null, - CancellationToken cancellationToken = default) => - getResponse(messages, options, cancellationToken); - - async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( - IEnumerable messages, - ChatOptions? options, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); - foreach (var update in response.ToChatResponseUpdates()) - { - yield return update; - } - } - - object? IChatClient.GetService(Type serviceType, object? serviceKey) => null; - void IDisposable.Dispose() { } - } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs index 7f1cc3689..45d8a467f 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs @@ -352,10 +352,13 @@ public async Task ResourceLinkTool_ReturnsJsonElement() Assert.IsType(result); var jsonElement = (JsonElement)result!; - Assert.True(jsonElement.TryGetProperty("content", out var contentValue)); - Assert.Equal(JsonValueKind.Array, contentValue.ValueKind); + Assert.True(jsonElement.TryGetProperty("content", out var contentArray)); + Assert.Equal(JsonValueKind.Array, contentArray.ValueKind); + Assert.Equal(1, contentArray.GetArrayLength()); - Assert.Equal(1, contentValue.GetArrayLength()); + var firstContent = contentArray[0]; + Assert.True(firstContent.TryGetProperty("type", out var typeProperty)); + Assert.Equal("resource_link", typeProperty.GetString()); } [Fact] diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index ff6f56e24..16fad124a 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; @@ -383,7 +383,7 @@ public async Task Sampling_Stdio(string clientId) { Model = "test-model", Role = Role.Assistant, - Content = [new TextContentBlock { Text = "Test response" }], + Content = new TextContentBlock { Text = "Test response" }, }; } } diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index 31a8236f2..2d5ef5f2d 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Client; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Tests.Utils; @@ -81,7 +81,7 @@ public async Task Sampling_Sse_EverythingServer() { Model = "test-model", Role = Role.Assistant, - Content = [new TextContentBlock { Text = "Test response" }], + Content = new TextContentBlock { Text = "Test response" }, }; } } diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj index 425944624..6bdca3438 100644 --- a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj +++ b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj @@ -6,8 +6,6 @@ enable enable - false - true ModelContextProtocol.Tests $(NoWarn);NU1903;NU1902 @@ -18,13 +16,6 @@ false - - - true - - @@ -35,29 +26,20 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + diff --git a/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs index 0113b77f3..3d8d8ff18 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ContentBlockTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; using System.Text.Json; @@ -124,105 +125,4 @@ public void Deserialize_IgnoresUnknownObjectProperties() var textBlock = Assert.IsType(contentBlock); Assert.Contains("Sample text", textBlock.Text); } - - [Fact] - public void ToolResultContentBlock_WithError_SerializationRoundtrips() - { - ToolResultContentBlock toolResult = new() - { - ToolUseId = "call_123", - Content = [new TextContentBlock { Text = "Error: City not found" }], - IsError = true - }; - - var json = JsonSerializer.Serialize(toolResult, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - var result = Assert.IsType(deserialized); - Assert.Equal("call_123", result.ToolUseId); - Assert.True(result.IsError); - Assert.Single(result.Content); - var textBlock = Assert.IsType(result.Content[0]); - Assert.Equal("Error: City not found", textBlock.Text); - } - - [Fact] - public void ToolResultContentBlock_WithStructuredContent_SerializationRoundtrips() - { - ToolResultContentBlock toolResult = new() - { - ToolUseId = "call_123", - Content = - [ - new TextContentBlock { Text = "Result data" } - ], - StructuredContent = JsonElement.Parse("""{"temperature":18,"condition":"cloudy"}"""), - IsError = false - }; - - var json = JsonSerializer.Serialize(toolResult, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - var result = Assert.IsType(deserialized); - Assert.Equal("call_123", result.ToolUseId); - Assert.Single(result.Content); - var textBlock = Assert.IsType(result.Content[0]); - Assert.Equal("Result data", textBlock.Text); - Assert.NotNull(result.StructuredContent); - Assert.Equal(18, result.StructuredContent.Value.GetProperty("temperature").GetInt32()); - Assert.Equal("cloudy", result.StructuredContent.Value.GetProperty("condition").GetString()); - Assert.False(result.IsError); - } - - [Fact] - public void ToolResultContentBlock_SerializationRoundTrip() - { - ToolResultContentBlock toolResult = new() - { - ToolUseId = "call_123", - Content = - [ - new TextContentBlock { Text = "Result data" }, - new ImageContentBlock { Data = "base64data", MimeType = "image/png" } - ], - StructuredContent = JsonElement.Parse("""{"temperature":18,"condition":"cloudy"}"""), - IsError = false - }; - - var json = JsonSerializer.Serialize(toolResult, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - var result = Assert.IsType(deserialized); - Assert.Equal("call_123", result.ToolUseId); - Assert.Equal(2, result.Content.Count); - var textBlock = Assert.IsType(result.Content[0]); - Assert.Equal("Result data", textBlock.Text); - var imageBlock = Assert.IsType(result.Content[1]); - Assert.Equal("base64data", imageBlock.Data); - Assert.Equal("image/png", imageBlock.MimeType); - Assert.NotNull(result.StructuredContent); - Assert.Equal(18, result.StructuredContent.Value.GetProperty("temperature").GetInt32()); - Assert.Equal("cloudy", result.StructuredContent.Value.GetProperty("condition").GetString()); - Assert.False(result.IsError); - } - - [Fact] - public void ToolUseContentBlock_SerializationRoundTrip() - { - ToolUseContentBlock toolUse = new() - { - Id = "call_abc123", - Name = "get_weather", - Input = JsonElement.Parse("""{"city":"Paris","units":"metric"}""") - }; - - var json = JsonSerializer.Serialize(toolUse, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - var result = Assert.IsType(deserialized); - Assert.Equal("call_abc123", result.Id); - Assert.Equal("get_weather", result.Name); - Assert.Equal("Paris", result.Input.GetProperty("city").GetString()); - Assert.Equal("metric", result.Input.GetProperty("units").GetString()); - } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs deleted file mode 100644 index f57faf1d8..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/CreateMessageRequestParamsTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; - -namespace ModelContextProtocol.Tests.Protocol; - -public class CreateMessageRequestParamsTests -{ - [Fact] - public void WithTools_SerializationRoundtrips() - { - CreateMessageRequestParams requestParams = new() - { - MaxTokens = 1000, - Messages = - [ - new SamplingMessage - { - Role = Role.User, - Content = [new TextContentBlock { Text = "What's the weather in Paris?" }] - } - ], - Tools = - [ - new Tool - { - Name = "get_weather", - Description = "Get weather for a city", - InputSchema = JsonElement.Parse(""" - { - "type": "object", - "properties": { - "city": { "type": "string" } - }, - "required": ["city"] - } - """) - } - ], - ToolChoice = new ToolChoice { Mode = "auto" } - }; - - var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(1000, deserialized.MaxTokens); - Assert.NotNull(deserialized.Messages); - Assert.Single(deserialized.Messages); - Assert.Equal(Role.User, deserialized.Messages[0].Role); - Assert.Single(deserialized.Messages[0].Content); - var textContent = Assert.IsType(deserialized.Messages[0].Content[0]); - Assert.Equal("What's the weather in Paris?", textContent.Text); - Assert.NotNull(deserialized.Tools); - Assert.Single(deserialized.Tools); - Assert.Equal("get_weather", deserialized.Tools[0].Name); - Assert.Equal("Get weather for a city", deserialized.Tools[0].Description); - Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString()); - Assert.True(deserialized.Tools[0].InputSchema.GetProperty("properties").TryGetProperty("city", out var cityProp)); - Assert.Equal("string", cityProp.GetProperty("type").GetString()); - Assert.Single(deserialized.Tools[0].InputSchema.GetProperty("required").EnumerateArray()); - Assert.Equal("city", deserialized.Tools[0].InputSchema.GetProperty("required")[0].GetString()); - Assert.NotNull(deserialized.ToolChoice); - Assert.Equal("auto", deserialized.ToolChoice.Mode); - } - - [Fact] - public void WithToolChoiceRequired_SerializationRoundtrips() - { - CreateMessageRequestParams requestParams = new() - { - MaxTokens = 1000, - Messages = - [ - new SamplingMessage - { - Role = Role.User, - Content = [new TextContentBlock { Text = "What's the weather?" }] - } - ], - Tools = - [ - new Tool - { - Name = "get_weather", - Description = "Get weather for a city", - InputSchema = JsonElement.Parse(""" - { - "type": "object", - "properties": { "city": { "type": "string" } }, - "required": ["city"] - } - """) - } - ], - ToolChoice = new ToolChoice { Mode = "required" } - }; - - var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(1000, deserialized.MaxTokens); - Assert.NotNull(deserialized.Messages); - Assert.Single(deserialized.Messages); - Assert.Equal(Role.User, deserialized.Messages[0].Role); - Assert.Single(deserialized.Messages[0].Content); - var textContent = Assert.IsType(deserialized.Messages[0].Content[0]); - Assert.Equal("What's the weather?", textContent.Text); - Assert.NotNull(deserialized.Tools); - Assert.Single(deserialized.Tools); - Assert.Equal("get_weather", deserialized.Tools[0].Name); - Assert.Equal("Get weather for a city", deserialized.Tools[0].Description); - Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString()); - Assert.NotNull(deserialized.ToolChoice); - Assert.Equal("required", deserialized.ToolChoice.Mode); - } - - [Fact] - public void WithToolChoiceNone_SerializationRoundtrips() - { - CreateMessageRequestParams requestParams = new() - { - MaxTokens = 1000, - Messages = - [ - new SamplingMessage - { - Role = Role.User, - Content = [new TextContentBlock { Text = "What's the weather in Paris?" }] - } - ], - Tools = - [ - new Tool - { - Name = "get_weather", - Description = "Get weather for a city", - InputSchema = JsonElement.Parse(""" - { - "type": "object", - "properties": { "city": { "type": "string" } }, - "required": ["city"] - } - """) - } - ], - ToolChoice = new ToolChoice { Mode = "none" } - }; - - var json = JsonSerializer.Serialize(requestParams, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(1000, deserialized.MaxTokens); - Assert.NotNull(deserialized.Messages); - Assert.Single(deserialized.Messages); - Assert.Equal(Role.User, deserialized.Messages[0].Role); - Assert.Single(deserialized.Messages[0].Content); - var textContent = Assert.IsType(deserialized.Messages[0].Content[0]); - Assert.Equal("What's the weather in Paris?", textContent.Text); - Assert.NotNull(deserialized.Tools); - Assert.Single(deserialized.Tools); - Assert.Equal("get_weather", deserialized.Tools[0].Name); - Assert.Equal("Get weather for a city", deserialized.Tools[0].Description); - Assert.Equal("object", deserialized.Tools[0].InputSchema.GetProperty("type").GetString()); - Assert.NotNull(deserialized.ToolChoice); - Assert.Equal("none", deserialized.ToolChoice.Mode); - } -} - - - - - diff --git a/tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs b/tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs deleted file mode 100644 index 67ab5f4f9..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/CreateMessageResultTests.cs +++ /dev/null @@ -1,247 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace ModelContextProtocol.Tests.Protocol; - -public class CreateMessageResultTests -{ - [Fact] - public void CreateMessageResult_WithSingleContent_Serializes() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = [new TextContentBlock { Text = "Hello" }], - StopReason = "endTurn" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Single(deserialized.Content); - Assert.IsType(deserialized.Content[0]); - } - - [Fact] - public void CreateMessageResult_WithMultipleToolUses_Serializes() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = - [ - new ToolUseContentBlock - { - Id = "call_1", - Name = "tool1", - Input = JsonElement.Parse("""{}""") - }, - new ToolUseContentBlock - { - Id = "call_2", - Name = "tool2", - Input = JsonElement.Parse("""{}""") - } - ], - StopReason = "toolUse" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(2, deserialized.Content.Count); - Assert.All(deserialized.Content, c => Assert.IsType(c)); - Assert.Equal("call_1", ((ToolUseContentBlock)deserialized.Content[0]).Id); - Assert.Equal("call_2", ((ToolUseContentBlock)deserialized.Content[1]).Id); - } - - [Fact] - public void CreateMessageResult_WithMixedContent_Serializes() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = - [ - new TextContentBlock { Text = "Let me check that." }, - new ToolUseContentBlock - { - Id = "call_1", - Name = "tool1", - Input = JsonElement.Parse("""{}""") - } - ], - StopReason = "toolUse" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(2, deserialized.Content.Count); - Assert.IsType(deserialized.Content[0]); - Assert.IsType(deserialized.Content[1]); - } - - [Fact] - public void CreateMessageResult_EmptyContent_AllowedButUnusual() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = [], - StopReason = "endTurn" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Empty(deserialized.Content); - } - - [Fact] - public void CreateMessageResult_WithImageContent_Serializes() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = - [ - new ImageContentBlock - { - Data = Convert.ToBase64String([1, 2, 3, 4, 5]), - MimeType = "image/png" - } - ], - StopReason = "endTurn" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Single(deserialized.Content); - var imageBlock = Assert.IsType(deserialized.Content[0]); - Assert.Equal("image/png", imageBlock.MimeType); - } - - [Fact] - public void CreateMessageResult_RoundTripWithAllFields() - { - CreateMessageResult original = new() - { - Role = Role.Assistant, - Model = "claude-3-sonnet", - Content = - [ - new TextContentBlock { Text = "I'll help you with that." }, - new ToolUseContentBlock - { - Id = "call_xyz", - Name = "calculator", - Input = JsonElement.Parse("""{"operation":"add","a":5,"b":3}""") - } - ], - StopReason = "toolUse", - Meta = (JsonObject)JsonNode.Parse("""{"custom":"metadata"}""")! - }; - - var json = JsonSerializer.Serialize(original, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(Role.Assistant, deserialized.Role); - Assert.Equal("claude-3-sonnet", deserialized.Model); - Assert.Equal(2, deserialized.Content.Count); - Assert.Equal("toolUse", deserialized.StopReason); - Assert.NotNull(deserialized.Meta); - } - - [Fact] - public void CreateMessageResult_WithToolUse_SerializationRoundtrips() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = - [ - new ToolUseContentBlock - { - Id = "call_123", - Name = "get_weather", - Input = JsonElement.Parse("""{"city":"Paris"}""") - } - ], - StopReason = "toolUse" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(Role.Assistant, deserialized.Role); - Assert.Equal("test-model", deserialized.Model); - Assert.Equal("toolUse", deserialized.StopReason); - Assert.Single(deserialized.Content); - - var toolUse = Assert.IsType(deserialized.Content[0]); - Assert.Equal("call_123", toolUse.Id); - Assert.Equal("get_weather", toolUse.Name); - Assert.Equal("Paris", toolUse.Input.GetProperty("city").GetString()); - } - - [Fact] - public void CreateMessageResult_WithParallelToolUses_SerializationRoundtrips() - { - CreateMessageResult result = new() - { - Role = Role.Assistant, - Model = "test-model", - Content = - [ - new ToolUseContentBlock - { - Id = "call_abc123", - Name = "get_weather", - Input = JsonElement.Parse("""{"city":"Paris"}""") - }, - new ToolUseContentBlock - { - Id = "call_def456", - Name = "get_weather", - Input = JsonElement.Parse("""{"city":"London"}""") - } - ], - StopReason = "toolUse" - }; - - var json = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(Role.Assistant, deserialized.Role); - Assert.Equal("test-model", deserialized.Model); - Assert.Equal("toolUse", deserialized.StopReason); - Assert.Equal(2, deserialized.Content.Count); - - var toolUse1 = Assert.IsType(deserialized.Content[0]); - Assert.Equal("call_abc123", toolUse1.Id); - Assert.Equal("get_weather", toolUse1.Name); - Assert.Equal("Paris", toolUse1.Input.GetProperty("city").GetString()); - - var toolUse2 = Assert.IsType(deserialized.Content[1]); - Assert.Equal("call_def456", toolUse2.Id); - Assert.Equal("get_weather", toolUse2.Name); - Assert.Equal("London", toolUse2.Input.GetProperty("city").GetString()); - } -} diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs index c29f7d033..420c35f95 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs @@ -1,8 +1,6 @@ using ModelContextProtocol.Protocol; using System.Text.Json; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Protocol; public class ElicitationDefaultValuesTests @@ -261,8 +259,7 @@ public void PrimitiveSchemaDefinition_EnumSchema_WithDefault_RoundTrips() // Assert Assert.NotNull(deserialized); - // EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema - var enumSchema = Assert.IsType(deserialized); + var enumSchema = Assert.IsType(deserialized); Assert.Equal("draft", enumSchema.Default); Assert.Equal(["draft", "published", "archived"], enumSchema.Enum); } @@ -329,8 +326,7 @@ public void RequestSchema_WithAllDefaultTypes_Serializes_Correctly() var activeSchema = Assert.IsType(deserialized.RequestedSchema.Properties["active"]); Assert.True(activeSchema.Default); - // EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema - var statusSchema = Assert.IsType(deserialized.RequestedSchema.Properties["status"]); + var statusSchema = Assert.IsType(deserialized.RequestedSchema.Properties["status"]); Assert.Equal("active", statusSchema.Default); } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index fc4dacb78..76f967bed 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -3,8 +3,6 @@ using ModelContextProtocol.Protocol; using System.Text.Json; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Configuration; public partial class ElicitationTests : ClientServerTestBase diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs index b0cdbe3d9..b4afb1af3 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs @@ -4,8 +4,6 @@ using System.Text.Json; using System.Text.Json.Serialization; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Configuration; public partial class ElicitationTypedTests : ClientServerTestBase @@ -149,7 +147,7 @@ public async Task Can_Elicit_Typed_Information() break; case nameof(SampleForm.Role): - var enumSchema = Assert.IsType(value); + var enumSchema = Assert.IsType(value); Assert.Equal("string", enumSchema.Type); Assert.Equal([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum); break; diff --git a/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs b/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs deleted file mode 100644 index 86f0be591..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs +++ /dev/null @@ -1,309 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; - -#pragma warning disable CS0618 // Type or member is obsolete - -namespace ModelContextProtocol.Tests.Protocol; - -public class EnumSchemaTests -{ - [Fact] - public void UntitledSingleSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.UntitledSingleSelectEnumSchema - { - Title = "Priority", - Description = "Task priority level", - Enum = ["low", "medium", "high"], - Default = "medium" - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Priority", result.Title); - Assert.Equal("Task priority level", result.Description); - Assert.Equal(["low", "medium", "high"], result.Enum); - Assert.Equal("medium", result.Default); - Assert.Contains("\"type\":\"string\"", json); - Assert.Contains("\"enum\":[\"low\",\"medium\",\"high\"]", json); - Assert.DoesNotContain("enumNames", json); - Assert.DoesNotContain("oneOf", json); - } - - [Fact] - public void TitledSingleSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.TitledSingleSelectEnumSchema - { - Title = "Severity", - Description = "Issue severity", - OneOf = - [ - new ElicitRequestParams.EnumSchemaOption { Const = "critical", Title = "Critical" }, - new ElicitRequestParams.EnumSchemaOption { Const = "high", Title = "High Priority" }, - new ElicitRequestParams.EnumSchemaOption { Const = "low", Title = "Low Priority" } - ], - Default = "high" - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Severity", result.Title); - Assert.Equal("Issue severity", result.Description); - Assert.Equal(3, result.OneOf.Count); - Assert.Equal("critical", result.OneOf[0].Const); - Assert.Equal("Critical", result.OneOf[0].Title); - Assert.Equal("high", result.Default); - Assert.Contains("\"oneOf\":", json); - Assert.Contains("\"const\":\"critical\"", json); - Assert.Contains("\"title\":\"Critical\"", json); - Assert.DoesNotContain("enum\":", json); - Assert.DoesNotContain("enumNames", json); - } - - [Fact] - public void UntitledMultiSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.UntitledMultiSelectEnumSchema - { - Title = "Tags", - Description = "Select multiple tags", - MinItems = 1, - MaxItems = 3, - Items = new ElicitRequestParams.UntitledEnumItemsSchema - { - Type = "string", - Enum = ["bug", "feature", "documentation", "enhancement"] - }, - Default = ["bug", "feature"] - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Tags", result.Title); - Assert.Equal("Select multiple tags", result.Description); - Assert.Equal(1, result.MinItems); - Assert.Equal(3, result.MaxItems); - Assert.NotNull(result.Items); - Assert.Equal("string", result.Items.Type); - Assert.Equal(["bug", "feature", "documentation", "enhancement"], result.Items.Enum); - Assert.Equal(["bug", "feature"], result.Default); - Assert.Contains("\"type\":\"array\"", json); - Assert.Contains("\"minItems\":1", json); - Assert.Contains("\"maxItems\":3", json); - Assert.Contains("\"items\":", json); - Assert.DoesNotContain("anyOf", json); - } - - [Fact] - public void TitledMultiSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.TitledMultiSelectEnumSchema - { - Title = "Features", - Description = "Select desired features", - MinItems = 2, - Items = new ElicitRequestParams.TitledEnumItemsSchema - { - AnyOf = - [ - new ElicitRequestParams.EnumSchemaOption { Const = "auth", Title = "Authentication" }, - new ElicitRequestParams.EnumSchemaOption { Const = "api", Title = "REST API" }, - new ElicitRequestParams.EnumSchemaOption { Const = "ui", Title = "User Interface" } - ] - }, - Default = ["auth", "api"] - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Features", result.Title); - Assert.Equal("Select desired features", result.Description); - Assert.Equal(2, result.MinItems); - Assert.NotNull(result.Items); - Assert.NotNull(result.Items.AnyOf); - Assert.Equal(3, result.Items.AnyOf.Count); - Assert.Equal("auth", result.Items.AnyOf[0].Const); - Assert.Equal("Authentication", result.Items.AnyOf[0].Title); - Assert.Equal(["auth", "api"], result.Default); - Assert.Contains("\"type\":\"array\"", json); - Assert.Contains("\"anyOf\":", json); - Assert.Contains("\"const\":\"auth\"", json); - Assert.Contains("\"title\":\"Authentication\"", json); - } - - [Fact] - public void LegacyTitledEnumSchema_WithEnumNames_Deserializes_As_EnumSchema() - { - // Arrange - JSON with enumNames should deserialize as EnumSchema for backward compatibility - string json = """ - { - "type": "string", - "title": "Status", - "enum": ["active", "inactive", "pending"], - "enumNames": ["Active", "Inactive", "Pending"], - "default": "active" - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Status", result.Title); - Assert.Equal(["active", "inactive", "pending"], result.Enum); - Assert.Equal(["Active", "Inactive", "Pending"], result.EnumNames); - Assert.Equal("active", result.Default); - } - - [Fact] - public void EnumSchema_WithoutEnumNames_Deserializes_As_UntitledSingleSelect() - { - // Arrange - JSON without enumNames should deserialize as UntitledSingleSelectEnumSchema - string json = """ - { - "type": "string", - "title": "Status", - "enum": ["draft", "published", "archived"], - "default": "draft" - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Status", result.Title); - Assert.Equal(["draft", "published", "archived"], result.Enum); - Assert.Equal("draft", result.Default); - } - - [Fact] - public void EnumSchema_WithOneOf_Deserializes_As_TitledSingleSelect() - { - // Arrange - JSON with oneOf should deserialize as TitledSingleSelectEnumSchema - string json = """ - { - "type": "string", - "title": "Priority", - "oneOf": [ - { "const": "p0", "title": "P0 - Critical" }, - { "const": "p1", "title": "P1 - High" }, - { "const": "p2", "title": "P2 - Medium" } - ], - "default": "p1" - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Priority", result.Title); - Assert.Equal(3, result.OneOf.Count); - Assert.Equal("p0", result.OneOf[0].Const); - Assert.Equal("P0 - Critical", result.OneOf[0].Title); - Assert.Equal("p1", result.Default); - } - - [Fact] - public void MultiSelectEnum_WithEnum_Deserializes_As_UntitledMultiSelect() - { - // Arrange - string json = """ - { - "type": "array", - "title": "Categories", - "items": { - "type": "string", - "enum": ["tech", "business", "lifestyle"] - }, - "default": ["tech"] - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Categories", result.Title); - Assert.NotNull(result.Items); - Assert.Equal(["tech", "business", "lifestyle"], result.Items.Enum); - Assert.Equal(["tech"], result.Default); - } - - [Fact] - public void MultiSelectEnum_WithAnyOf_Deserializes_As_TitledMultiSelect() - { - // Arrange - string json = """ - { - "type": "array", - "title": "Roles", - "items": { - "anyOf": [ - { "const": "admin", "title": "Administrator" }, - { "const": "user", "title": "User" }, - { "const": "guest", "title": "Guest" } - ] - }, - "default": ["user"] - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Roles", result.Title); - Assert.NotNull(result.Items); - Assert.NotNull(result.Items.AnyOf); - Assert.Equal(3, result.Items.AnyOf.Count); - Assert.Equal("admin", result.Items.AnyOf[0].Const); - Assert.Equal("Administrator", result.Items.AnyOf[0].Title); - Assert.Equal(["user"], result.Default); - } -} diff --git a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs index 1e577f965..bb5ab1c67 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs @@ -1,8 +1,6 @@ using ModelContextProtocol.Protocol; using System.Text.Json; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Protocol; public static class PrimitiveSchemaDefinitionTests diff --git a/tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs b/tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs deleted file mode 100644 index 9765d1be3..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/SamplingMessageTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; - -namespace ModelContextProtocol.Tests.Protocol; - -public class SamplingMessageTests -{ - [Fact] - public void WithToolResults_SerializationRoundtrips() - { - SamplingMessage message = new() - { - Role = Role.User, - Content = - [ - new ToolResultContentBlock - { - ToolUseId = "call_123", - Content = - [ - new TextContentBlock { Text = "Weather in Paris: 18°C, partly cloudy" } - ] - } - ] - }; - - var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(Role.User, deserialized.Role); - Assert.Single(deserialized.Content); - - var toolResult = Assert.IsType(deserialized.Content[0]); - Assert.Equal("call_123", toolResult.ToolUseId); - Assert.Single(toolResult.Content); - - var textBlock = Assert.IsType(toolResult.Content[0]); - Assert.Equal("Weather in Paris: 18°C, partly cloudy", textBlock.Text); - } - - [Fact] - public void WithMultipleToolResults_SerializationRoundtrips() - { - SamplingMessage message = new() - { - Role = Role.User, - Content = - [ - new ToolResultContentBlock - { - ToolUseId = "call_abc123", - Content = [new TextContentBlock { Text = "Weather in Paris: 18°C, partly cloudy" }] - }, - new ToolResultContentBlock - { - ToolUseId = "call_def456", - Content = [new TextContentBlock { Text = "Weather in London: 15°C, rainy" }] - } - ] - }; - - var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(Role.User, deserialized.Role); - Assert.Equal(2, deserialized.Content.Count); - - var toolResult1 = Assert.IsType(deserialized.Content[0]); - Assert.Equal("call_abc123", toolResult1.ToolUseId); - Assert.Single(toolResult1.Content); - var textBlock1 = Assert.IsType(toolResult1.Content[0]); - Assert.Equal("Weather in Paris: 18°C, partly cloudy", textBlock1.Text); - - var toolResult2 = Assert.IsType(deserialized.Content[1]); - Assert.Equal("call_def456", toolResult2.ToolUseId); - Assert.Single(toolResult2.Content); - var textBlock2 = Assert.IsType(toolResult2.Content[0]); - Assert.Equal("Weather in London: 15°C, rainy", textBlock2.Text); - } - - [Fact] - public void WithToolResultOnly_SerializationRoundtrips() - { - SamplingMessage message = new() - { - Role = Role.User, - Content = - [ - new ToolResultContentBlock - { - ToolUseId = "call_123", - Content = [new TextContentBlock { Text = "Result" }] - } - ] - }; - - var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(Role.User, deserialized.Role); - Assert.Single(deserialized.Content); - var toolResult = Assert.IsType(deserialized.Content[0]); - Assert.Equal("call_123", toolResult.ToolUseId); - Assert.Single(toolResult.Content); - var textBlock = Assert.IsType(toolResult.Content[0]); - Assert.Equal("Result", textBlock.Text); - } -} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs deleted file mode 100644 index 548579d50..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/ToolChoiceTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; - -namespace ModelContextProtocol.Tests.Protocol; - -public class ToolChoiceTests -{ - [Fact] - public void DefaultModeIsNull() - { - Assert.Null(new ToolChoice().Mode); - } - - [Theory] - [InlineData(null)] - [InlineData("none")] - [InlineData("required")] - [InlineData("auto")] - [InlineData("something_custom")] - public void SerializesWithMode(string? mode) - { - ToolChoice toolChoice = new() { Mode = mode }; - - var json = JsonSerializer.Serialize(toolChoice, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - Assert.Equal(mode, deserialized.Mode); - } -} diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index ab2537b6b..810bcef48 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.AI; +using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; @@ -802,12 +802,12 @@ public override Task SendRequestAsync(JsonRpcRequest request, C Assert.Equal($"You are a helpful assistant.{Environment.NewLine}More system stuff.", rp.SystemPrompt); Assert.Equal(2, rp.Messages.Count); - Assert.Equal("I am going to France.", Assert.IsType(Assert.Single(rp.Messages[0].Content)).Text); - Assert.Equal("What is the most famous tower in Paris?", Assert.IsType(Assert.Single(rp.Messages[1].Content)).Text); + Assert.Equal("I am going to France.", Assert.IsType(rp.Messages[0].Content).Text); + Assert.Equal("What is the most famous tower in Paris?", Assert.IsType(rp.Messages[1].Content).Text); CreateMessageResult result = new() { - Content = [new TextContentBlock { Text = "The Eiffel Tower." }], + Content = new TextContentBlock { Text = "The Eiffel Tower." }, Model = "amazingmodel", Role = Role.Assistant, StopReason = "endTurn", diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs index 7f9cd61a5..5394ba30e 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs @@ -22,7 +22,8 @@ public async Task CreateAsync_ValidProcessInvalidServer_Throws() await Assert.ThrowsAsync(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); } - [Fact(Skip = "Platform not supported by this test.", SkipUnless = nameof(IsStdErrCallbackSupported))] + // [Fact(Skip = "Platform not supported by this test.", SkipUnless = nameof(IsStdErrCallbackSupported))] + [Fact] public async Task CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked() { string id = Guid.NewGuid().ToString("N"); From 93a68777e7df0c424a3b562bd7d9e32836df7d5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:48:59 +0000 Subject: [PATCH 11/12] Remove remaining obsolete EnumSchema and LegacyTitledEnumSchema types and all CS0618 pragma warnings Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> --- .../Protocol/ElicitRequestParams.cs | 105 +----- .../Protocol/ElicitationDefaultValuesTests.cs | 70 +--- .../Protocol/ElicitationTests.cs | 25 +- .../Protocol/ElicitationTypedTests.cs | 2 - .../Protocol/EnumSchemaTests.cs | 309 ------------------ .../PrimitiveSchemaDefinitionTests.cs | 114 ------- 6 files changed, 24 insertions(+), 601 deletions(-) delete mode 100644 tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index 74d0fd8a9..12f4dec3d 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -23,8 +23,7 @@ public sealed class ElicitRequestParams /// /// May be one of , , , /// , , - /// , , - /// or (deprecated). + /// , or . /// [JsonPropertyName("requestedSchema")] [field: MaybeNull] @@ -64,8 +63,7 @@ public IDictionary Properties /// Represents restricted subset of JSON Schema: /// , , , /// , , - /// , , - /// or (deprecated). + /// , or . /// [JsonConverter(typeof(Converter))] public abstract class PrimitiveSchemaDefinition @@ -121,7 +119,6 @@ public class Converter : JsonConverter string? defaultString = null; IList? defaultStringArray = null; IList? enumValues = null; - IList? enumNames = null; IList? oneOf = null; int? minItems = null; int? maxItems = null; @@ -207,10 +204,6 @@ public class Converter : JsonConverter enumValues = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); break; - case "enumNames": - enumNames = JsonSerializer.Deserialize(ref reader, McpJsonUtilities.JsonContext.Default.IListString); - break; - case "oneOf": oneOf = DeserializeEnumOptions(ref reader); break; @@ -245,27 +238,12 @@ public class Converter : JsonConverter } else if (enumValues is not null) { - if (enumNames is not null) - { - // EnumSchema for backward compatibility -#pragma warning disable CS0618 // Type or member is obsolete - psd = new EnumSchema -#pragma warning restore CS0618 // Type or member is obsolete - { - Enum = enumValues, - EnumNames = enumNames, - Default = defaultString, - }; - } - else + // UntitledSingleSelectEnumSchema + psd = new UntitledSingleSelectEnumSchema { - // UntitledSingleSelectEnumSchema - psd = new UntitledSingleSelectEnumSchema - { - Enum = enumValues, - Default = defaultString, - }; - } + Enum = enumValues, + Default = defaultString, + }; } else { @@ -558,25 +536,6 @@ public override void Write(Utf8JsonWriter writer, PrimitiveSchemaDefinition valu } break; -#pragma warning disable CS0618 // Type or member is obsolete - case LegacyTitledEnumSchema legacyEnum: -#pragma warning restore CS0618 // Type or member is obsolete - if (legacyEnum.Enum is not null) - { - writer.WritePropertyName("enum"); - JsonSerializer.Serialize(writer, legacyEnum.Enum, McpJsonUtilities.JsonContext.Default.IListString); - } - if (legacyEnum.EnumNames is not null) - { - writer.WritePropertyName("enumNames"); - JsonSerializer.Serialize(writer, legacyEnum.EnumNames, McpJsonUtilities.JsonContext.Default.IListString); - } - if (legacyEnum.Default is not null) - { - writer.WriteString("default", legacyEnum.Default); - } - break; - default: throw new JsonException($"Unexpected schema type: {value.GetType().Name}"); } @@ -740,49 +699,6 @@ public override string Type public bool? Default { get; set; } } - /// - /// Represents a legacy schema for an enum type with enumNames. - /// This schema is deprecated in favor of . - /// - [Obsolete("Use TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")] - public class LegacyTitledEnumSchema : PrimitiveSchemaDefinition - { - /// - [JsonPropertyName("type")] - public override string Type - { - get => "string"; - set - { - if (value is not "string") - { - throw new ArgumentException("Type must be 'string'.", nameof(value)); - } - } - } - - /// Gets or sets the list of allowed string values for the enum. - [JsonPropertyName("enum")] - [field: MaybeNull] - public IList Enum - { - get => field ??= []; - set - { - Throw.IfNull(value); - field = value; - } - } - - /// Gets or sets optional display names corresponding to the enum values. - [JsonPropertyName("enumNames")] - public IList? EnumNames { get; set; } - - /// Gets or sets the default value for the enum. - [JsonPropertyName("default")] - public string? Default { get; set; } - } - /// /// Represents a schema for single-selection enumeration without display titles for options. /// @@ -967,11 +883,4 @@ public override string Type public IList? Default { get; set; } } - /// - /// Represents a schema for an enum type. This is a compatibility alias for . - /// - [Obsolete("Use UntitledSingleSelectEnumSchema or TitledSingleSelectEnumSchema instead. This type will be removed in a future version.")] - public sealed class EnumSchema : LegacyTitledEnumSchema - { - } } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs index c29f7d033..b239c548a 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationDefaultValuesTests.cs @@ -1,8 +1,6 @@ using ModelContextProtocol.Protocol; using System.Text.Json; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Protocol; public class ElicitationDefaultValuesTests @@ -112,49 +110,6 @@ public void NumberSchema_Default_Null_DoesNotSerialize() Assert.DoesNotContain("\"default\"", json); } - [Fact] - public void EnumSchema_Default_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.EnumSchema - { - Title = "Priority", - Description = "Task priority", - Enum = ["low", "medium", "high"], - EnumNames = ["Low Priority", "Medium Priority", "High Priority"], - Default = "medium" - }; - - // Act - serialize as base type to use the converter - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var enumSchema = Assert.IsType(deserialized); - Assert.Equal("medium", enumSchema.Default); - Assert.Equal("Priority", enumSchema.Title); - Assert.Equal("Task priority", enumSchema.Description); - Assert.Contains("\"default\":\"medium\"", json); - } - - [Fact] - public void EnumSchema_Default_Null_DoesNotSerialize() - { - // Arrange - var schema = new ElicitRequestParams.EnumSchema - { - Title = "Priority", - Enum = ["low", "medium", "high"] - }; - - // Act - serialize as base type to use the converter - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.DoesNotContain("\"default\"", json); - } - [Fact] public void BooleanSchema_Default_True_Serializes_Correctly() { @@ -244,29 +199,6 @@ public void PrimitiveSchemaDefinition_NumberSchema_WithDefault_RoundTrips() Assert.Equal(100, numberSchema.Maximum); } - [Fact] - public void PrimitiveSchemaDefinition_EnumSchema_WithDefault_RoundTrips() - { - // Arrange - var schema = new ElicitRequestParams.EnumSchema - { - Title = "Status", - Enum = ["draft", "published", "archived"], - Default = "draft" - }; - - // Act - serialize as base type to test the converter - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - // EnumSchema without enumNames deserializes as UntitledSingleSelectEnumSchema - var enumSchema = Assert.IsType(deserialized); - Assert.Equal("draft", enumSchema.Default); - Assert.Equal(["draft", "published", "archived"], enumSchema.Enum); - } - [Fact] public void RequestSchema_WithAllDefaultTypes_Serializes_Correctly() { @@ -299,7 +231,7 @@ public void RequestSchema_WithAllDefaultTypes_Serializes_Correctly() Title = "Active", Default = true }, - ["status"] = new ElicitRequestParams.EnumSchema + ["status"] = new ElicitRequestParams.UntitledSingleSelectEnumSchema { Title = "Status", Enum = ["active", "inactive"], diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index fc4dacb78..5152855c8 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -1,10 +1,8 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using System.Text.Json; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Configuration; public partial class ElicitationTests : ClientServerTestBase @@ -46,10 +44,14 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer Description = "description4", Default = true, }, - ["prop4"] = new ElicitRequestParams.EnumSchema + ["prop4"] = new ElicitRequestParams.TitledSingleSelectEnumSchema { - Enum = ["option1", "option2", "option3"], - EnumNames = ["Name1", "Name2", "Name3"], + OneOf = + [ + new ElicitRequestParams.EnumSchemaOption { Const = "option1", Title = "Name1" }, + new ElicitRequestParams.EnumSchemaOption { Const = "option2", Title = "Name2" }, + new ElicitRequestParams.EnumSchemaOption { Const = "option3", Title = "Name3" } + ], }, }, }, @@ -104,9 +106,14 @@ public async Task Can_Elicit_Information() break; case "prop4": - var primitiveEnum = Assert.IsType(entry.Value); - Assert.Equal(["option1", "option2", "option3"], primitiveEnum.Enum); - Assert.Equal(["Name1", "Name2", "Name3"], primitiveEnum.EnumNames); + var primitiveEnum = Assert.IsType(entry.Value); + Assert.Equal(3, primitiveEnum.OneOf.Count); + Assert.Equal("option1", primitiveEnum.OneOf[0].Const); + Assert.Equal("Name1", primitiveEnum.OneOf[0].Title); + Assert.Equal("option2", primitiveEnum.OneOf[1].Const); + Assert.Equal("Name2", primitiveEnum.OneOf[1].Title); + Assert.Equal("option3", primitiveEnum.OneOf[2].Const); + Assert.Equal("Name3", primitiveEnum.OneOf[2].Title); break; default: diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs index b0cdbe3d9..4509b3891 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs @@ -4,8 +4,6 @@ using System.Text.Json; using System.Text.Json.Serialization; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Configuration; public partial class ElicitationTypedTests : ClientServerTestBase diff --git a/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs b/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs deleted file mode 100644 index 86f0be591..000000000 --- a/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs +++ /dev/null @@ -1,309 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Text.Json; - -#pragma warning disable CS0618 // Type or member is obsolete - -namespace ModelContextProtocol.Tests.Protocol; - -public class EnumSchemaTests -{ - [Fact] - public void UntitledSingleSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.UntitledSingleSelectEnumSchema - { - Title = "Priority", - Description = "Task priority level", - Enum = ["low", "medium", "high"], - Default = "medium" - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Priority", result.Title); - Assert.Equal("Task priority level", result.Description); - Assert.Equal(["low", "medium", "high"], result.Enum); - Assert.Equal("medium", result.Default); - Assert.Contains("\"type\":\"string\"", json); - Assert.Contains("\"enum\":[\"low\",\"medium\",\"high\"]", json); - Assert.DoesNotContain("enumNames", json); - Assert.DoesNotContain("oneOf", json); - } - - [Fact] - public void TitledSingleSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.TitledSingleSelectEnumSchema - { - Title = "Severity", - Description = "Issue severity", - OneOf = - [ - new ElicitRequestParams.EnumSchemaOption { Const = "critical", Title = "Critical" }, - new ElicitRequestParams.EnumSchemaOption { Const = "high", Title = "High Priority" }, - new ElicitRequestParams.EnumSchemaOption { Const = "low", Title = "Low Priority" } - ], - Default = "high" - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Severity", result.Title); - Assert.Equal("Issue severity", result.Description); - Assert.Equal(3, result.OneOf.Count); - Assert.Equal("critical", result.OneOf[0].Const); - Assert.Equal("Critical", result.OneOf[0].Title); - Assert.Equal("high", result.Default); - Assert.Contains("\"oneOf\":", json); - Assert.Contains("\"const\":\"critical\"", json); - Assert.Contains("\"title\":\"Critical\"", json); - Assert.DoesNotContain("enum\":", json); - Assert.DoesNotContain("enumNames", json); - } - - [Fact] - public void UntitledMultiSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.UntitledMultiSelectEnumSchema - { - Title = "Tags", - Description = "Select multiple tags", - MinItems = 1, - MaxItems = 3, - Items = new ElicitRequestParams.UntitledEnumItemsSchema - { - Type = "string", - Enum = ["bug", "feature", "documentation", "enhancement"] - }, - Default = ["bug", "feature"] - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Tags", result.Title); - Assert.Equal("Select multiple tags", result.Description); - Assert.Equal(1, result.MinItems); - Assert.Equal(3, result.MaxItems); - Assert.NotNull(result.Items); - Assert.Equal("string", result.Items.Type); - Assert.Equal(["bug", "feature", "documentation", "enhancement"], result.Items.Enum); - Assert.Equal(["bug", "feature"], result.Default); - Assert.Contains("\"type\":\"array\"", json); - Assert.Contains("\"minItems\":1", json); - Assert.Contains("\"maxItems\":3", json); - Assert.Contains("\"items\":", json); - Assert.DoesNotContain("anyOf", json); - } - - [Fact] - public void TitledMultiSelectEnumSchema_Serializes_Correctly() - { - // Arrange - var schema = new ElicitRequestParams.TitledMultiSelectEnumSchema - { - Title = "Features", - Description = "Select desired features", - MinItems = 2, - Items = new ElicitRequestParams.TitledEnumItemsSchema - { - AnyOf = - [ - new ElicitRequestParams.EnumSchemaOption { Const = "auth", Title = "Authentication" }, - new ElicitRequestParams.EnumSchemaOption { Const = "api", Title = "REST API" }, - new ElicitRequestParams.EnumSchemaOption { Const = "ui", Title = "User Interface" } - ] - }, - Default = ["auth", "api"] - }; - - // Act - string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Features", result.Title); - Assert.Equal("Select desired features", result.Description); - Assert.Equal(2, result.MinItems); - Assert.NotNull(result.Items); - Assert.NotNull(result.Items.AnyOf); - Assert.Equal(3, result.Items.AnyOf.Count); - Assert.Equal("auth", result.Items.AnyOf[0].Const); - Assert.Equal("Authentication", result.Items.AnyOf[0].Title); - Assert.Equal(["auth", "api"], result.Default); - Assert.Contains("\"type\":\"array\"", json); - Assert.Contains("\"anyOf\":", json); - Assert.Contains("\"const\":\"auth\"", json); - Assert.Contains("\"title\":\"Authentication\"", json); - } - - [Fact] - public void LegacyTitledEnumSchema_WithEnumNames_Deserializes_As_EnumSchema() - { - // Arrange - JSON with enumNames should deserialize as EnumSchema for backward compatibility - string json = """ - { - "type": "string", - "title": "Status", - "enum": ["active", "inactive", "pending"], - "enumNames": ["Active", "Inactive", "Pending"], - "default": "active" - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Status", result.Title); - Assert.Equal(["active", "inactive", "pending"], result.Enum); - Assert.Equal(["Active", "Inactive", "Pending"], result.EnumNames); - Assert.Equal("active", result.Default); - } - - [Fact] - public void EnumSchema_WithoutEnumNames_Deserializes_As_UntitledSingleSelect() - { - // Arrange - JSON without enumNames should deserialize as UntitledSingleSelectEnumSchema - string json = """ - { - "type": "string", - "title": "Status", - "enum": ["draft", "published", "archived"], - "default": "draft" - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Status", result.Title); - Assert.Equal(["draft", "published", "archived"], result.Enum); - Assert.Equal("draft", result.Default); - } - - [Fact] - public void EnumSchema_WithOneOf_Deserializes_As_TitledSingleSelect() - { - // Arrange - JSON with oneOf should deserialize as TitledSingleSelectEnumSchema - string json = """ - { - "type": "string", - "title": "Priority", - "oneOf": [ - { "const": "p0", "title": "P0 - Critical" }, - { "const": "p1", "title": "P1 - High" }, - { "const": "p2", "title": "P2 - Medium" } - ], - "default": "p1" - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("string", result.Type); - Assert.Equal("Priority", result.Title); - Assert.Equal(3, result.OneOf.Count); - Assert.Equal("p0", result.OneOf[0].Const); - Assert.Equal("P0 - Critical", result.OneOf[0].Title); - Assert.Equal("p1", result.Default); - } - - [Fact] - public void MultiSelectEnum_WithEnum_Deserializes_As_UntitledMultiSelect() - { - // Arrange - string json = """ - { - "type": "array", - "title": "Categories", - "items": { - "type": "string", - "enum": ["tech", "business", "lifestyle"] - }, - "default": ["tech"] - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Categories", result.Title); - Assert.NotNull(result.Items); - Assert.Equal(["tech", "business", "lifestyle"], result.Items.Enum); - Assert.Equal(["tech"], result.Default); - } - - [Fact] - public void MultiSelectEnum_WithAnyOf_Deserializes_As_TitledMultiSelect() - { - // Arrange - string json = """ - { - "type": "array", - "title": "Roles", - "items": { - "anyOf": [ - { "const": "admin", "title": "Administrator" }, - { "const": "user", "title": "User" }, - { "const": "guest", "title": "Guest" } - ] - }, - "default": ["user"] - } - """; - - // Act - var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); - - // Assert - Assert.NotNull(deserialized); - var result = Assert.IsType(deserialized); - Assert.Equal("array", result.Type); - Assert.Equal("Roles", result.Title); - Assert.NotNull(result.Items); - Assert.NotNull(result.Items.AnyOf); - Assert.Equal(3, result.Items.AnyOf.Count); - Assert.Equal("admin", result.Items.AnyOf[0].Const); - Assert.Equal("Administrator", result.Items.AnyOf[0].Title); - Assert.Equal(["user"], result.Default); - } -} diff --git a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs index 1e577f965..3d69adaa9 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs @@ -1,8 +1,6 @@ using ModelContextProtocol.Protocol; using System.Text.Json; -#pragma warning disable CS0618 // Type or member is obsolete - namespace ModelContextProtocol.Tests.Protocol; public static class PrimitiveSchemaDefinitionTests @@ -109,51 +107,6 @@ public static void BooleanSchema_UnknownMixedProperties_AreIgnored() Assert.False(boolSchema.Default); } - [Fact] - public static void EnumSchema_UnknownNestedArrays_AreIgnored() - { - // Test complex unknown properties with arrays of objects - - const string jsonWithNestedArrays = """ - { - "type": "string", - "enum": ["option1", "option2", "option3"], - "enumNames": ["Name1", "Name2", "Name3"], - "unknownComplex": [ - { - "nested": [ - {"deep": "value1"}, - {"deep": "value2"} - ] - }, - { - "nested": [ - {"deep": "value3"} - ] - } - ], - "default": "option1" - } - """; - - var result = JsonSerializer.Deserialize( - jsonWithNestedArrays, - McpJsonUtilities.DefaultOptions); - - Assert.NotNull(result); - var enumSchema = Assert.IsType(result); - Assert.Equal("string", enumSchema.Type); - Assert.Equal(3, enumSchema.Enum.Count); - Assert.Contains("option1", enumSchema.Enum); - Assert.Contains("option2", enumSchema.Enum); - Assert.Contains("option3", enumSchema.Enum); - Assert.Equal(3, enumSchema.EnumNames!.Count); - Assert.Contains("Name1", enumSchema.EnumNames); - Assert.Contains("Name2", enumSchema.EnumNames); - Assert.Contains("Name3", enumSchema.EnumNames); - Assert.Equal("option1", enumSchema.Default); - } - [Fact] public static void StringSchema_MultipleUnknownProperties_AllIgnored() { @@ -270,37 +223,6 @@ public static void NumberSchema_EmptyUnknownObject_IsIgnored() Assert.Equal(100.0, numberSchema.Maximum); } - [Fact] - public static void EnumSchema_UnknownPropertiesBetweenRequired_AreIgnored() - { - // Test unknown properties interspersed with required ones - - const string jsonWithInterspersedUnknown = """ - { - "unknownFirst": {"x": 1}, - "type": "string", - "unknownSecond": [1, 2], - "enum": ["a", "b"], - "unknownThird": {"nested": {"value": true}}, - "enumNames": ["Alpha", "Beta"] - } - """; - - var result = JsonSerializer.Deserialize( - jsonWithInterspersedUnknown, - McpJsonUtilities.DefaultOptions); - - Assert.NotNull(result); - var enumSchema = Assert.IsType(result); - Assert.Equal("string", enumSchema.Type); - Assert.Equal(2, enumSchema.Enum.Count); - Assert.Contains("a", enumSchema.Enum); - Assert.Contains("b", enumSchema.Enum); - Assert.Equal(2, enumSchema.EnumNames!.Count); - Assert.Contains("Alpha", enumSchema.EnumNames); - Assert.Contains("Beta", enumSchema.EnumNames); - } - [Fact] public static void BooleanSchema_VeryDeeplyNestedUnknown_IsIgnored() { @@ -335,40 +257,4 @@ public static void BooleanSchema_VeryDeeplyNestedUnknown_IsIgnored() Assert.Equal("boolean", boolSchema.Type); Assert.True(boolSchema.Default); } - - [Fact] - public static void EnumSchema_Deserialization_PreservesKnownProperties() - { - // Test deserialization of enum schema with all properties - - const string enumSchemaJson = """ - { - "type": "string", - "title": "Test Enum", - "description": "A test enum schema", - "enum": ["option1", "option2", "option3"], - "enumNames": ["Name1", "Name2", "Name3"], - "default": "option2" - } - """; - - var deserialized = JsonSerializer.Deserialize( - enumSchemaJson, - McpJsonUtilities.DefaultOptions); - - Assert.NotNull(deserialized); - var enumSchema = Assert.IsType(deserialized); - Assert.Equal("string", enumSchema.Type); - Assert.Equal("Test Enum", enumSchema.Title); - Assert.Equal("A test enum schema", enumSchema.Description); - Assert.Equal(3, enumSchema.Enum.Count); - Assert.Contains("option1", enumSchema.Enum); - Assert.Contains("option2", enumSchema.Enum); - Assert.Contains("option3", enumSchema.Enum); - Assert.Equal(3, enumSchema.EnumNames!.Count); - Assert.Contains("Name1", enumSchema.EnumNames); - Assert.Contains("Name2", enumSchema.EnumNames); - Assert.Contains("Name3", enumSchema.EnumNames); - Assert.Equal("option2", enumSchema.Default); - } } From 186d85c6e162bfd15373e5a2519c6139bbc4ac9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:34:31 +0000 Subject: [PATCH 12/12] Restore EnumSchemaTests.cs and add back valid enum schema tests without obsolete types Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com> --- .../Protocol/EnumSchemaTests.cs | 280 ++++++++++++++++++ .../PrimitiveSchemaDefinitionTests.cs | 98 ++++++ 2 files changed, 378 insertions(+) create mode 100644 tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs diff --git a/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs b/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs new file mode 100644 index 000000000..593f4690c --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Protocol/EnumSchemaTests.cs @@ -0,0 +1,280 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Protocol; + +public class EnumSchemaTests +{ + [Fact] + public void UntitledSingleSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.UntitledSingleSelectEnumSchema + { + Title = "Priority", + Description = "Task priority level", + Enum = ["low", "medium", "high"], + Default = "medium" + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Priority", result.Title); + Assert.Equal("Task priority level", result.Description); + Assert.Equal(["low", "medium", "high"], result.Enum); + Assert.Equal("medium", result.Default); + Assert.Contains("\"type\":\"string\"", json); + Assert.Contains("\"enum\":[\"low\",\"medium\",\"high\"]", json); + Assert.DoesNotContain("enumNames", json); + Assert.DoesNotContain("oneOf", json); + } + + [Fact] + public void TitledSingleSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.TitledSingleSelectEnumSchema + { + Title = "Severity", + Description = "Issue severity", + OneOf = + [ + new ElicitRequestParams.EnumSchemaOption { Const = "critical", Title = "Critical" }, + new ElicitRequestParams.EnumSchemaOption { Const = "high", Title = "High Priority" }, + new ElicitRequestParams.EnumSchemaOption { Const = "low", Title = "Low Priority" } + ], + Default = "high" + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Severity", result.Title); + Assert.Equal("Issue severity", result.Description); + Assert.Equal(3, result.OneOf.Count); + Assert.Equal("critical", result.OneOf[0].Const); + Assert.Equal("Critical", result.OneOf[0].Title); + Assert.Equal("high", result.Default); + Assert.Contains("\"oneOf\":", json); + Assert.Contains("\"const\":\"critical\"", json); + Assert.Contains("\"title\":\"Critical\"", json); + Assert.DoesNotContain("enum\":", json); + Assert.DoesNotContain("enumNames", json); + } + + [Fact] + public void UntitledMultiSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.UntitledMultiSelectEnumSchema + { + Title = "Tags", + Description = "Select multiple tags", + MinItems = 1, + MaxItems = 3, + Items = new ElicitRequestParams.UntitledEnumItemsSchema + { + Type = "string", + Enum = ["bug", "feature", "documentation", "enhancement"] + }, + Default = ["bug", "feature"] + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Tags", result.Title); + Assert.Equal("Select multiple tags", result.Description); + Assert.Equal(1, result.MinItems); + Assert.Equal(3, result.MaxItems); + Assert.NotNull(result.Items); + Assert.Equal("string", result.Items.Type); + Assert.Equal(["bug", "feature", "documentation", "enhancement"], result.Items.Enum); + Assert.Equal(["bug", "feature"], result.Default); + Assert.Contains("\"type\":\"array\"", json); + Assert.Contains("\"minItems\":1", json); + Assert.Contains("\"maxItems\":3", json); + Assert.Contains("\"items\":", json); + Assert.DoesNotContain("anyOf", json); + } + + [Fact] + public void TitledMultiSelectEnumSchema_Serializes_Correctly() + { + // Arrange + var schema = new ElicitRequestParams.TitledMultiSelectEnumSchema + { + Title = "Features", + Description = "Select desired features", + MinItems = 2, + Items = new ElicitRequestParams.TitledEnumItemsSchema + { + AnyOf = + [ + new ElicitRequestParams.EnumSchemaOption { Const = "auth", Title = "Authentication" }, + new ElicitRequestParams.EnumSchemaOption { Const = "api", Title = "REST API" }, + new ElicitRequestParams.EnumSchemaOption { Const = "ui", Title = "User Interface" } + ] + }, + Default = ["auth", "api"] + }; + + // Act + string json = JsonSerializer.Serialize(schema, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Features", result.Title); + Assert.Equal("Select desired features", result.Description); + Assert.Equal(2, result.MinItems); + Assert.NotNull(result.Items); + Assert.NotNull(result.Items.AnyOf); + Assert.Equal(3, result.Items.AnyOf.Count); + Assert.Equal("auth", result.Items.AnyOf[0].Const); + Assert.Equal("Authentication", result.Items.AnyOf[0].Title); + Assert.Equal(["auth", "api"], result.Default); + Assert.Contains("\"type\":\"array\"", json); + Assert.Contains("\"anyOf\":", json); + Assert.Contains("\"const\":\"auth\"", json); + Assert.Contains("\"title\":\"Authentication\"", json); + } + + [Fact] + public void EnumSchema_Deserializes_As_UntitledSingleSelect() + { + // Arrange - JSON without enumNames should deserialize as UntitledSingleSelectEnumSchema + string json = """ + { + "type": "string", + "title": "Status", + "enum": ["draft", "published", "archived"], + "default": "draft" + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Status", result.Title); + Assert.Equal(["draft", "published", "archived"], result.Enum); + Assert.Equal("draft", result.Default); + } + + [Fact] + public void EnumSchema_WithOneOf_Deserializes_As_TitledSingleSelect() + { + // Arrange - JSON with oneOf should deserialize as TitledSingleSelectEnumSchema + string json = """ + { + "type": "string", + "title": "Priority", + "oneOf": [ + { "const": "p0", "title": "P0 - Critical" }, + { "const": "p1", "title": "P1 - High" }, + { "const": "p2", "title": "P2 - Medium" } + ], + "default": "p1" + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("string", result.Type); + Assert.Equal("Priority", result.Title); + Assert.Equal(3, result.OneOf.Count); + Assert.Equal("p0", result.OneOf[0].Const); + Assert.Equal("P0 - Critical", result.OneOf[0].Title); + Assert.Equal("p1", result.Default); + } + + [Fact] + public void MultiSelectEnum_WithEnum_Deserializes_As_UntitledMultiSelect() + { + // Arrange + string json = """ + { + "type": "array", + "title": "Categories", + "items": { + "type": "string", + "enum": ["tech", "business", "lifestyle"] + }, + "default": ["tech"] + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Categories", result.Title); + Assert.NotNull(result.Items); + Assert.Equal(["tech", "business", "lifestyle"], result.Items.Enum); + Assert.Equal(["tech"], result.Default); + } + + [Fact] + public void MultiSelectEnum_WithAnyOf_Deserializes_As_TitledMultiSelect() + { + // Arrange + string json = """ + { + "type": "array", + "title": "Roles", + "items": { + "anyOf": [ + { "const": "admin", "title": "Administrator" }, + { "const": "user", "title": "User" }, + { "const": "guest", "title": "Guest" } + ] + }, + "default": ["user"] + } + """; + + // Act + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + // Assert + Assert.NotNull(deserialized); + var result = Assert.IsType(deserialized); + Assert.Equal("array", result.Type); + Assert.Equal("Roles", result.Title); + Assert.NotNull(result.Items); + Assert.NotNull(result.Items.AnyOf); + Assert.Equal(3, result.Items.AnyOf.Count); + Assert.Equal("admin", result.Items.AnyOf[0].Const); + Assert.Equal("Administrator", result.Items.AnyOf[0].Title); + Assert.Equal(["user"], result.Default); + } +} diff --git a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs index 3d69adaa9..df42aeda8 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/PrimitiveSchemaDefinitionTests.cs @@ -107,6 +107,46 @@ public static void BooleanSchema_UnknownMixedProperties_AreIgnored() Assert.False(boolSchema.Default); } + [Fact] + public static void EnumSchema_UnknownNestedArrays_AreIgnored() + { + // Test complex unknown properties with arrays of objects + + const string jsonWithNestedArrays = """ + { + "type": "string", + "enum": ["option1", "option2", "option3"], + "unknownComplex": [ + { + "nested": [ + {"deep": "value1"}, + {"deep": "value2"} + ] + }, + { + "nested": [ + {"deep": "value3"} + ] + } + ], + "default": "option1" + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithNestedArrays, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var enumSchema = Assert.IsType(result); + Assert.Equal("string", enumSchema.Type); + Assert.Equal(3, enumSchema.Enum.Count); + Assert.Contains("option1", enumSchema.Enum); + Assert.Contains("option2", enumSchema.Enum); + Assert.Contains("option3", enumSchema.Enum); + Assert.Equal("option1", enumSchema.Default); + } + [Fact] public static void StringSchema_MultipleUnknownProperties_AllIgnored() { @@ -223,6 +263,33 @@ public static void NumberSchema_EmptyUnknownObject_IsIgnored() Assert.Equal(100.0, numberSchema.Maximum); } + [Fact] + public static void EnumSchema_UnknownPropertiesBetweenRequired_AreIgnored() + { + // Test unknown properties interspersed with required ones + + const string jsonWithInterspersedUnknown = """ + { + "unknownFirst": {"x": 1}, + "type": "string", + "unknownSecond": [1, 2], + "enum": ["a", "b"], + "unknownThird": {"nested": {"value": true}} + } + """; + + var result = JsonSerializer.Deserialize( + jsonWithInterspersedUnknown, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(result); + var enumSchema = Assert.IsType(result); + Assert.Equal("string", enumSchema.Type); + Assert.Equal(2, enumSchema.Enum.Count); + Assert.Contains("a", enumSchema.Enum); + Assert.Contains("b", enumSchema.Enum); + } + [Fact] public static void BooleanSchema_VeryDeeplyNestedUnknown_IsIgnored() { @@ -257,4 +324,35 @@ public static void BooleanSchema_VeryDeeplyNestedUnknown_IsIgnored() Assert.Equal("boolean", boolSchema.Type); Assert.True(boolSchema.Default); } + + [Fact] + public static void EnumSchema_Deserialization_PreservesKnownProperties() + { + // Test deserialization of enum schema with all properties + + const string enumSchemaJson = """ + { + "type": "string", + "title": "Test Enum", + "description": "A test enum schema", + "enum": ["option1", "option2", "option3"], + "default": "option2" + } + """; + + var deserialized = JsonSerializer.Deserialize( + enumSchemaJson, + McpJsonUtilities.DefaultOptions); + + Assert.NotNull(deserialized); + var enumSchema = Assert.IsType(deserialized); + Assert.Equal("string", enumSchema.Type); + Assert.Equal("Test Enum", enumSchema.Title); + Assert.Equal("A test enum schema", enumSchema.Description); + Assert.Equal(3, enumSchema.Enum.Count); + Assert.Contains("option1", enumSchema.Enum); + Assert.Contains("option2", enumSchema.Enum); + Assert.Contains("option3", enumSchema.Enum); + Assert.Equal("option2", enumSchema.Default); + } }