diff --git a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs index 1dfcf9ac4..3e6d24ad0 100644 --- a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs +++ b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs @@ -14,7 +14,7 @@ public sealed class InteractiveTools [McpServerTool, Description("A simple game where the user has to guess a number between 1 and 10.")] public async Task GuessTheNumber( McpServer server, // Get the McpServer from DI container - CancellationToken token + CancellationToken cancellationToken ) { // Check if the client supports elicitation @@ -37,7 +37,7 @@ CancellationToken token { Message = "Do you want to play a game?", RequestedSchema = playSchema - }, token); + }, cancellationToken); // Check if user wants to play if (playResponse.Action != "accept" || playResponse.Content?["Answer"].ValueKind != JsonValueKind.True) @@ -64,7 +64,7 @@ CancellationToken token { Message = "What is your name?", RequestedSchema = nameSchema - }, token); + }, cancellationToken); if (nameResponse.Action != "accept") { @@ -100,7 +100,7 @@ CancellationToken token { Message = message, RequestedSchema = guessSchema - }, token); + }, cancellationToken); if (guessResponse.Action != "accept") { @@ -128,7 +128,7 @@ CancellationToken token [McpServerTool, Description("Example tool demonstrating various enum schema types")] public async Task EnumExamples( McpServer server, - CancellationToken token + CancellationToken cancellationToken ) { // Example 1: UntitledSingleSelectEnumSchema - Simple enum without display titles @@ -150,7 +150,7 @@ CancellationToken token { Message = "Select a priority level:", RequestedSchema = prioritySchema - }, token); + }, cancellationToken); if (priorityResponse.Action != "accept") { @@ -184,7 +184,7 @@ CancellationToken token { Message = "Select the issue severity:", RequestedSchema = severitySchema - }, token); + }, cancellationToken); if (severityResponse.Action != "accept") { @@ -218,7 +218,7 @@ CancellationToken token { Message = "Select up to 3 tags:", RequestedSchema = tagsSchema - }, token); + }, cancellationToken); if (tagsResponse.Action != "accept") { @@ -257,7 +257,7 @@ CancellationToken token { Message = "Select desired features:", RequestedSchema = featuresSchema - }, token); + }, cancellationToken); if (featuresResponse.Action != "accept") { diff --git a/docs/concepts/logging/logging.md b/docs/concepts/logging/logging.md index d5aba253e..fa5b6ed0e 100644 --- a/docs/concepts/logging/logging.md +++ b/docs/concepts/logging/logging.md @@ -69,7 +69,7 @@ Clients should check if the server supports logging by checking the method on . If the client does not set a logging level, the server might choose +the method on . If the client does not set a logging level, the server might choose to send all log messages or none—this is not specified in the protocol. So it's important that the client sets a logging level to ensure it receives the desired log messages and only those messages. diff --git a/docs/concepts/logging/samples/client/Program.cs b/docs/concepts/logging/samples/client/Program.cs index 29a15726a..4283ef3d5 100644 --- a/docs/concepts/logging/samples/client/Program.cs +++ b/docs/concepts/logging/samples/client/Program.cs @@ -30,7 +30,7 @@ if (Enum.TryParse(firstArgument, true, out var loggingLevel)) { // - await mcpClient.SetLoggingLevel(loggingLevel); + await mcpClient.SetLoggingLevelAsync(loggingLevel); // } else diff --git a/samples/ProtectedMcpClient/Program.cs b/samples/ProtectedMcpClient/Program.cs index 042d47713..eba792556 100644 --- a/samples/ProtectedMcpClient/Program.cs +++ b/samples/ProtectedMcpClient/Program.cs @@ -70,7 +70,7 @@ /// /// The authorization URL to open in the browser. /// The redirect URI where the authorization code will be sent. -/// The cancellation token. +/// The to monitor for cancellation requests. The default is . /// The authorization code extracted from the callback, or null if the operation failed. static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { diff --git a/src/ModelContextProtocol.Core/AIContentExtensions.cs b/src/ModelContextProtocol.Core/AIContentExtensions.cs index 374f00555..b1ba32bf4 100644 --- a/src/ModelContextProtocol.Core/AIContentExtensions.cs +++ b/src/ModelContextProtocol.Core/AIContentExtensions.cs @@ -161,6 +161,7 @@ internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonO /// This method transforms a protocol-specific from the Model Context Protocol /// into a standard object that can be used with AI client libraries. /// + /// is . public static ChatMessage ToChatMessage(this PromptMessage promptMessage) { Throw.IfNull(promptMessage); @@ -187,6 +188,7 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage) /// message containing a with result as a /// serialized . /// + /// or is . public static ChatMessage ToChatMessage(this CallToolResult result, string callId) { Throw.IfNull(result); @@ -207,6 +209,7 @@ public static ChatMessage ToChatMessage(this CallToolResult result, string callI /// This method transforms protocol-specific objects from a Model Context Protocol /// prompt result into standard objects that can be used with AI client libraries. /// + /// is . public static IList ToChatMessages(this GetPromptResult promptResult) { Throw.IfNull(promptResult); @@ -224,6 +227,7 @@ public static IList ToChatMessages(this GetPromptResult promptResul /// protocol-specific objects for the Model Context Protocol system. /// Only representable content items are processed. /// + /// is . public static IList ToPromptMessages(this ChatMessage chatMessage) { Throw.IfNull(chatMessage); @@ -251,6 +255,7 @@ public static IList ToPromptMessages(this ChatMessage chatMessage /// This method converts Model Context Protocol content types to the equivalent Microsoft.Extensions.AI /// content types, enabling seamless integration between the protocol and AI client libraries. /// + /// is . public static AIContent? ToAIContent(this ContentBlock content) { Throw.IfNull(content); @@ -294,6 +299,8 @@ public static IList ToPromptMessages(this ChatMessage chatMessage /// This method converts Model Context Protocol resource types to the equivalent Microsoft.Extensions.AI /// content types, enabling seamless integration between the protocol and AI client libraries. /// + /// is . + /// The resource type is not supported. public static AIContent ToAIContent(this ResourceContents content) { Throw.IfNull(content); @@ -325,6 +332,7 @@ public static AIContent ToAIContent(this ResourceContents content) /// preserving the type-specific conversion logic for text, images, audio, and resources. /// /// + /// is . public static IList ToAIContents(this IEnumerable contents) { Throw.IfNull(contents); @@ -347,6 +355,7 @@ public static IList ToAIContents(this IEnumerable conte /// binary resources become objects. /// /// + /// is . public static IList ToAIContents(this IEnumerable contents) { Throw.IfNull(contents); @@ -357,8 +366,11 @@ public static IList ToAIContents(this IEnumerable c /// Creates a new from the content of an . /// The to convert. /// The created . + /// is . public static ContentBlock ToContentBlock(this AIContent content) { + Throw.IfNull(content); + ContentBlock contentBlock = content switch { TextContent textContent => new TextContentBlock diff --git a/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs b/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs index d3c33231f..a811e51cc 100644 --- a/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs +++ b/src/ModelContextProtocol.Core/Authentication/AuthorizationRedirectDelegate.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Authentication; /// /// The authorization URL that the user needs to visit. /// The redirect URI where the authorization code will be sent. -/// The cancellation token. +/// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the authorization code if successful, or null if the operation failed or was cancelled. /// /// diff --git a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs index e895272c3..5454c591c 100644 --- a/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs +++ b/src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs @@ -102,7 +102,7 @@ public ClientOAuthProvider( /// /// The authorization URL to handle. /// The redirect URI where the authorization code will be sent. - /// The cancellation token. + /// The to monitor for cancellation requests. The default is . /// The authorization code entered by the user, or null if none was provided. private static Task DefaultAuthorizationUrlHandler(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { @@ -135,7 +135,7 @@ public ClientOAuthProvider( /// /// The authentication scheme to use. /// The URI of the resource requiring authentication. - /// A token to cancel the operation. + /// The to monitor for cancellation requests. The default is . /// An authentication token string, or null if no token could be obtained for the specified scheme. public async Task GetCredentialAsync(string scheme, Uri resourceUri, CancellationToken cancellationToken = default) { @@ -168,7 +168,7 @@ public ClientOAuthProvider( /// /// The authentication scheme that was used when the unauthorized response was received. /// The HTTP response that contained the 401 status code. - /// A token to cancel the operation. + /// The to monitor for cancellation requests. The default is . /// /// A result object indicating if the provider was able to handle the unauthorized response, /// and the authentication scheme that should be used for the next attempt, if any. @@ -186,7 +186,7 @@ public async Task HandleUnauthorizedResponseAsync( /// Performs OAuth authorization by selecting an appropriate authorization server and completing the OAuth flow. /// /// The 401 Unauthorized response containing authentication challenge. - /// The cancellation token. + /// The to monitor for cancellation requests. The default is . /// A result object indicating whether authorization was successful. private async Task PerformOAuthAuthorizationAsync( HttpResponseMessage response, @@ -473,7 +473,7 @@ private async Task HandleSuccessfulTokenResponseAsync(HttpRespon /// Fetches the protected resource metadata from the provided URL. /// /// The URL to fetch the metadata from. - /// A token to cancel the operation. + /// The to monitor for cancellation requests. The default is . /// The fetched ProtectedResourceMetadata, or null if it couldn't be fetched. private async Task FetchProtectedResourceMetadataAsync(Uri metadataUrl, CancellationToken cancellationToken = default) { @@ -488,7 +488,7 @@ private async Task HandleSuccessfulTokenResponseAsync(HttpRespon /// Performs dynamic client registration with the authorization server. /// /// The authorization server metadata. - /// Cancellation token. + /// The to monitor for cancellation requests. The default is . /// A task representing the asynchronous operation. private async Task PerformDynamicClientRegistrationAsync( AuthorizationServerMetadata authServerMetadata, @@ -609,7 +609,7 @@ private static string NormalizeUri(Uri uri) /// /// The HTTP response containing the WWW-Authenticate header. /// The server URL to verify against the resource metadata. - /// A token to cancel the operation. + /// The to monitor for cancellation requests. The default is . /// The resource metadata if the resource matches the server, otherwise throws an exception. /// Thrown when the response is not a 401, lacks a WWW-Authenticate header, /// lacks a resource_metadata parameter, the metadata can't be fetched, or the resource URI doesn't match the server URL. diff --git a/src/ModelContextProtocol.Core/Client/HttpClientTransport.cs b/src/ModelContextProtocol.Core/Client/HttpClientTransport.cs index f49a496e3..48f8c5af1 100644 --- a/src/ModelContextProtocol.Core/Client/HttpClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/HttpClientTransport.cs @@ -41,6 +41,7 @@ public HttpClientTransport(HttpClientTransportOptions transportOptions, ILoggerF /// to dispose of when the transport is disposed; /// if the caller is retaining ownership of the 's lifetime. /// + /// or is . public HttpClientTransport(HttpClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory = null, bool ownsHttpClient = false) { Throw.IfNull(transportOptions); diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index 10cbc7ad5..d5bbf977b 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -1,8 +1,6 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; @@ -22,7 +20,7 @@ public abstract partial class McpClient : McpSession /// 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. - /// or is . + /// is . public static async Task CreateAsync( IClientTransport clientTransport, McpClientOptions? clientOptions = null, @@ -41,7 +39,12 @@ public static async Task CreateAsync( } catch { - await clientSession.DisposeAsync().ConfigureAwait(false); + try + { + await clientSession.DisposeAsync().ConfigureAwait(false); + } + catch { } // allow the original exception to propagate + throw; } @@ -55,9 +58,9 @@ public static async Task CreateAsync( /// The metadata captured from the original session that should be applied when resuming. /// Optional client settings that should mirror those used to create the original session. /// An optional logger factory for diagnostics. - /// Token used when establishing the transport connection. + /// The to monitor for cancellation requests. The default is . /// An bound to the resumed session. - /// Thrown when or is . + /// , , , or is . public static async Task ResumeSessionAsync( IClientTransport clientTransport, ResumeClientSessionOptions resumeOptions, @@ -71,10 +74,10 @@ public static async Task ResumeSessionAsync( Throw.IfNull(resumeOptions.ServerInfo); var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); - var endpointName = clientTransport.Name; - var clientSession = new McpClientImpl(transport, endpointName, clientOptions, loggerFactory); + McpClientImpl clientSession = new(transport, clientTransport.Name, clientOptions, loggerFactory); clientSession.ResumeSession(resumeOptions); + return clientSession; } @@ -87,9 +90,31 @@ public static async Task ResumeSessionAsync( /// The server cannot be reached or returned an error response. public ValueTask PingAsync(RequestOptions? options = null, CancellationToken cancellationToken = default) { + return PingAsync( + new PingRequestParams + { + Meta = options?.GetMetaForRequest() + }, + cancellationToken); + } + + /// + /// Sends a ping request to verify server connectivity. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// A task containing the ping result. + /// is . + /// The server cannot be reached or returned an error response. + public ValueTask PingAsync( + PingRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + return SendRequestAsync( RequestMethods.Ping, - new PingRequestParams { Meta = options?.Meta }, + requestParams, McpJsonUtilities.JsonContext.Default.PingRequestParams, McpJsonUtilities.JsonContext.Default.PingResult, cancellationToken: cancellationToken); @@ -106,29 +131,49 @@ public async ValueTask> ListToolsAsync( CancellationToken cancellationToken = default) { List? tools = null; - string? cursor = null; + ListToolsRequestParams requestParams = new() { Meta = options?.GetMetaForRequest() }; do { - var toolResults = await SendRequestAsync( - RequestMethods.ToolsList, - new() { Cursor = cursor, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, - McpJsonUtilities.JsonContext.Default.ListToolsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - tools ??= new List(toolResults.Tools.Count); + var toolResults = await ListToolsAsync(requestParams, cancellationToken).ConfigureAwait(false); + tools ??= new(toolResults.Tools.Count); foreach (var tool in toolResults.Tools) { - tools.Add(new McpClientTool(this, tool, options?.JsonSerializerOptions)); + tools.Add(new(this, tool, options?.JsonSerializerOptions)); } - cursor = toolResults.NextCursor; + requestParams.Cursor = toolResults.NextCursor; } - while (cursor is not null); + while (requestParams.Cursor is not null); return tools; } + /// + /// Retrieves a list of available tools from the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request as provided by the server. + /// is . + /// + /// The overload retrieves all tools by automatically handling pagination. + /// This overload works with the lower-level and , returning the raw result from the server. + /// Any pagination needs to be managed by the caller. + /// + public ValueTask ListToolsAsync( + ListToolsRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.ToolsList, + requestParams, + McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, + McpJsonUtilities.JsonContext.Default.ListToolsResult, + cancellationToken: cancellationToken); + } + /// /// Retrieves a list of available prompts from the server. /// @@ -140,29 +185,49 @@ public async ValueTask> ListPromptsAsync( CancellationToken cancellationToken = default) { List? prompts = null; - string? cursor = null; + ListPromptsRequestParams requestParams = new() { Meta = options?.GetMetaForRequest() }; do { - var promptResults = await SendRequestAsync( - RequestMethods.PromptsList, - new() { Cursor = cursor, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, - McpJsonUtilities.JsonContext.Default.ListPromptsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - prompts ??= new List(promptResults.Prompts.Count); + var promptResults = await ListPromptsAsync(requestParams, cancellationToken).ConfigureAwait(false); + prompts ??= new(promptResults.Prompts.Count); foreach (var prompt in promptResults.Prompts) { - prompts.Add(new McpClientPrompt(this, prompt)); + prompts.Add(new(this, prompt)); } - cursor = promptResults.NextCursor; + requestParams.Cursor = promptResults.NextCursor; } - while (cursor is not null); + while (requestParams.Cursor is not null); return prompts; } + /// + /// Retrieves a list of available prompts from the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request as provided by the server. + /// is . + /// + /// The overload retrieves all prompts by automatically handling pagination. + /// This overload works with the lower-level and , returning the raw result from the server. + /// Any pagination needs to be managed by the caller. + /// + public ValueTask ListPromptsAsync( + ListPromptsRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.PromptsList, + requestParams, + McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, + McpJsonUtilities.JsonContext.Default.ListPromptsResult, + cancellationToken: cancellationToken); + } + /// /// Retrieves a specific prompt from the MCP server. /// @@ -171,6 +236,8 @@ public async ValueTask> ListPromptsAsync( /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task containing the prompt's result with content and messages. + /// is . + /// is empty or composed entirely of whitespace. public ValueTask GetPromptAsync( string name, IReadOnlyDictionary? arguments = null, @@ -182,9 +249,32 @@ public ValueTask GetPromptAsync( var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); + return GetPromptAsync( + new GetPromptRequestParams + { + Name = name, + Arguments = ToArgumentsDictionary(arguments, serializerOptions), + Meta = options?.GetMetaForRequest(), + }, + cancellationToken); + } + + /// + /// Retrieves a list of available prompts from the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request as provided by the server. + /// is . + public ValueTask GetPromptAsync( + GetPromptRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + return SendRequestAsync( RequestMethods.PromptsGet, - new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions), Meta = options?.Meta }, + requestParams, McpJsonUtilities.JsonContext.Default.GetPromptRequestParams, McpJsonUtilities.JsonContext.Default.GetPromptResult, cancellationToken: cancellationToken); @@ -201,30 +291,49 @@ public async ValueTask> ListResourceTemplatesAs CancellationToken cancellationToken = default) { List? resourceTemplates = null; - - string? cursor = null; + ListResourceTemplatesRequestParams requestParams = new() { Meta = options?.GetMetaForRequest() }; do { - var templateResults = await SendRequestAsync( - RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - resourceTemplates ??= new List(templateResults.ResourceTemplates.Count); + var templateResults = await ListResourceTemplatesAsync(requestParams, cancellationToken).ConfigureAwait(false); + resourceTemplates ??= new(templateResults.ResourceTemplates.Count); foreach (var template in templateResults.ResourceTemplates) { - resourceTemplates.Add(new McpClientResourceTemplate(this, template)); + resourceTemplates.Add(new(this, template)); } - cursor = templateResults.NextCursor; + requestParams.Cursor = templateResults.NextCursor; } - while (cursor is not null); + while (requestParams.Cursor is not null); return resourceTemplates; } + /// + /// Retrieves a list of available resource templates from the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request as provided by the server. + /// is . + /// + /// The overload retrieves all resource templates by automatically handling pagination. + /// This overload works with the lower-level and , returning the raw result from the server. + /// Any pagination needs to be managed by the caller. + /// + public ValueTask ListResourceTemplatesAsync( + ListResourceTemplatesRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.ResourcesTemplatesList, + requestParams, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, + cancellationToken: cancellationToken); + } + /// /// Retrieves a list of available resources from the server. /// @@ -236,47 +345,62 @@ public async ValueTask> ListResourcesAsync( CancellationToken cancellationToken = default) { List? resources = null; - - string? cursor = null; + ListResourcesRequestParams requestParams = new() { Meta = options?.GetMetaForRequest() }; do { - var resourceResults = await SendRequestAsync( - RequestMethods.ResourcesList, - new() { Cursor = cursor, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourcesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - resources ??= new List(resourceResults.Resources.Count); + var resourceResults = await ListResourcesAsync(requestParams, cancellationToken).ConfigureAwait(false); + resources ??= new(resourceResults.Resources.Count); foreach (var resource in resourceResults.Resources) { - resources.Add(new McpClientResource(this, resource)); + resources.Add(new(this, resource)); } - cursor = resourceResults.NextCursor; + requestParams.Cursor = resourceResults.NextCursor; } - while (cursor is not null); + while (requestParams.Cursor is not null); return resources; } + /// + /// Retrieves a list of available resources from the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request as provided by the server. + /// is . + /// + /// The overload retrieves all resources by automatically handling pagination. + /// This overload works with the lower-level and , returning the raw result from the server. + /// Any pagination needs to be managed by the caller. + /// + public ValueTask ListResourcesAsync( + ListResourcesRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.ResourcesList, + requestParams, + McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourcesResult, + cancellationToken: cancellationToken); + } + /// /// Reads a resource from the server. /// /// The URI of the resource. /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . + /// is . public ValueTask ReadResourceAsync( - string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) + Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { - Throw.IfNullOrWhiteSpace(uri); + Throw.IfNull(uri); - return SendRequestAsync( - RequestMethods.ResourcesRead, - new() { Uri = uri, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, - McpJsonUtilities.JsonContext.Default.ReadResourceResult, - cancellationToken: cancellationToken); + return ReadResourceAsync(uri.AbsoluteUri, options, cancellationToken); } /// @@ -285,12 +409,18 @@ public ValueTask ReadResourceAsync( /// The URI of the resource. /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . + /// is . + /// is empty or composed entirely of whitespace. public ValueTask ReadResourceAsync( - Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) + string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { - Throw.IfNull(uri); + Throw.IfNullOrWhiteSpace(uri); - return ReadResourceAsync(uri.ToString(), options, cancellationToken); + return ReadResourceAsync(new ReadResourceRequestParams + { + Uri = uri, + Meta = options?.GetMetaForRequest(), + }, cancellationToken); } /// @@ -300,15 +430,39 @@ public ValueTask ReadResourceAsync( /// Arguments to use to format . /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . + /// or is . + /// is empty or composed entirely of whitespace. public ValueTask ReadResourceAsync( string uriTemplate, IReadOnlyDictionary arguments, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uriTemplate); Throw.IfNull(arguments); + return ReadResourceAsync( + new ReadResourceRequestParams + { + Uri = UriTemplate.FormatUri(uriTemplate, arguments), + Meta = options?.GetMetaForRequest(), + }, + cancellationToken); + } + + /// + /// Reads a resource from the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request. + /// is . + public ValueTask ReadResourceAsync( + ReadResourceRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + return SendRequestAsync( RequestMethods.ResourcesRead, - new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments), Meta = options?.Meta }, + requestParams, McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, McpJsonUtilities.JsonContext.Default.ReadResourceResult, cancellationToken: cancellationToken); @@ -320,25 +474,64 @@ public ValueTask ReadResourceAsync( /// 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. + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A containing completion suggestions. - public ValueTask CompleteAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + /// or is . + /// is empty or composed entirely of whitespace. + public ValueTask CompleteAsync( + Reference reference, string argumentName, string argumentValue, + RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNull(reference); Throw.IfNullOrWhiteSpace(argumentName); - return SendRequestAsync( - RequestMethods.CompletionComplete, - new() + return CompleteAsync( + new CompleteRequestParams { Ref = reference, - Argument = new Argument { Name = argumentName, Value = argumentValue } + Argument = new() { Name = argumentName, Value = argumentValue }, + Meta = options?.GetMetaForRequest(), }, + cancellationToken); + } + + /// + /// Requests completion suggestions for a prompt argument or resource reference. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request. + /// is . + public ValueTask CompleteAsync( + CompleteRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.CompletionComplete, + requestParams, McpJsonUtilities.JsonContext.Default.CompleteRequestParams, McpJsonUtilities.JsonContext.Default.CompleteResult, cancellationToken: cancellationToken); } + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The URI of the resource to which to subscribe. + /// Optional request options including metadata, serialization settings, and progress tracking. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + /// is . + public Task SubscribeToResourceAsync(Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return SubscribeToResourceAsync(uri.AbsoluteUri, options, cancellationToken); + } + /// /// Subscribes to a resource on the server to receive notifications when it changes. /// @@ -346,30 +539,55 @@ public ValueTask CompleteAsync(Reference reference, string argum /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. + /// is . + /// is empty or composed entirely of whitespace. public Task SubscribeToResourceAsync(string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uri); + return SubscribeToResourceAsync( + new SubscribeRequestParams + { + Uri = uri, + Meta = options?.GetMetaForRequest(), + }, + cancellationToken); + } + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request. + /// is . + public Task SubscribeToResourceAsync( + SubscribeRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + return SendRequestAsync( RequestMethods.ResourcesSubscribe, - new() { Uri = uri, Meta = options?.Meta }, + requestParams, McpJsonUtilities.JsonContext.Default.SubscribeRequestParams, McpJsonUtilities.JsonContext.Default.EmptyResult, cancellationToken: cancellationToken).AsTask(); } /// - /// Subscribes to a resource on the server to receive notifications when it changes. + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. /// - /// The URI of the resource to which to subscribe. + /// The URI of the resource to unsubscribe from. /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. - public Task SubscribeToResourceAsync(Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) + /// is . + public Task UnsubscribeFromResourceAsync(Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNull(uri); - return SubscribeToResourceAsync(uri.ToString(), options, cancellationToken); + return UnsubscribeFromResourceAsync(uri.AbsoluteUri, options, cancellationToken); } /// @@ -379,30 +597,40 @@ public Task SubscribeToResourceAsync(Uri uri, RequestOptions? options = null, Ca /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. + /// is . + /// is empty or composed entirely of whitespace. public Task UnsubscribeFromResourceAsync(string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uri); - return SendRequestAsync( - RequestMethods.ResourcesUnsubscribe, - new() { Uri = uri, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); + return UnsubscribeFromResourceAsync( + new UnsubscribeRequestParams + { + Uri = uri, + Meta = options?.GetMetaForRequest() + }, + cancellationToken); } /// /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. /// - /// The URI of the resource to unsubscribe from. - /// Optional request options including metadata, serialization settings, and progress tracking. + /// The request parameters to send in the request. /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - public Task UnsubscribeFromResourceAsync(Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) + /// The result of the request. + /// is . + public Task UnsubscribeFromResourceAsync( + UnsubscribeRequestParams requestParams, + CancellationToken cancellationToken = default) { - Throw.IfNull(uri); + Throw.IfNull(requestParams); - return UnsubscribeFromResourceAsync(uri.ToString(), options, cancellationToken); + return SendRequestAsync( + RequestMethods.ResourcesUnsubscribe, + requestParams, + McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); } /// @@ -412,8 +640,9 @@ public Task UnsubscribeFromResourceAsync(Uri uri, RequestOptions? options = null /// An optional dictionary of arguments to pass to the tool. /// An optional progress reporter for server notifications. /// Optional request options including metadata, serialization settings, and progress tracking. - /// A cancellation token. + /// The to monitor for cancellation requests. The default is . /// The from the tool execution. + /// is . public ValueTask CallToolAsync( string toolName, IReadOnlyDictionary? arguments = null, @@ -422,25 +651,23 @@ public ValueTask CallToolAsync( CancellationToken cancellationToken = default) { Throw.IfNull(toolName); + var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); - if (progress is not null) + if (progress is null) { - return SendRequestWithProgressAsync(toolName, arguments, progress, options?.Meta, serializerOptions, cancellationToken); + return CallToolAsync( + new CallToolRequestParams + { + Name = toolName, + Arguments = ToArgumentsDictionary(arguments, serializerOptions), + Meta = options?.GetMetaForRequest(), + }, + cancellationToken); } - return SendRequestAsync( - RequestMethods.ToolsCall, - new() - { - Name = toolName, - Arguments = ToArgumentsDictionary(arguments, serializerOptions), - Meta = options?.Meta, - }, - McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResult, - cancellationToken: cancellationToken); + return SendRequestWithProgressAsync(toolName, arguments, progress, options?.GetMetaForRequest(), serializerOptions, cancellationToken); async ValueTask SendRequestWithProgressAsync( string toolName, @@ -464,23 +691,41 @@ async ValueTask SendRequestWithProgressAsync( return default; }).ConfigureAwait(false); - var metaWithProgress = meta is not null ? new JsonObject(meta) : new JsonObject(); + JsonObject metaWithProgress = meta is not null ? new(meta) : []; metaWithProgress["progressToken"] = progressToken.ToString(); - return await SendRequestAsync( - RequestMethods.ToolsCall, + return await CallToolAsync( new() { Name = toolName, Arguments = ToArgumentsDictionary(arguments, serializerOptions), Meta = metaWithProgress, }, - McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResult, - cancellationToken: cancellationToken).ConfigureAwait(false); + cancellationToken).ConfigureAwait(false); } } + /// + /// Invokes a tool on the server. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request. + /// is . + public ValueTask CallToolAsync( + CallToolRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.ToolsCall, + requestParams, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken); + } + /// /// Sets the logging level for the server to control which log messages are sent to the client. /// @@ -488,15 +733,8 @@ async ValueTask SendRequestWithProgressAsync( /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task representing the asynchronous operation. - public Task SetLoggingLevel(LoggingLevel level, RequestOptions? options = null, CancellationToken cancellationToken = default) - { - return SendRequestAsync( - RequestMethods.LoggingSetLevel, - new() { Level = level, Meta = options?.Meta }, - McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } + public Task SetLoggingLevelAsync(LogLevel level, RequestOptions? options = null, CancellationToken cancellationToken = default) => + SetLoggingLevelAsync(McpServerImpl.ToLoggingLevel(level), options, cancellationToken); /// /// Sets the logging level for the server to control which log messages are sent to the client. @@ -505,8 +743,37 @@ public Task SetLoggingLevel(LoggingLevel level, RequestOptions? options = null, /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task representing the asynchronous operation. - public Task SetLoggingLevel(LogLevel level, RequestOptions? options = null, CancellationToken cancellationToken = default) => - SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), options, cancellationToken); + public Task SetLoggingLevelAsync(LoggingLevel level, RequestOptions? options = null, CancellationToken cancellationToken = default) + { + return SetLoggingLevelAsync( + new SetLevelRequestParams + { + Level = level, + Meta = options?.GetMetaForRequest() + }, + cancellationToken); + } + + /// + /// Sets the logging level for the server to control which log messages are sent to the client. + /// + /// The request parameters to send in the request. + /// The to monitor for cancellation requests. The default is . + /// The result of the request. + /// is . + public Task SetLoggingLevelAsync( + SetLevelRequestParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendRequestAsync( + RequestMethods.LoggingSetLevel, + requestParams, + McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } /// Converts a dictionary with values to a dictionary with values. private static Dictionary? ToArgumentsDictionary( diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index 1332f1b55..fb5ec9517 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -157,7 +157,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) { // Send initialize request string requestProtocol = _options.ProtocolVersion ?? McpSessionHandler.LatestProtocolVersion; - var initializeResponse = await this.SendRequestAsync( + var initializeResponse = await SendRequestAsync( RequestMethods.Initialize, new InitializeRequestParams { diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs index 23c75fb49..448f53737 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Client; /// /// This class provides a client-side wrapper around a prompt defined on an MCP server. It allows /// retrieving the prompt's content by sending a request to the server with optional arguments. -/// Instances of this class are typically obtained by calling . +/// Instances of this class are typically obtained by calling . /// /// /// Each prompt has a name and optionally a description, and it can be invoked with arguments @@ -29,7 +29,7 @@ public sealed class McpClientPrompt /// /// /// This constructor enables reusing cached prompt definitions across different instances - /// without needing to call on every reconnect. This is particularly useful + /// without needing to call on every reconnect. This is particularly useful /// in scenarios where prompt definitions are stable and network round-trips should be minimized. /// /// @@ -83,7 +83,8 @@ public McpClientPrompt(McpClient client, Prompt prompt) /// The server will process the request and return a result containing messages or other content. /// /// - /// This is a convenience method that internally calls + /// This is a convenience method that internally calls + /// /// with this prompt's name and arguments. /// /// diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs index 73f75baeb..c8c4c75d6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Client; /// /// This class provides a client-side wrapper around a resource defined on an MCP server. It allows /// retrieving the resource's content by sending a request to the server with the resource's URI. -/// Instances of this class are typically obtained by calling . +/// Instances of this class are typically obtained by calling . /// /// public sealed class McpClientResource @@ -24,7 +24,7 @@ public sealed class McpClientResource /// /// /// This constructor enables reusing cached resource definitions across different instances - /// without needing to call on every reconnect. This is particularly useful + /// without needing to call on every reconnect. This is particularly useful /// in scenarios where resource definitions are stable and network round-trips should be minimized. /// /// diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs index f60be6585..c9eb1fb7c 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs @@ -9,7 +9,7 @@ namespace ModelContextProtocol.Client; /// /// This class provides a client-side wrapper around a resource template defined on an MCP server. It allows /// retrieving the resource template's content by sending a request to the server with the resource's URI. -/// Instances of this class are typically obtained by calling . +/// Instances of this class are typically obtained by calling . /// /// public sealed class McpClientResourceTemplate @@ -24,7 +24,7 @@ public sealed class McpClientResourceTemplate /// /// /// This constructor enables reusing cached resource template definitions across different instances - /// without needing to call on every reconnect. This is particularly useful + /// without needing to call on every reconnect. This is particularly useful /// in scenarios where resource template definitions are stable and network round-trips should be minimized. /// /// diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index b8bbab12c..a531a2653 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -19,7 +19,7 @@ namespace ModelContextProtocol.Client; /// and without changing the underlying tool functionality. /// /// -/// Typically, you would get instances of this class by calling the +/// Typically, you would get instances of this class by calling the /// method on an instance. /// /// @@ -49,8 +49,8 @@ public sealed class McpClientTool : AIFunction /// /// /// This constructor enables reusing cached tool definitions across different instances - /// without needing to call on every reconnect. This is particularly useful - /// in scenarios where tool definitions are stable and network round-trips should be minimized. + /// without needing to call on every reconnect. + /// This is particularly useful in scenarios where tool definitions are stable and network round-trips should be minimized. /// /// /// The provided must represent a tool that is actually available on the server @@ -275,6 +275,7 @@ public McpClientTool WithDescription(string description) => /// /// /// A new instance of , configured with the provided progress instance. + /// is . public McpClientTool WithProgress(IProgress progress) { Throw.IfNull(progress); diff --git a/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs b/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs index edb812485..95f5221f2 100644 --- a/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs @@ -39,6 +39,7 @@ public sealed partial class StdioClientTransport : IClientTransport /// /// Configuration options for the transport, including the command to execute, arguments, working directory, and environment variables. /// A logger factory for creating loggers used for diagnostic output during transport operations. + /// is . public StdioClientTransport(StdioClientTransportOptions options, ILoggerFactory? loggerFactory = null) { Throw.IfNull(options); diff --git a/src/ModelContextProtocol.Core/Client/StreamClientTransport.cs b/src/ModelContextProtocol.Core/Client/StreamClientTransport.cs index a0e335be6..deca7e6ef 100644 --- a/src/ModelContextProtocol.Core/Client/StreamClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamClientTransport.cs @@ -29,6 +29,7 @@ public sealed class StreamClientTransport : IClientTransport /// Reads from this stream will receive messages from the server. /// /// A logger factory for creating loggers. + /// or is . public StreamClientTransport( Stream serverInput, Stream serverOutput, ILoggerFactory? loggerFactory = null) { diff --git a/src/ModelContextProtocol.Core/McpSession.Methods.cs b/src/ModelContextProtocol.Core/McpSession.Methods.cs index 49b6d5913..3bba48b17 100644 --- a/src/ModelContextProtocol.Core/McpSession.Methods.cs +++ b/src/ModelContextProtocol.Core/McpSession.Methods.cs @@ -29,9 +29,13 @@ public ValueTask SendRequestAsync( serializerOptions ??= McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); - JsonTypeInfo paramsTypeInfo = serializerOptions.GetTypeInfo(); - JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); - return SendRequestAsync(method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); + return SendRequestAsync( + method, + parameters, + serializerOptions.GetTypeInfo(), + serializerOptions.GetTypeInfo(), + requestId, + cancellationToken); } /// @@ -76,6 +80,8 @@ internal async ValueTask SendRequestAsync( /// The notification method name. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous send operation. + /// is . + /// is empty or composed entirely of whitespace. /// /// /// This method sends a notification without any parameters. Notifications are one-way messages @@ -86,6 +92,7 @@ internal async ValueTask SendRequestAsync( public Task SendNotificationAsync(string method, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(method); + return SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken); } @@ -98,6 +105,8 @@ public Task SendNotificationAsync(string method, CancellationToken cancellationT /// 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. + /// is . + /// is empty or composed entirely of whitespace. /// /// /// This method sends a notification with parameters to the connected session. Notifications are one-way @@ -153,7 +162,7 @@ internal Task SendNotificationAsync( /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task representing the completion of the notification operation (not the operation being tracked). - /// The current session instance is . + /// is . /// /// /// This method sends a progress notification to the connected session using the Model Context Protocol's @@ -170,14 +179,44 @@ public Task NotifyProgressAsync( RequestOptions? options = null, CancellationToken cancellationToken = default) { - return SendNotificationAsync( - NotificationMethods.ProgressNotification, + Throw.IfNull(progress); + + return NotifyProgressAsync( new ProgressNotificationParams { ProgressToken = progressToken, Progress = progress, - Meta = options?.Meta, + Meta = options?.GetMetaForRequest(), }, + cancellationToken); + } + + /// + /// Notifies the connected session of progress for a long-running operation. + /// + /// The request parameters to send in the request. + /// 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 session 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. + /// + /// + public Task NotifyProgressAsync( + ProgressNotificationParams requestParams, + CancellationToken cancellationToken = default) + { + Throw.IfNull(requestParams); + + return SendNotificationAsync( + NotificationMethods.ProgressNotification, + requestParams, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, cancellationToken); } diff --git a/src/ModelContextProtocol.Core/NotificationHandlers.cs b/src/ModelContextProtocol.Core/NotificationHandlers.cs index da09c75e8..fb2f75981 100644 --- a/src/ModelContextProtocol.Core/NotificationHandlers.cs +++ b/src/ModelContextProtocol.Core/NotificationHandlers.cs @@ -94,7 +94,7 @@ public IAsyncDisposable Register( /// /// The notification method name to invoke handlers for. /// The notification object to pass to each handler. - /// A token that can be used to cancel the operation. + /// The to monitor for cancellation requests. The default is . /// /// Handlers are invoked in reverse order of registration (newest first). /// If any handler throws an exception, all handlers will still be invoked, and an diff --git a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs index be5c42d7f..8f222caa0 100644 --- a/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/ElicitRequestParams.cs @@ -391,7 +391,7 @@ public class Converter : JsonConverter return psd; } - private static IList DeserializeEnumOptions(ref Utf8JsonReader reader) + private static List DeserializeEnumOptions(ref Utf8JsonReader reader) { if (reader.TokenType != JsonTokenType.StartArray) { diff --git a/src/ModelContextProtocol.Core/Protocol/ProgressToken.cs b/src/ModelContextProtocol.Core/Protocol/ProgressToken.cs index bdb35f85b..553e239d3 100644 --- a/src/ModelContextProtocol.Core/Protocol/ProgressToken.cs +++ b/src/ModelContextProtocol.Core/Protocol/ProgressToken.cs @@ -13,6 +13,7 @@ namespace ModelContextProtocol.Protocol; { /// Initializes a new instance of the with a specified value. /// The required ID value. + /// is . public ProgressToken(string value) { Throw.IfNull(value); diff --git a/src/ModelContextProtocol.Core/Protocol/Reference.cs b/src/ModelContextProtocol.Core/Protocol/Reference.cs index 10ccfa886..2661aef32 100644 --- a/src/ModelContextProtocol.Core/Protocol/Reference.cs +++ b/src/ModelContextProtocol.Core/Protocol/Reference.cs @@ -12,8 +12,8 @@ namespace ModelContextProtocol.Protocol; /// /// /// -/// References are commonly used with to request completion suggestions for arguments, -/// and with other methods that need to reference resources or prompts. +/// References are commonly used with +/// to request completion suggestions for arguments, and with other methods that need to reference resources or prompts. /// /// /// See the schema for details. diff --git a/src/ModelContextProtocol.Core/Protocol/RequestId.cs b/src/ModelContextProtocol.Core/Protocol/RequestId.cs index c932bb7a7..47a6fde61 100644 --- a/src/ModelContextProtocol.Core/Protocol/RequestId.cs +++ b/src/ModelContextProtocol.Core/Protocol/RequestId.cs @@ -13,6 +13,7 @@ namespace ModelContextProtocol.Protocol; { /// Initializes a new instance of the with a specified value. /// The required ID value. + /// is . public RequestId(string value) { Throw.IfNull(value); diff --git a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs index cb1b02937..97897b53f 100644 --- a/src/ModelContextProtocol.Core/Protocol/TransportBase.cs +++ b/src/ModelContextProtocol.Core/Protocol/TransportBase.cs @@ -94,6 +94,8 @@ protected async Task WriteMessageAsync(JsonRpcMessage message, CancellationToken throw new InvalidOperationException("Transport is not connected."); } + cancellationToken.ThrowIfCancellationRequested(); + if (_logger.IsEnabled(LogLevel.Debug)) { var messageId = (message as JsonRpcMessageWithId)?.Id.ToString() ?? "(no id)"; diff --git a/src/ModelContextProtocol.Core/RequestOptions.cs b/src/ModelContextProtocol.Core/RequestOptions.cs index 2b2c4943b..ca900bf11 100644 --- a/src/ModelContextProtocol.Core/RequestOptions.cs +++ b/src/ModelContextProtocol.Core/RequestOptions.cs @@ -5,15 +5,10 @@ namespace ModelContextProtocol; /// -/// Contains optional parameters for MCP requests. +/// Provides a bag of optional parameters for use with MCP requests. /// public sealed class RequestOptions { - /// - /// Optional metadata to include in the request. - /// - private JsonObject? _meta; - /// /// Initializes a new instance of the class. /// @@ -22,75 +17,61 @@ public RequestOptions() } /// - /// Optional metadata to include in the request. - /// When getting, automatically includes the progress token if set. + /// Gets or sets optional metadata to include as the "_meta" property in a request. /// - public JsonObject? Meta - { - get => _meta ??= []; - set - { - // Capture the existing progressToken value if set. - var existingProgressToken = _meta?["progressToken"]; - - if (value is not null) - { - if (existingProgressToken is not null) - { - value["progressToken"] ??= existingProgressToken; - } + /// + /// Although progress tokens are propagated in MCP "_meta" objects, the + /// property and the property do not interact (setting + /// does not affect , and the object returned from + /// is not impacting by the value of ). To get the actual + /// that contains state from both and , use the + /// method. + /// + public JsonObject? Meta { get; set; } - _meta = value; - } - else if (existingProgressToken is not null) - { - _meta = new() - { - ["progressToken"] = existingProgressToken, - }; - } - else - { - _meta = null; - } - } - } + /// + /// Gets or sets an optional progress token to use for tracking long-running operations. + /// + /// + /// Although progress tokens are propagated in MCP "_meta" objects, the + /// property and the property do not interact (setting + /// does not affect , and getting does not read from + /// . To get the actual that contains state from both + /// and , use the method. + /// + public ProgressToken? ProgressToken { get; set; } /// - /// The serializer options governing tool parameter serialization. If null, the default options are used. + /// Gets or sets a to use for any serialization of arguments or results in the request. /// + /// + /// If , is used. + /// public JsonSerializerOptions? JsonSerializerOptions { get; set; } /// - /// The progress token for tracking long-running operations. + /// Gets a to use in requests for the "_meta" property. /// - public ProgressToken? ProgressToken + /// + /// A suitable for use in requests for the "_meta" property. + /// + /// + /// Progress tokens are part of MCP's _meta property. As such, if + /// is non- but is , will + /// manufacture and return a new instance containing the token. If both + /// and are non-, a new clone of will be created and its + /// "progressToken" property overwritten with . Otherwise, + /// will just return . + /// + public JsonObject? GetMetaForRequest() { - get + JsonObject? meta = Meta; + if (ProgressToken is not null) { - return _meta?["progressToken"] switch - { - JsonValue v when v.TryGetValue(out string? s) => new(s), - JsonValue v when v.TryGetValue(out long l) => new(l), - _ => null - }; - } - set - { - if (value?.Token is { } token) - { - _meta ??= []; - _meta["progressToken"] = token switch - { - string s => s, - long l => l, - _ => throw new InvalidOperationException("ProgressToken must be a string or long"), - }; - } - else - { - _meta?.Remove("progressToken"); - } + meta = (JsonObject?)meta?.DeepClone() ?? []; + meta["progressToken"] = ProgressToken.ToString(); } + + return meta; } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs index 7a0fa04ca..c55c76465 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerPrompt.cs @@ -137,12 +137,12 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( Description = options?.Description ?? function.Description, Arguments = args, Icons = options?.Icons, - }; - // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available - prompt.Meta = function.UnderlyingMethod is not null ? - AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta, options?.SerializerOptions) : - options?.Meta; + // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available + Meta = function.UnderlyingMethod is not null ? + AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta) : + options?.Meta + }; return new AIFunctionMcpServerPrompt(function, prompt, options?.Metadata ?? []); } diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs index 510806898..fcd855de9 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerResource.cs @@ -221,7 +221,7 @@ private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MimeType = options?.MimeType ?? "application/octet-stream", Icons = options?.Icons, Meta = function.UnderlyingMethod is not null ? - AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta, options?.SerializerOptions) : + AIFunctionMcpServerTool.CreateMetaFromAttributes(function.UnderlyingMethod, options?.Meta) : options?.Meta, }; diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index f714bb184..1d0f632d3 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -146,7 +146,7 @@ options.OpenWorld is not null || // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available tool.Meta = function.UnderlyingMethod is not null ? - CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta, options.SerializerOptions) : + CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta) : options.Meta; } @@ -361,9 +361,8 @@ internal static IReadOnlyList CreateMetadata(MethodInfo method) /// Creates a Meta from instances on the specified method. /// The method to extract instances from. /// Optional to seed the Meta with. Properties from this object take precedence over attributes. - /// Optional to use for serialization. This parameter is ignored when parsing JSON strings from attributes. /// A with metadata, or null if no metadata is present. - internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null, JsonSerializerOptions? serializerOptions = null) + internal static JsonObject? CreateMetaFromAttributes(MethodInfo method, JsonObject? meta = null) { // Transfer all McpMetaAttribute instances to the Meta JsonObject, ignoring any that would overwrite existing properties. foreach (var attr in method.GetCustomAttributes()) diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerPrompt.cs index b5e49002c..9fc4b574e 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerPrompt.cs @@ -13,6 +13,7 @@ public abstract class DelegatingMcpServerPrompt : McpServerPrompt /// Initializes a new instance of the class around the specified . /// The inner prompt wrapped by this delegating prompt. + /// is . protected DelegatingMcpServerPrompt(McpServerPrompt innerPrompt) { Throw.IfNull(innerPrompt); diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs index 56759b0ec..3058878e3 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerResource.cs @@ -13,6 +13,7 @@ public abstract class DelegatingMcpServerResource : McpServerResource /// Initializes a new instance of the class around the specified . /// The inner resource wrapped by this delegating resource. + /// is . protected DelegatingMcpServerResource(McpServerResource innerResource) { Throw.IfNull(innerResource); diff --git a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs index 6cad5e79a..cd14664bc 100644 --- a/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/DelegatingMcpServerTool.cs @@ -13,6 +13,7 @@ public abstract class DelegatingMcpServerTool : McpServerTool /// Initializes a new instance of the class around the specified . /// The inner tool wrapped by this delegating tool. + /// is . protected DelegatingMcpServerTool(McpServerTool innerTool) { Throw.IfNull(innerTool); diff --git a/src/ModelContextProtocol.Core/Server/McpRequestHandler.cs b/src/ModelContextProtocol.Core/Server/McpRequestHandler.cs index 651e070e5..bed0176a2 100644 --- a/src/ModelContextProtocol.Core/Server/McpRequestHandler.cs +++ b/src/ModelContextProtocol.Core/Server/McpRequestHandler.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol.Server; /// The type of the parameters sent with the request. /// The type of the response returned by the handler. /// The request context containing the parameters and other metadata. -/// A cancellation token to cancel the operation. +/// The to monitor for cancellation requests. The default is . /// A task representing the asynchronous operation, with the result of the handler. public delegate ValueTask McpRequestHandler( RequestContext request, diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index 28a0f52ed..cd052d0ee 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -47,18 +47,21 @@ public static McpServer Create( /// /// Requests to sample an LLM via the client using the specified request parameters. /// - /// The parameters for the sampling 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. public ValueTask SampleAsync( - CreateMessageRequestParams request, CancellationToken cancellationToken = default) + CreateMessageRequestParams requestParams, + CancellationToken cancellationToken = default) { + Throw.IfNull(requestParams); ThrowIfSamplingUnsupported(); return SendRequestAsync( RequestMethods.SamplingCreateMessage, - request, + requestParams, McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, McpJsonUtilities.JsonContext.Default.CreateMessageResult, cancellationToken: cancellationToken); @@ -157,7 +160,7 @@ public async Task SampleAsync( _ => null, }; - var result = await SampleAsync(new() + var result = await SampleAsync(new CreateMessageRequestParams { MaxTokens = chatOptions?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, Messages = samplingMessages, @@ -181,14 +184,16 @@ public async Task SampleAsync( return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContents)) { - ModelId = result.Model, + CreatedAt = DateTimeOffset.UtcNow, FinishReason = result.StopReason switch { + CreateMessageResult.StopReasonEndTurn => ChatFinishReason.Stop, CreateMessageResult.StopReasonMaxTokens => ChatFinishReason.Length, + CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop, CreateMessageResult.StopReasonToolUse => ChatFinishReason.ToolCalls, - CreateMessageResult.StopReasonEndTurn or CreateMessageResult.StopReasonStopSequence => ChatFinishReason.Stop, _ => null, - } + }, + ModelId = result.Model, }; } @@ -200,31 +205,33 @@ public async Task SampleAsync( public IChatClient AsSamplingChatClient() { ThrowIfSamplingUnsupported(); + return new SamplingChatClient(this); } /// Gets an on which logged messages will be sent as notifications to the client. /// An that can be used to log to the client. - public ILoggerProvider AsClientLoggerProvider() - { - return new ClientLoggerProvider(this); - } + public ILoggerProvider AsClientLoggerProvider() => + new ClientLoggerProvider(this); /// /// Requests the client to list the roots it exposes. /// - /// The parameters for the list roots 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. public ValueTask RequestRootsAsync( - ListRootsRequestParams request, CancellationToken cancellationToken = default) + ListRootsRequestParams requestParams, + CancellationToken cancellationToken = default) { + Throw.IfNull(requestParams); ThrowIfRootsUnsupported(); return SendRequestAsync( RequestMethods.RootsList, - request, + requestParams, McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, McpJsonUtilities.JsonContext.Default.ListRootsResult, cancellationToken: cancellationToken); @@ -233,19 +240,21 @@ public ValueTask RequestRootsAsync( /// /// Requests additional information from the user via the client, allowing the server to elicit structured data. /// - /// The parameters for the elicitation 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. public ValueTask ElicitAsync( - ElicitRequestParams request, CancellationToken cancellationToken = default) + ElicitRequestParams requestParams, + CancellationToken cancellationToken = default) { - Throw.IfNull(request); - ThrowIfElicitationUnsupported(request); + Throw.IfNull(requestParams); + ThrowIfElicitationUnsupported(requestParams); return SendRequestAsync( RequestMethods.ElicitationCreate, - request, + requestParams, McpJsonUtilities.JsonContext.Default.ElicitRequestParams, McpJsonUtilities.JsonContext.Default.ElicitResult, cancellationToken: cancellationToken); @@ -257,33 +266,40 @@ public ValueTask ElicitAsync( /// /// The type describing the expected input shape. Only primitive members are supported (string, number, boolean, enum). /// The message to present to the user. - /// Serializer options that influence property naming and deserialization. + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. /// An with the user's response, if accepted. + /// is . + /// is empty or composed entirely of whitespace. + /// The client does not support elicitation. /// /// Elicitation uses a constrained subset of JSON Schema and only supports strings, numbers/integers, booleans and string enums. /// Unsupported member types are ignored when constructing the schema. /// public async ValueTask> ElicitAsync( string message, - JsonSerializerOptions? serializerOptions = null, + RequestOptions? options = null, CancellationToken cancellationToken = default) { - serializerOptions ??= McpJsonUtilities.DefaultOptions; + Throw.IfNullOrWhiteSpace(message); + + var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); var dict = s_elicitResultSchemaCache.GetValue(serializerOptions, _ => new()); + var schema = dict.GetOrAdd(typeof(T), #if NET - var schema = dict.GetOrAdd(typeof(T), static (t, s) => BuildRequestSchema(t, s), serializerOptions); + static (t, s) => BuildRequestSchema(t, s), serializerOptions); #else - var schema = dict.GetOrAdd(typeof(T), type => BuildRequestSchema(type, serializerOptions)); + type => BuildRequestSchema(type, serializerOptions)); #endif var request = new ElicitRequestParams { Message = message, RequestedSchema = schema, + Meta = options?.GetMetaForRequest(), }; ThrowIfElicitationUnsupported(request); @@ -295,7 +311,7 @@ public async ValueTask> ElicitAsync( return new ElicitResult { Action = raw.Action, Content = default }; } - var obj = new JsonObject(); + JsonObject obj = []; foreach (var kvp in raw.Content) { obj[kvp.Key] = JsonNode.Parse(kvp.Value.GetRawText()); diff --git a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs index 18272ffe6..c4b043b0b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs @@ -201,7 +201,7 @@ public static McpServerPrompt Create( /// /// Optional options used in the creation of the to control its behavior. /// The created for invoking . - /// is . + /// or is . public static McpServerPrompt Create( MethodInfo method, Func, object> createTargetFunc, diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index 4441b437c..9f10b0545 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -170,6 +170,7 @@ protected McpServerResource() /// /// if the matches the ; otherwise, . /// + /// is . public abstract bool IsMatch(string uri); /// @@ -235,7 +236,7 @@ public static McpServerResource Create( /// /// Optional options used in the creation of the to control its behavior. /// The created for invoking . - /// is . + /// or is . public static McpServerResource Create( MethodInfo method, Func, object> createTargetFunc, diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index 4c0a4fb3a..cebf7209a 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -195,7 +195,7 @@ public static McpServerTool Create( /// /// Optional options used in the creation of the to control its behavior. /// The created for invoking . - /// is . + /// or is . public static McpServerTool Create( MethodInfo method, Func, object> createTargetFunc, diff --git a/src/ModelContextProtocol.Core/Server/RequestContext.cs b/src/ModelContextProtocol.Core/Server/RequestContext.cs index 6f1bc8566..a8d1f66c9 100644 --- a/src/ModelContextProtocol.Core/Server/RequestContext.cs +++ b/src/ModelContextProtocol.Core/Server/RequestContext.cs @@ -22,6 +22,7 @@ public sealed class RequestContext /// /// The server with which this instance is associated. /// The JSON-RPC request associated with this context. + /// or is . public RequestContext(McpServer server, JsonRpcRequest jsonRpcRequest) { Throw.IfNull(server); diff --git a/src/ModelContextProtocol.Core/Server/SseResponseStreamTransport.cs b/src/ModelContextProtocol.Core/Server/SseResponseStreamTransport.cs index 5d7315241..afdf29943 100644 --- a/src/ModelContextProtocol.Core/Server/SseResponseStreamTransport.cs +++ b/src/ModelContextProtocol.Core/Server/SseResponseStreamTransport.cs @@ -76,6 +76,7 @@ public async Task SendMessageAsync(JsonRpcMessage message, CancellationToken can /// The JSON-RPC message received from the client. /// The to monitor for cancellation requests. The default is . /// A task representing the asynchronous operation to buffer the JSON-RPC message for processing. + /// is . /// There is an attempt to process a message before calling . /// /// diff --git a/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs b/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs index 8a64094e4..c99b1fa39 100644 --- a/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StreamableHttpServerTransport.cs @@ -76,6 +76,11 @@ public sealed class StreamableHttpServerTransport : ITransport /// The response stream to write MCP JSON-RPC messages as SSE events to. /// The to monitor for cancellation requests. The default is . /// A task representing the send loop that writes JSON-RPC messages to the SSE response stream. + /// is . + /// + /// is and GET requests are not supported in stateless mode, + /// or a GET request has already been started for this session. + /// public async Task HandleGetRequestAsync(Stream sseResponseStream, CancellationToken cancellationToken = default) { Throw.IfNull(sseResponseStream); @@ -100,13 +105,14 @@ public async Task HandleGetRequestAsync(Stream sseResponseStream, CancellationTo /// to the that initiated the message. /// /// The JSON-RPC message received from the client via the POST request body. - /// This token allows for the operation to be canceled if needed. The default is . + /// The to monitor for cancellation requests. The default is . /// The POST response body to write MCP JSON-RPC messages to. /// /// if data was written to the response body. /// if nothing was written because the request body did not contain any messages to respond to. /// The HTTP application should typically respond with an empty "202 Accepted" response in this scenario. /// + /// or is . /// /// If an authenticated sent the message, that can be included in the . /// No other part of the context should be set. diff --git a/src/ModelContextProtocol.Core/UriTemplate.cs b/src/ModelContextProtocol.Core/UriTemplate.cs index 27e2f0d8d..447ec004c 100644 --- a/src/ModelContextProtocol.Core/UriTemplate.cs +++ b/src/ModelContextProtocol.Core/UriTemplate.cs @@ -178,6 +178,7 @@ static void AppendParameter(ref DefaultInterpolatedStringHandler pattern, string /// /// Expand a URI template using the given variable values. /// + /// is . public static string FormatUri(string uriTemplate, IReadOnlyDictionary arguments) { Throw.IfNull(uriTemplate); diff --git a/src/ModelContextProtocol.Core/UrlElicitationRequiredException.cs b/src/ModelContextProtocol.Core/UrlElicitationRequiredException.cs index 622c1def2..7ab23769c 100644 --- a/src/ModelContextProtocol.Core/UrlElicitationRequiredException.cs +++ b/src/ModelContextProtocol.Core/UrlElicitationRequiredException.cs @@ -19,6 +19,8 @@ public sealed class UrlElicitationRequiredException : McpProtocolException /// /// A description of why the elicitation is required. /// One or more URL-mode elicitation requests that must complete before retrying the original request. + /// is . + /// is empty or contains invalid elicitations. public UrlElicitationRequiredException(string message, IEnumerable elicitations) : base(message, McpErrorCode.UrlElicitationRequired) { @@ -66,7 +68,7 @@ internal static bool TryCreateFromError( private static bool TryParseElicitations(JsonElement dataElement, out IReadOnlyList elicitations) { - elicitations = Array.Empty(); + elicitations = []; if (dataElement.ValueKind is not JsonValueKind.Object) { diff --git a/src/ModelContextProtocol/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/McpServerBuilderExtensions.cs index 58470fbb6..e990cfcb9 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -59,7 +59,7 @@ public static partial class McpServerBuilderExtensions /// The target instance from which the tools should be sourced. /// The serializer options governing tool parameter marshalling. /// The builder provided in . - /// is . + /// or is . /// /// /// This method discovers all methods (public and non-public) on the specified @@ -244,7 +244,7 @@ where t.GetCustomAttribute() is not null /// The target instance from which the prompts should be sourced. /// The serializer options governing prompt parameter marshalling. /// The builder provided in . - /// is . + /// or is . /// /// /// This method discovers all methods (public and non-public) on the specified @@ -423,7 +423,7 @@ where t.GetCustomAttribute() is not null /// The builder instance. /// The target instance from which the prompts should be sourced. /// The builder provided in . - /// is . + /// or is . /// /// /// This method discovers all methods (public and non-public) on the specified diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index c2ac09c79..ce4f3b56a 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -35,7 +35,7 @@ public async Task ConnectAndPing_Sse_TestServer() // Act await using var client = await GetClientAsync(); - await client.PingAsync(null, TestContext.Current.CancellationToken); + await client.PingAsync(cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(client); @@ -139,7 +139,7 @@ public async Task ListResources_Sse_TestServer() // act await using var client = await GetClientAsync(); - IList allResources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + IList allResources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); // The everything server provides 100 test resources Assert.Equal(100, allResources.Count); @@ -190,7 +190,7 @@ public async Task ListPrompts_Sse_TestServer() // act await using var client = await GetClientAsync(); - var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); // assert Assert.NotNull(prompts); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index ffec1a4be..1a48cc86b 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -88,7 +88,7 @@ public async Task ConnectAndReceiveNotification_InMemoryServer() mcpServer.RegisterNotificationHandler("test/notification", async (notification, cancellationToken) => { Assert.Equal("Hello from client!", notification.Params?["message"]?.GetValue()); - await mcpServer.SendNotificationAsync("test/notification", new Envelope { Message = "Hello from server!" }, serializerOptions: JsonContext.Default.Options, cancellationToken: cancellationToken); + await mcpServer.SendNotificationAsync("test/notification", new Envelope { Message = "Hello from server!" }, serializerOptions: JsonContext.Default.Options, cancellationToken); }); return mcpServer.RunAsync(cancellationToken); }; diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index b400c6b0b..86cefcf10 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -407,7 +407,7 @@ public async Task AsClientLoggerProvider_MessagesSentToClient() Assert.False(logger.IsEnabled(LogLevel.Error)); Assert.False(logger.IsEnabled(LogLevel.Critical)); - await client.SetLoggingLevel(LoggingLevel.Info, options: null, TestContext.Current.CancellationToken); + await client.SetLoggingLevelAsync(LoggingLevel.Info, options: null, TestContext.Current.CancellationToken); DateTime start = DateTime.UtcNow; while (Server.LoggingLevel is null) @@ -610,4 +610,84 @@ async IAsyncEnumerable IChatClient.GetStreamingResponseAsync object? IChatClient.GetService(Type serviceType, object? serviceKey) => null; void IDisposable.Dispose() { } } + + [Fact] + public async Task ListToolsAsync_WithRequestParams_ReturnsTools() + { + await using McpClient client = await CreateMcpClientForServer(); + + var result = await client.ListToolsAsync(new ListToolsRequestParams(), TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Equal(12, result.Tools.Count); + Assert.Contains(result.Tools, t => t.Name == "Method4"); + } + + [Fact] + public async Task ListToolsAsync_WithRequestParams_NullThrows() + { + await using McpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync("requestParams", + () => client.ListToolsAsync((ListToolsRequestParams)null!, TestContext.Current.CancellationToken).AsTask()); + } + + [Fact] + public async Task CallToolAsync_WithRequestParams_ExecutesTool() + { + await using McpClient client = await CreateMcpClientForServer(); + + var result = await client.CallToolAsync( + new CallToolRequestParams + { + Name = "Method4", + Arguments = new Dictionary + { + ["i"] = JsonSerializer.SerializeToElement(42, McpJsonUtilities.DefaultOptions) + } + }, + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Contains("Method4 Result 42", result.Content.OfType().FirstOrDefault()?.Text); + } + + [Fact] + public async Task CallToolAsync_WithRequestParams_NullThrows() + { + await using McpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync("requestParams", + () => client.CallToolAsync((CallToolRequestParams)null!, TestContext.Current.CancellationToken).AsTask()); + } + + [Fact] + public async Task SetLoggingLevelAsync_WithRequestParams_SetsLevel() + { + await using McpClient client = await CreateMcpClientForServer(); + + // Should not throw + await client.SetLoggingLevelAsync( + new SetLevelRequestParams { Level = LoggingLevel.Warning }, + TestContext.Current.CancellationToken); + + // Wait a bit for the server to process + DateTime start = DateTime.UtcNow; + while (Server.LoggingLevel is null) + { + await Task.Delay(1, TestContext.Current.CancellationToken); + Assert.True(DateTime.UtcNow - start < TimeSpan.FromSeconds(10), "Timed out waiting for logging level to be set"); + } + + Assert.Equal(LoggingLevel.Warning, Server.LoggingLevel); + } + + [Fact] + public async Task SetLoggingLevelAsync_WithRequestParams_NullThrows() + { + await using McpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync("requestParams", + () => client.SetLoggingLevelAsync((SetLevelRequestParams)null!, TestContext.Current.CancellationToken)); + } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 0b29b1786..018e12dbe 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -34,7 +34,7 @@ public async Task ConnectAndPing_Stdio(string clientId) // Act await using var client = await _fixture.CreateClientAsync(clientId); - await client.PingAsync(null, TestContext.Current.CancellationToken); + await client.PingAsync(cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.NotNull(client); @@ -141,7 +141,7 @@ public async Task ListPrompts_Stdio(string clientId) // act await using var client = await _fixture.CreateClientAsync(clientId); - var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); // assert Assert.NotEmpty(prompts); @@ -206,7 +206,7 @@ public async Task ListResourceTemplates_Stdio(string clientId) // act await using var client = await _fixture.CreateClientAsync(clientId); - IList allResourceTemplates = await client.ListResourceTemplatesAsync(null, TestContext.Current.CancellationToken); + IList allResourceTemplates = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); // The server provides a single test resource template Assert.Single(allResourceTemplates); @@ -221,7 +221,7 @@ public async Task ListResources_Stdio(string clientId) // act await using var client = await _fixture.CreateClientAsync(clientId); - IList allResources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + IList allResources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); // The server provides 100 test resources Assert.Equal(100, allResources.Count); @@ -339,7 +339,7 @@ public async Task Complete_Stdio_ResourceTemplateReference(string clientId) var result = await client.CompleteAsync( new ResourceTemplateReference { Uri = "test://static/resource/1" }, "argument_name", "1", - TestContext.Current.CancellationToken + cancellationToken: TestContext.Current.CancellationToken ); Assert.NotNull(result); @@ -358,7 +358,7 @@ public async Task Complete_Stdio_PromptReference(string clientId) var result = await client.CompleteAsync( new PromptReference { Name = "irrelevant" }, argumentName: "style", argumentValue: "fo", - TestContext.Current.CancellationToken + cancellationToken: TestContext.Current.CancellationToken ); Assert.NotNull(result); @@ -573,12 +573,199 @@ public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId) }); // act - await client.SetLoggingLevel(LoggingLevel.Debug, options: null, TestContext.Current.CancellationToken); + await client.SetLoggingLevelAsync(LoggingLevel.Debug, options: null, TestContext.Current.CancellationToken); // assert await receivedNotification.Task; } + [Theory] + [MemberData(nameof(GetClients))] + public async Task ListToolsAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.ListToolsAsync(new ListToolsRequestParams(), TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.NotEmpty(result.Tools); + Assert.Contains(result.Tools, t => t.Name == "echo"); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task ListPromptsAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.ListPromptsAsync(new ListPromptsRequestParams(), TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.NotEmpty(result.Prompts); + Assert.Contains(result.Prompts, p => p.Name == "simple_prompt"); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task GetPromptAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.GetPromptAsync( + new GetPromptRequestParams { Name = "simple_prompt" }, + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.NotEmpty(result.Messages); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task ListResourceTemplatesAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.ListResourceTemplatesAsync( + new ListResourceTemplatesRequestParams(), + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Single(result.ResourceTemplates); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task ListResourcesAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.ListResourcesAsync( + new ListResourcesRequestParams(), + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + // Low-level API returns only one page; the server provides 100 resources but paginates + Assert.NotEmpty(result.Resources); + Assert.True(result.Resources.Count <= 100); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task ReadResourceAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.ReadResourceAsync( + new ReadResourceRequestParams { Uri = "test://static/resource/1" }, + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Single(result.Contents); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task CompleteAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.CompleteAsync( + new CompleteRequestParams + { + Ref = new PromptReference { Name = "irrelevant" }, + Argument = new Argument { Name = "style", Value = "fo" } + }, + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Single(result.Completion.Values); + Assert.Equal("formal", result.Completion.Values[0]); + } + + [Theory] + [MemberData(nameof(GetClients))] + public async Task CallToolAsync_WithRequestParams_ReturnsRawResult(string clientId) + { + await using var client = await _fixture.CreateClientAsync(clientId); + + var result = await client.CallToolAsync( + new CallToolRequestParams + { + Name = "echo", + Arguments = new Dictionary + { + ["message"] = JsonSerializer.SerializeToElement("Hello from RequestParams!", McpJsonUtilities.DefaultOptions) + } + }, + TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Null(result.IsError); + var textContent = Assert.Single(result.Content.OfType()); + Assert.Equal("Echo: Hello from RequestParams!", textContent.Text); + } + + // Not supported by "everything" server version on npx + [Fact] + public async Task SubscribeToResourceAsync_WithRequestParams_Succeeds() + { + var clientId = "test_server"; + + TaskCompletionSource tcs = new(); + await using var client = await _fixture.CreateClientAsync(clientId, new() + { + Handlers = new() + { + NotificationHandlers = + [ + new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => + { + tcs.TrySetResult(true); + return default; + }) + ] + } + }); + + await client.SubscribeToResourceAsync( + new SubscribeRequestParams { Uri = "test://static/resource/1" }, + TestContext.Current.CancellationToken); + + await tcs.Task; + } + + // Not supported by "everything" server version on npx + [Fact] + public async Task UnsubscribeFromResourceAsync_WithRequestParams_Succeeds() + { + var clientId = "test_server"; + + TaskCompletionSource receivedNotification = new(); + await using var client = await _fixture.CreateClientAsync(clientId, new() + { + Handlers = new() + { + NotificationHandlers = + [ + new(NotificationMethods.ResourceUpdatedNotification, (notification, cancellationToken) => + { + receivedNotification.TrySetResult(true); + return default; + }) + ] + } + }); + await client.SubscribeToResourceAsync( + new SubscribeRequestParams { Uri = "test://static/resource/1" }, + TestContext.Current.CancellationToken); + + await receivedNotification.Task; + + await client.UnsubscribeFromResourceAsync( + new UnsubscribeRequestParams { Uri = "test://static/resource/1" }, + TestContext.Current.CancellationToken); + } + [JsonSerializable(typeof(TestNotification))] partial class JsonContext3 : JsonSerializerContext; } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs index fb35fee85..79644eb6b 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsFilterTests.cs @@ -277,7 +277,7 @@ public async Task AddSetLoggingLevelFilter_Logs_When_SetLoggingLevel_Called() { await using McpClient client = await CreateMcpClientForServer(); - await client.SetLoggingLevel(LoggingLevel.Info, cancellationToken: TestContext.Current.CancellationToken); + await client.SetLoggingLevelAsync(LoggingLevel.Info, cancellationToken: TestContext.Current.CancellationToken); var logMessage = Assert.Single(_mockLoggerProvider.LogMessages, m => m.Message == "SetLoggingLevelFilter executed"); Assert.Equal(LogLevel.Information, logMessage.LogLevel); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 40aa343b8..3b9137f61 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -100,7 +100,7 @@ public async Task Can_List_And_Call_Registered_Prompts() { await using McpClient client = await CreateMcpClientForServer(); - var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); var prompt = prompts.First(t => t.Name == "returns_chat_messages"); @@ -129,7 +129,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() { await using McpClient client = await CreateMcpClientForServer(); - var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); Channel listChanged = Channel.CreateUnbounded(); @@ -150,7 +150,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() serverPrompts.Add(newPrompt); await notificationRead; - prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); + prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(7, prompts.Count); Assert.Contains(prompts, t => t.Name == "NewPrompt"); @@ -160,7 +160,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() await notificationRead; } - prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); + prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); Assert.DoesNotContain(prompts, t => t.Name == "NewPrompt"); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index da5361641..b25c4ea17 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -129,7 +129,7 @@ public async Task Can_List_And_Call_Registered_Resources() Assert.NotNull(client.ServerCapabilities.Resources); - var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(5, resources.Count); var resource = resources.First(t => t.Name == "some_neat_direct_resource"); @@ -146,7 +146,7 @@ public async Task Can_List_And_Call_Registered_ResourceTemplates() { await using McpClient client = await CreateMcpClientForServer(); - var resources = await client.ListResourceTemplatesAsync(null, TestContext.Current.CancellationToken); + var resources = await client.ListResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(3, resources.Count); var resource = resources.First(t => t.Name == "some_neat_templated_resource"); @@ -163,7 +163,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes() { await using McpClient client = await CreateMcpClientForServer(); - var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(5, resources.Count); Channel listChanged = Channel.CreateUnbounded(); @@ -184,7 +184,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes() serverResources.Add(newResource); await notificationRead; - resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(6, resources.Count); Assert.Contains(resources, t => t.Name == "NewResource"); @@ -194,7 +194,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes() await notificationRead; } - resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(5, resources.Count); Assert.DoesNotContain(resources, t => t.Name == "NewResource"); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 4fe51fc86..2b8fb0bc6 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -682,8 +682,7 @@ public async Task HandlesIProgressParameter() return default; })) { - var result = await client.SendRequestAsync( - RequestMethods.ToolsCall, + var result = await client.CallToolAsync( new CallToolRequestParams { Name = progressTool.ProtocolTool.Name, diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceCapabilityIntegrationTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceCapabilityIntegrationTests.cs index 0f457aae2..f9b6217cf 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceCapabilityIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceCapabilityIntegrationTests.cs @@ -41,7 +41,7 @@ public async Task Client_CanListResources_WhenSubscribeCapabilityIsManuallySet() Assert.True(client.ServerCapabilities.Resources.Subscribe, "Server should advertise Subscribe capability when manually set"); // The resources should be exposed and listable - var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotEmpty(resources); Assert.Contains(resources, r => r.Name == "test_resource"); } @@ -56,7 +56,7 @@ public async Task Client_CanListResources_WhenCapabilitySetViaAddMcpServerCallba Assert.NotNull(client.ServerCapabilities.Resources); // The resources should be exposed and listable - var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotEmpty(resources); } diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs index 66a6f22af..6f0988e48 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTypedTests.cs @@ -23,8 +23,8 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer { var result = await request.Server.ElicitAsync( message: "Please provide more information.", - serializerOptions: ElicitationTypedDefaultJsonContext.Default.Options, - cancellationToken: CancellationToken.None); + options: new() { JsonSerializerOptions = ElicitationTypedDefaultJsonContext.Default.Options }, + CancellationToken.None); Assert.Equal("accept", result.Action); Assert.NotNull(result.Content); @@ -38,8 +38,8 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer { var result = await request.Server.ElicitAsync( message: "Please provide more information.", - serializerOptions: ElicitationTypedCamelJsonContext.Default.Options, - cancellationToken: CancellationToken.None); + options: new() { JsonSerializerOptions = ElicitationTypedCamelJsonContext.Default.Options }, + CancellationToken.None); Assert.Equal("accept", result.Action); Assert.NotNull(result.Content); @@ -51,8 +51,8 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer { var result = await request.Server.ElicitAsync( message: "Please provide more information.", - serializerOptions: ElicitationNullablePropertyJsonContext.Default.Options, - cancellationToken: CancellationToken.None); + options: new() { JsonSerializerOptions = ElicitationNullablePropertyJsonContext.Default.Options }, + CancellationToken.None); // Should be unreachable return new CallToolResult @@ -64,8 +64,8 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer { await request.Server.ElicitAsync( message: "Please provide more information.", - serializerOptions: ElicitationUnsupportedJsonContext.Default.Options, - cancellationToken: CancellationToken.None); + options: new() { JsonSerializerOptions = ElicitationUnsupportedJsonContext.Default.Options }, + CancellationToken.None); // Should be unreachable return new CallToolResult @@ -78,8 +78,8 @@ await request.Server.ElicitAsync( // This should throw because T is not an object type with properties (string primitive) await request.Server.ElicitAsync( message: "Any message", - serializerOptions: McpJsonUtilities.DefaultOptions, - cancellationToken: CancellationToken.None); + options: new() { JsonSerializerOptions = McpJsonUtilities.DefaultOptions }, + CancellationToken.None); return new CallToolResult { @@ -90,8 +90,8 @@ await request.Server.ElicitAsync( { var result = await request.Server.ElicitAsync( message: "Please provide information.", - serializerOptions: ElicitationDefaultsJsonContext.Default.Options, - cancellationToken: CancellationToken.None); + options: new() { JsonSerializerOptions = ElicitationDefaultsJsonContext.Default.Options }, + CancellationToken.None); // The test will validate the schema in the client handler return new CallToolResult diff --git a/tests/ModelContextProtocol.Tests/RequestOptionsTests.cs b/tests/ModelContextProtocol.Tests/RequestOptionsTests.cs index fe54900e8..1df78608a 100644 --- a/tests/ModelContextProtocol.Tests/RequestOptionsTests.cs +++ b/tests/ModelContextProtocol.Tests/RequestOptionsTests.cs @@ -7,442 +7,257 @@ namespace ModelContextProtocol.Tests; public static class RequestOptionsTests { [Fact] - public static void RequestOptions_DefaultConstructor() + public static void DefaultConstructor_AllPropertiesNull() { - // Arrange & Act - var options = new RequestOptions(); + RequestOptions options = new(); - // Assert - // ProgressToken and JsonSerializerOptions should be null by default + Assert.Null(options.Meta); Assert.Null(options.ProgressToken); Assert.Null(options.JsonSerializerOptions); - // Meta should return a new (empty) JsonObject when accessed - Assert.NotNull(options.Meta); - Assert.Empty(options.Meta); } [Fact] - public static void RequestOptions_MetaSetter_SetsValue() + public static void Meta_GetSet_RoundTrips() { - // Arrange - var options = new RequestOptions(); - var metaValue = new JsonObject - { - ["key"] = "value" - }; - - // Act - options.Meta = metaValue; - - // Assert - Assert.Same(metaValue, options.Meta); - Assert.Equal("value", options.Meta["key"]?.ToString()); - } - - [Fact] - public static void RequestOptions_Meta_SetsSingleField() - { - // Arrange - var options = new RequestOptions(); - - // Act - options.Meta!["key"] = "value"; - - // Assert - Assert.Equal("value", options.Meta!["key"]?.ToString()); - } + RequestOptions options = new(); + JsonObject meta = new() { ["key"] = "value" }; - [Fact] - public static void RequestOptions_MetaSetter_NullValue_RemovesMeta() - { - // Arrange - var options = new RequestOptions + for (int i = 0; i < 2; i++) { - Meta = new JsonObject { ["key"] = "value" } - }; - - // Act - options.Meta = null; - - // Assert - accessing Meta will create a new empty JsonObject - Assert.NotNull(options.Meta); - Assert.Empty(options.Meta); + options.Meta = meta; + Assert.Same(meta, options.Meta); + Assert.Null(options.ProgressToken); + + options.Meta = null; + Assert.Null(options.Meta); + Assert.Null(options.ProgressToken); + } } [Fact] - public static void RequestOptions_MetaSetter_WithProgressToken_InMeta() + public static void ProgressToken_GetSet_RoundTrips() { - // Arrange - var options = new RequestOptions(); - var newMeta = new JsonObject - { - ["custom"] = "data", - ["progressToken"] = "token" - }; - - // Act - options.Meta = newMeta; - - // Assert - Assert.Equal("token", options.ProgressToken?.ToString()); - Assert.Equal("data", options.Meta["custom"]?.ToString()); - Assert.True(options.Meta.ContainsKey("progressToken")); - } + RequestOptions options = new(); + ProgressToken token = new("my-token"); - [Fact] - public static void RequestOptions_ProgressTokenGetter_ReturnsNull_WhenNotSet() - { - // Arrange - var options = new RequestOptions + for (int i = 0; i < 2; i++) { - Meta = new JsonObject { ["other"] = "value" } - }; - - // Act - var token = options.ProgressToken; - - // Assert - Assert.Null(token); + options.ProgressToken = token; + Assert.Equal(token, options.ProgressToken); + Assert.Null(options.Meta); + + options.ProgressToken = null; + Assert.Null(options.ProgressToken); + Assert.Null(options.Meta); + } } [Fact] - public static void RequestOptions_ProgressTokenSetter_StringToken_SetsInMeta() + public static void ProgressToken_DoesNotAffectMeta() { - // Arrange - var options = new RequestOptions(); - var token = new ProgressToken("my-token"); + RequestOptions options = new() { Meta = new JsonObject { ["existing"] = "data" } }; - // Act - options.ProgressToken = token; + options.ProgressToken = new ProgressToken("my-token"); - // Assert - Assert.Equal("my-token", options.ProgressToken?.ToString()); - Assert.NotNull(options.Meta); - Assert.Equal("my-token", options.Meta["progressToken"]?.ToString()); + Assert.False(options.Meta.ContainsKey("progressToken")); + Assert.Equal("data", options.Meta["existing"]?.ToString()); } [Fact] - public static void RequestOptions_ProgressTokenSetter_LongToken_SetsInMeta() + public static void Meta_DoesNotAffectProgressToken() { - // Arrange - var options = new RequestOptions(); - var token = new ProgressToken(42L); + RequestOptions options = new() { ProgressToken = new ProgressToken("original") }; - // Act - options.ProgressToken = token; + options.Meta = new JsonObject { ["progressToken"] = "in-meta" }; - // Assert - Assert.Equal("42", options.ProgressToken?.ToString()); - Assert.NotNull(options.Meta); - Assert.Equal(42L, options.Meta["progressToken"]?.AsValue().GetValue()); + Assert.Equal("original", options.ProgressToken?.ToString()); } [Fact] - public static void RequestOptions_ProgressTokenSetter_Null_RemovesFromMeta() + public static void JsonSerializerOptions_GetSet_RoundTrips() { - // Arrange - var options = new RequestOptions - { - ProgressToken = new ProgressToken("token-to-remove") - }; + RequestOptions options = new(); + JsonSerializerOptions serializerOptions = new() { WriteIndented = true }; - // Act - options.ProgressToken = null; + for (int i = 0; i < 2; i++) + { + options.JsonSerializerOptions = serializerOptions; + Assert.Same(serializerOptions, options.JsonSerializerOptions); - // Assert - Assert.Null(options.ProgressToken); - Assert.False(options.Meta!.ContainsKey("progressToken")); + options.JsonSerializerOptions = null; + Assert.Null(options.JsonSerializerOptions); + } } [Fact] - public static void RequestOptions_ProgressTokenSetter_Null_WhenNoProgressToken_DoesNothing() + public static void GetMetaForRequest_BothNull_ReturnsNull() { - // Arrange - var options = new RequestOptions(); + RequestOptions options = new(); - // Act - options.ProgressToken = null; + var actual = options.GetMetaForRequest(); - // Assert - Assert.Null(options.ProgressToken); + Assert.Null(actual); } [Fact] - public static void RequestOptions_ProgressTokenGetter_StringValue_ReturnsCorrectToken() + public static void GetMetaForRequest_OnlyMetaSet_ReturnsMeta() { - // Arrange - var options = new RequestOptions - { - Meta = new JsonObject { ["progressToken"] = "test-token" } - }; + JsonObject meta = new() { ["key"] = "value" }; + RequestOptions options = new() { Meta = meta }; - // Act - var token = options.ProgressToken; + var actual = options.GetMetaForRequest(); - // Assert - Assert.NotNull(token); - Assert.Equal("test-token", token.Value.ToString()); + Assert.Same(meta, actual); } [Fact] - public static void RequestOptions_ProgressTokenGetter_LongValue_ReturnsCorrectToken() + public static void GetMetaForRequest_OnlyProgressTokenSet_ReturnsNewObjectWithToken() { - // Arrange - var options = new RequestOptions - { - Meta = new JsonObject { ["progressToken"] = 123L } - }; + RequestOptions options = new() { ProgressToken = new ProgressToken("my-token") }; - // Act - var token = options.ProgressToken; + var actual = options.GetMetaForRequest(); - // Assert - Assert.NotNull(token); - Assert.Equal("123", token.Value.ToString()); - } - - [Fact] - public static void RequestOptions_ProgressTokenGetter_InvalidValue_ReturnsNull() - { - // Arrange - var options = new RequestOptions - { - Meta = new JsonObject { ["progressToken"] = new JsonObject() } - }; + Assert.NotNull(actual); + Assert.Single(actual); + Assert.Equal("my-token", actual["progressToken"]?.ToString()); - // Act - var token = options.ProgressToken; - - // Assert - Assert.Null(token); + Assert.NotSame(actual, options.Meta); + Assert.NotSame(actual, options.GetMetaForRequest()); } [Fact] - public static void RequestOptions_JsonSerializerOptions_GetSet() + public static void GetMetaForRequest_OnlyProgressTokenSetAsLong_ReturnsNewObjectWithToken() { - // Arrange - var options = new RequestOptions(); - var serializerOptions = new JsonSerializerOptions { WriteIndented = true }; + RequestOptions options = new() { ProgressToken = new ProgressToken(42L) }; - // Act - options.JsonSerializerOptions = serializerOptions; + var actual = options.GetMetaForRequest(); - // Assert - Assert.Same(serializerOptions, options.JsonSerializerOptions); - } + Assert.NotNull(actual); + Assert.Single(actual); + Assert.Equal("42", actual["progressToken"]?.ToString()); - [Fact] - public static void RequestOptions_MetaAndProgressToken_WorkTogether() - { - // Arrange - var options = new RequestOptions(); - - // Act - set progress token first - options.ProgressToken = new ProgressToken("token1"); - options.Meta!["custom"] = "value1"; - - // Assert - Assert.Equal("token1", options.ProgressToken?.ToString()); - Assert.Equal("value1", options.Meta["custom"]?.ToString()); - Assert.True(options.Meta.ContainsKey("progressToken")); + Assert.NotSame(actual, options.Meta); + Assert.NotSame(actual, options.GetMetaForRequest()); } [Fact] - public static void RequestOptions_ProgressToken_OverwritesPreviousValue() + public static void GetMetaForRequest_BothSet_ReturnsCloneWithProgressToken() { - // Arrange - var options = new RequestOptions + JsonObject meta = new() { ["custom"] = "data" }; + RequestOptions options = new() { - ProgressToken = new ProgressToken("old-token") + Meta = meta, + ProgressToken = new ProgressToken("my-token") }; - // Act - options.ProgressToken = new ProgressToken("new-token"); - - // Assert - Assert.Equal("new-token", options.ProgressToken?.ToString()); - Assert.Equal("new-token", options.Meta!["progressToken"]?.ToString()); - } - - [Fact] - public static void RequestOptions_ProgressToken_StringToLong_ChangesType() - { - // Arrange - var options = new RequestOptions - { - ProgressToken = new ProgressToken("string-token") - }; + var actual = options.GetMetaForRequest(); - // Act - options.ProgressToken = new ProgressToken(999L); + Assert.NotNull(actual); + Assert.NotSame(meta, actual); + Assert.Equal("data", actual["custom"]?.ToString()); + Assert.Equal("my-token", actual["progressToken"]?.ToString()); - // Assert - Assert.Equal("999", options.ProgressToken?.ToString()); - Assert.Equal(999L, options.Meta!["progressToken"]?.AsValue().GetValue()); + Assert.NotSame(actual, options.Meta); + Assert.NotSame(actual, options.GetMetaForRequest()); } [Fact] - public static void RequestOptions_Meta_MultipleProperties_Preserved() + public static void GetMetaForRequest_BothSet_DoesNotModifyOriginalMeta() { - // Arrange - var options = new RequestOptions + JsonObject meta = new() { ["custom"] = "data" }; + RequestOptions options = new() { - Meta = new JsonObject - { - ["prop1"] = "value1", - ["prop2"] = 42, - ["prop3"] = true - } + Meta = meta, + ProgressToken = new ProgressToken("my-token") }; - // Act - options.ProgressToken = new ProgressToken("my-token"); + _ = options.GetMetaForRequest(); - // Assert - Assert.Equal("value1", options.Meta["prop1"]?.ToString()); - Assert.Equal(42, options.Meta["prop2"]?.AsValue().GetValue()); - Assert.True(options.Meta["prop3"]?.AsValue().GetValue()); - Assert.Equal("my-token", options.Meta["progressToken"]?.ToString()); + Assert.False(meta.ContainsKey("progressToken")); + Assert.Single(meta); } [Fact] - public static void RequestOptions_MetaSetter_ReplacesExistingMeta() + public static void GetMetaForRequest_MetaHasProgressToken_OverwrittenByProperty() { - // Arrange - var options = new RequestOptions + JsonObject meta = new() { - Meta = new JsonObject - { - ["old1"] = "value1", - ["old2"] = "value2" - } + ["custom"] = "data", + ["progressToken"] = "meta-token" }; - - var newMeta = new JsonObject + RequestOptions options = new() { - ["new1"] = "newValue1", - ["new2"] = "newValue2" + Meta = meta, + ProgressToken = new ProgressToken("property-token") }; - // Act - options.Meta = newMeta; + var actual = options.GetMetaForRequest(); - // Assert - Assert.False(options.Meta.ContainsKey("old1")); - Assert.False(options.Meta.ContainsKey("old2")); - Assert.Equal("newValue1", options.Meta["new1"]?.ToString()); - Assert.Equal("newValue2", options.Meta["new2"]?.ToString()); + Assert.Equal("property-token", actual!["progressToken"]?.ToString()); + Assert.Equal("data", actual["custom"]?.ToString()); } [Fact] - public static void RequestOptions_MetaSetter_NullWithNoProgressToken_ClearsMeta() + public static void GetMetaForRequest_MetaHasProgressToken_NoPropertyToken_PreservesMetaToken() { - // Arrange - var options = new RequestOptions + JsonObject meta = new() { - Meta = new JsonObject { ["key"] = "value" } + ["custom"] = "data", + ["progressToken"] = "meta-token" }; + RequestOptions options = new() { Meta = meta }; - // Act - options.Meta = null; - - // Assert - getter creates new empty object - Assert.NotNull(options.Meta); - Assert.Empty(options.Meta); - } - - [Fact] - public static void RequestOptions_ProgressTokenSetter_CreatesMetaIfNeeded() - { - // Arrange - var options = new RequestOptions(); - Assert.Null(options.ProgressToken); - - // Act - options.ProgressToken = new ProgressToken("create-meta"); + var actual = options.GetMetaForRequest(); - // Assert - Assert.NotNull(options.ProgressToken); - Assert.Equal("create-meta", options.ProgressToken?.ToString()); - Assert.True(options.Meta!.ContainsKey("progressToken")); + Assert.Same(meta, actual); + Assert.Equal("meta-token", actual!["progressToken"]?.ToString()); } [Fact] - public static void RequestOptions_ComplexScenario_SetMetaThenProgressToken() + public static void GetMetaForRequest_CalledMultipleTimes_ReturnsNewCloneEachTime() { - // Arrange - var options = new RequestOptions + RequestOptions options = new() { - Meta = new JsonObject - { - ["custom1"] = "value1", - ["custom2"] = 123 - } + Meta = new JsonObject { ["key"] = "value" }, + ProgressToken = new ProgressToken("token") }; - // Act - options.ProgressToken = new ProgressToken("scenario-token"); + var actual1 = options.GetMetaForRequest(); + var actual2 = options.GetMetaForRequest(); - // Assert - Assert.Equal("value1", options.Meta["custom1"]?.ToString()); - Assert.Equal(123, options.Meta["custom2"]?.AsValue().GetValue()); - Assert.Equal("scenario-token", options.ProgressToken?.ToString()); - Assert.Equal(3, options.Meta.Count); + Assert.NotSame(actual1, actual2); + Assert.Equal(actual1!.ToJsonString(), actual2!.ToJsonString()); } [Fact] - public static void RequestOptions_ComplexScenario_SetMetaWithProgressToken() + public static void GetMetaForRequest_OnlyMeta_SameInstanceOnMultipleCalls() { - // Arrange - var options = new RequestOptions(); + JsonObject meta = new() { ["key"] = "value" }; + RequestOptions options = new() { Meta = meta }; - // Act - var newMeta = new JsonObject - { - ["data1"] = "info1", - ["data2"] = false, - ["progressToken"] = 456L - }; - options.Meta = newMeta; + var actual1 = options.GetMetaForRequest(); + var actual2 = options.GetMetaForRequest(); - // Assert - Assert.Equal("info1", options.Meta["data1"]?.ToString()); - Assert.False(options.Meta["data2"]?.AsValue().GetValue()); - Assert.Equal(456L, options.ProgressToken?.Token as long?); - Assert.Equal(3, options.Meta.Count); + Assert.Same(actual1, actual2); + Assert.Same(meta, actual1); } [Fact] - public static void RequestOptions_ComplexScenario_SetMetaWithProgressUpdatesProgress() + public static void AllProperties_CanBeSetIndependently() { - // Arrange - RequestOptions options = new(); + JsonObject meta = new() { ["field"] = "value" }; + ProgressToken token = new("independent"); + JsonSerializerOptions serializerOptions = new() { WriteIndented = true }; - // Act - options.ProgressToken = new ProgressToken("token1"); - JsonObject meta = new() { ["progressToken"] = "token2" }; - options.Meta = meta; - - // Assert - Assert.Equal("token2", options.ProgressToken?.Token as string); - Assert.Equal("token2", options.Meta["progressToken"]?.ToString()); - } + RequestOptions options = new() + { + Meta = meta, + ProgressToken = token, + JsonSerializerOptions = serializerOptions + }; - [Fact] - public static void RequestOptions_AllProperties_CanBeSetIndependently() - { - // Arrange - var options = new RequestOptions(); - var customOptions = new JsonSerializerOptions { WriteIndented = true }; - - // Act - options.JsonSerializerOptions = customOptions; - options.ProgressToken = new ProgressToken("independent"); - options.Meta!["field"] = "value"; - - // Assert - Assert.Same(customOptions, options.JsonSerializerOptions); - Assert.Equal("independent", options.ProgressToken?.ToString()); - Assert.Equal("value", options.Meta["field"]?.ToString()); + Assert.Same(meta, options.Meta); + Assert.Equal(token, options.ProgressToken); + Assert.Same(serializerOptions, options.JsonSerializerOptions); } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index da75459a0..12db80f78 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -130,7 +130,9 @@ public async Task SampleAsync_Should_Throw_Exception_If_Client_Does_Not_Support_ await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities()); - var action = async () => await server.SampleAsync(new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, CancellationToken.None); + var action = async () => await server.SampleAsync( + new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, + CancellationToken.None); // Act & Assert await Assert.ThrowsAsync(action); @@ -147,7 +149,9 @@ public async Task SampleAsync_Should_SendRequest() var runTask = server.RunAsync(TestContext.Current.CancellationToken); // Act - var result = await server.SampleAsync(new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, CancellationToken.None); + var result = await server.SampleAsync( + new CreateMessageRequestParams { Messages = [], MaxTokens = 1000 }, + CancellationToken.None); Assert.NotNull(result); Assert.NotEmpty(transport.SentMessages); @@ -167,7 +171,9 @@ public async Task RequestRootsAsync_Should_Throw_Exception_If_Client_Does_Not_Su SetClientCapabilities(server, new ClientCapabilities()); // Act & Assert - await Assert.ThrowsAsync(async () => await server.RequestRootsAsync(new ListRootsRequestParams(), CancellationToken.None)); + await Assert.ThrowsAsync(async () => await server.RequestRootsAsync( + new ListRootsRequestParams(), + CancellationToken.None)); } [Fact] @@ -201,7 +207,9 @@ public async Task ElicitAsync_Should_Throw_Exception_If_Client_Does_Not_Support_ SetClientCapabilities(server, new ClientCapabilities()); // Act & Assert - await Assert.ThrowsAsync(async () => await server.ElicitAsync(new ElicitRequestParams { Message = "" }, CancellationToken.None)); + await Assert.ThrowsAsync(async () => await server.ElicitAsync( + new ElicitRequestParams { Message = "" }, + CancellationToken.None)); } [Fact] @@ -943,4 +951,54 @@ await transport.SendMessageAsync(new JsonRpcNotification await server.DisposeAsync(); await serverTask; } + + [Fact] + public async Task NotifyProgressAsync_WithRequestParams_SendsNotification() + { + await using TestServerTransport transport = new(); + var options = CreateOptions(); + + var server = McpServer.Create(transport, options, LoggerFactory); + + Task serverTask = server.RunAsync(TestContext.Current.CancellationToken); + + var progressParams = new ProgressNotificationParams + { + ProgressToken = new("test-token"), + Progress = new() + { + Progress = 25, + Total = 100, + Message = "Sending progress via params", + }, + }; + + await server.NotifyProgressAsync(progressParams, TestContext.Current.CancellationToken); + + // Verify the notification was sent + var notification = Assert.IsType( + transport.SentMessages.FirstOrDefault(m => m is JsonRpcNotification n && n.Method == NotificationMethods.ProgressNotification)); + Assert.NotNull(notification); + var sentProgress = JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions); + Assert.NotNull(sentProgress); + Assert.Equal("test-token", sentProgress.ProgressToken.ToString()); + Assert.Equal(25, sentProgress.Progress.Progress); + Assert.Equal(100, sentProgress.Progress.Total); + Assert.Equal("Sending progress via params", sentProgress.Progress.Message); + + await server.DisposeAsync(); + await serverTask; + } + + [Fact] + public async Task NotifyProgressAsync_WithRequestParams_NullThrows() + { + await using TestServerTransport transport = new(); + var options = CreateOptions(); + + await using var server = McpServer.Create(transport, options, LoggerFactory); + + await Assert.ThrowsAsync("requestParams", + () => server.NotifyProgressAsync((ProgressNotificationParams)null!, TestContext.Current.CancellationToken)); + } }