diff --git a/samples/EverythingServer/Program.cs b/samples/EverythingServer/Program.cs index acaa7a37..86e46ec6 100644 --- a/samples/EverythingServer/Program.cs +++ b/samples/EverythingServer/Program.cs @@ -75,7 +75,7 @@ await ctx.Server.SampleAsync([ new ChatMessage(ChatRole.System, "You are a helpful test server"), new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), ], - options: new ChatOptions + chatOptions: new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs index 48c5184b..8a23ca49 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -15,7 +15,7 @@ public static async Task SampleLLM( CancellationToken cancellationToken) { var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); - var sampleResult = await server.SampleAsync(samplingParams, cancellationToken); + var sampleResult = await server.SampleAsync(samplingParams, cancellationToken: cancellationToken); return $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}"; } diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 7d4c6178..cd90220c 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -18,7 +18,7 @@ public static async Task SampleLLM( CancellationToken cancellationToken) { var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); - var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken); + var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken: cancellationToken); return $"LLM sampling result: {sampleResult.Content.OfType().FirstOrDefault()?.Text}"; } diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index f6545d92..0fa75935 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -4,6 +4,7 @@ using ModelContextProtocol.Server; using System.Runtime.CompilerServices; using System.Text.Json; +using System.Text.Json.Nodes; namespace ModelContextProtocol.Client; @@ -82,31 +83,32 @@ public static async Task ResumeSessionAsync( /// /// Sends a ping request to verify server connectivity. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A task that completes when the ping is successful. /// The server cannot be reached or returned an error response. - public Task PingAsync(CancellationToken cancellationToken = default) + /// Thrown when the server cannot be reached or returns an error response. + public ValueTask PingAsync(RequestOptions? options = null, CancellationToken cancellationToken = default) { - var opts = McpJsonUtilities.DefaultOptions; - opts.MakeReadOnly(); - return SendRequestAsync( + return SendRequestAsync( RequestMethods.Ping, - parameters: null, - serializerOptions: opts, - cancellationToken: cancellationToken).AsTask(); + new PingRequestParams { Meta = options?.Meta }, + McpJsonUtilities.JsonContext.Default.PingRequestParams, + McpJsonUtilities.JsonContext.Default.PingResult, + cancellationToken: cancellationToken); } /// /// Retrieves a list of available tools from the server. /// - /// The serializer options governing tool parameter serialization. If null, the default options are used. + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A list of all available tools as instances. public async ValueTask> ListToolsAsync( - JsonSerializerOptions? serializerOptions = null, + RequestOptions? options = null, CancellationToken cancellationToken = default) { - serializerOptions ??= McpJsonUtilities.DefaultOptions; + var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); List? tools = null; @@ -115,7 +117,7 @@ public async ValueTask> ListToolsAsync( { var toolResults = await SendRequestAsync( RequestMethods.ToolsList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, McpJsonUtilities.JsonContext.Default.ListToolsResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -136,14 +138,14 @@ public async ValueTask> ListToolsAsync( /// /// Creates an enumerable for asynchronously enumerating all available tools from the server. /// - /// The serializer options governing tool parameter serialization. If null, the default options are used. + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// An asynchronous sequence of all available tools as instances. public async IAsyncEnumerable EnumerateToolsAsync( - JsonSerializerOptions? serializerOptions = null, + RequestOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - serializerOptions ??= McpJsonUtilities.DefaultOptions; + var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); string? cursor = null; @@ -151,7 +153,7 @@ public async IAsyncEnumerable EnumerateToolsAsync( { var toolResults = await SendRequestAsync( RequestMethods.ToolsList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, McpJsonUtilities.JsonContext.Default.ListToolsResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -169,9 +171,11 @@ public async IAsyncEnumerable EnumerateToolsAsync( /// /// Retrieves a list of available prompts from the server. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A list of all available prompts as instances. public async ValueTask> ListPromptsAsync( + RequestOptions? options = null, CancellationToken cancellationToken = default) { List? prompts = null; @@ -180,7 +184,7 @@ public async ValueTask> ListPromptsAsync( { var promptResults = await SendRequestAsync( RequestMethods.PromptsList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, McpJsonUtilities.JsonContext.Default.ListPromptsResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -201,9 +205,11 @@ public async ValueTask> ListPromptsAsync( /// /// Creates an enumerable for asynchronously enumerating all available prompts from the server. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// An asynchronous sequence of all available prompts as instances. public async IAsyncEnumerable EnumeratePromptsAsync( + RequestOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { string? cursor = null; @@ -211,7 +217,7 @@ public async IAsyncEnumerable EnumeratePromptsAsync( { var promptResults = await SendRequestAsync( RequestMethods.PromptsList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, McpJsonUtilities.JsonContext.Default.ListPromptsResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -231,23 +237,23 @@ public async IAsyncEnumerable EnumeratePromptsAsync( /// /// The name of the prompt to retrieve. /// Optional arguments for the prompt. The dictionary keys are parameter names, and the values are the argument values. - /// The serialization options governing argument serialization. + /// 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. public ValueTask GetPromptAsync( string name, IReadOnlyDictionary? arguments = null, - JsonSerializerOptions? serializerOptions = null, + RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(name); - serializerOptions ??= McpJsonUtilities.DefaultOptions; + var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); return SendRequestAsync( RequestMethods.PromptsGet, - new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) }, + new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions), Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.GetPromptRequestParams, McpJsonUtilities.JsonContext.Default.GetPromptResult, cancellationToken: cancellationToken); @@ -256,9 +262,11 @@ public ValueTask GetPromptAsync( /// /// Retrieves a list of available resource templates from the server. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A list of all available resource templates as instances. public async ValueTask> ListResourceTemplatesAsync( + RequestOptions? options = null, CancellationToken cancellationToken = default) { List? resourceTemplates = null; @@ -268,7 +276,7 @@ public async ValueTask> ListResourceTemplatesAs { var templateResults = await SendRequestAsync( RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -289,9 +297,11 @@ public async ValueTask> ListResourceTemplatesAs /// /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// An asynchronous sequence of all available resource templates as instances. public async IAsyncEnumerable EnumerateResourceTemplatesAsync( + RequestOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { string? cursor = null; @@ -299,7 +309,7 @@ public async IAsyncEnumerable EnumerateResourceTempla { var templateResults = await SendRequestAsync( RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -317,9 +327,11 @@ public async IAsyncEnumerable EnumerateResourceTempla /// /// Retrieves a list of available resources from the server. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// A list of all available resources as instances. public async ValueTask> ListResourcesAsync( + RequestOptions? options = null, CancellationToken cancellationToken = default) { List? resources = null; @@ -329,7 +341,7 @@ public async ValueTask> ListResourcesAsync( { var resourceResults = await SendRequestAsync( RequestMethods.ResourcesList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, McpJsonUtilities.JsonContext.Default.ListResourcesResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -350,9 +362,11 @@ public async ValueTask> ListResourcesAsync( /// /// Creates an enumerable for asynchronously enumerating all available resources from the server. /// + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . /// An asynchronous sequence of all available resources as instances. public async IAsyncEnumerable EnumerateResourcesAsync( + RequestOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { string? cursor = null; @@ -360,7 +374,7 @@ public async IAsyncEnumerable EnumerateResourcesAsync( { var resourceResults = await SendRequestAsync( RequestMethods.ResourcesList, - new() { Cursor = cursor }, + new() { Cursor = cursor, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, McpJsonUtilities.JsonContext.Default.ListResourcesResult, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -379,15 +393,16 @@ public async IAsyncEnumerable EnumerateResourcesAsync( /// 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 . public ValueTask ReadResourceAsync( - string uri, CancellationToken cancellationToken = default) + string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uri); return SendRequestAsync( RequestMethods.ResourcesRead, - new() { Uri = uri }, + new() { Uri = uri, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, McpJsonUtilities.JsonContext.Default.ReadResourceResult, cancellationToken: cancellationToken); @@ -397,13 +412,14 @@ public ValueTask ReadResourceAsync( /// 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 . public ValueTask ReadResourceAsync( - Uri uri, CancellationToken cancellationToken = default) + Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNull(uri); - return ReadResourceAsync(uri.ToString(), cancellationToken); + return ReadResourceAsync(uri.ToString(), options, cancellationToken); } /// @@ -411,16 +427,17 @@ public ValueTask ReadResourceAsync( /// /// The URI template of the resource. /// Arguments to use to format . + /// Optional request options including metadata, serialization settings, and progress tracking. /// The to monitor for cancellation requests. The default is . public ValueTask ReadResourceAsync( - string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) + string uriTemplate, IReadOnlyDictionary arguments, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uriTemplate); Throw.IfNull(arguments); return SendRequestAsync( RequestMethods.ResourcesRead, - new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) }, + new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments), Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, McpJsonUtilities.JsonContext.Default.ReadResourceResult, cancellationToken: cancellationToken); @@ -455,15 +472,16 @@ public ValueTask CompleteAsync(Reference reference, string argum /// Subscribes to a resource on the server to receive notifications when it 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. - public Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default) + public Task SubscribeToResourceAsync(string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uri); return SendRequestAsync( RequestMethods.ResourcesSubscribe, - new() { Uri = uri }, + new() { Uri = uri, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.SubscribeRequestParams, McpJsonUtilities.JsonContext.Default.EmptyResult, cancellationToken: cancellationToken).AsTask(); @@ -473,28 +491,30 @@ public Task SubscribeToResourceAsync(string uri, CancellationToken cancellationT /// Subscribes to a resource on the server to receive notifications when it 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. - public Task SubscribeToResourceAsync(Uri uri, CancellationToken cancellationToken = default) + public Task SubscribeToResourceAsync(Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNull(uri); - return SubscribeToResourceAsync(uri.ToString(), cancellationToken); + return SubscribeToResourceAsync(uri.ToString(), options, 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 to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. - public Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default) + public Task UnsubscribeFromResourceAsync(string uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNullOrWhiteSpace(uri); return SendRequestAsync( RequestMethods.ResourcesUnsubscribe, - new() { Uri = uri }, + new() { Uri = uri, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, McpJsonUtilities.JsonContext.Default.EmptyResult, cancellationToken: cancellationToken).AsTask(); @@ -504,13 +524,14 @@ public Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellat /// 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 to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. - public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellationToken = default) + public Task UnsubscribeFromResourceAsync(Uri uri, RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNull(uri); - return UnsubscribeFromResourceAsync(uri.ToString(), cancellationToken); + return UnsubscribeFromResourceAsync(uri.ToString(), options, cancellationToken); } /// @@ -519,23 +540,23 @@ public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellation /// The name of the tool to call on the server. /// An optional dictionary of arguments to pass to the tool. /// An optional progress reporter for server notifications. - /// The JSON serializer options. + /// Optional request options including metadata, serialization settings, and progress tracking. /// A cancellation token. /// The from the tool execution. public ValueTask CallToolAsync( string toolName, IReadOnlyDictionary? arguments = null, IProgress? progress = null, - JsonSerializerOptions? serializerOptions = null, + RequestOptions? options = null, CancellationToken cancellationToken = default) { Throw.IfNull(toolName); - serializerOptions ??= McpJsonUtilities.DefaultOptions; + var serializerOptions = options?.JsonSerializerOptions ?? McpJsonUtilities.DefaultOptions; serializerOptions.MakeReadOnly(); if (progress is not null) { - return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, cancellationToken); + return SendRequestWithProgressAsync(toolName, arguments, progress, options?.Meta, serializerOptions, cancellationToken); } return SendRequestAsync( @@ -544,6 +565,7 @@ public ValueTask CallToolAsync( { Name = toolName, Arguments = ToArgumentsDictionary(arguments, serializerOptions), + Meta = options?.Meta, }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, McpJsonUtilities.JsonContext.Default.CallToolResult, @@ -553,6 +575,7 @@ async ValueTask SendRequestWithProgressAsync( string toolName, IReadOnlyDictionary? arguments, IProgress progress, + JsonObject? meta, JsonSerializerOptions serializerOptions, CancellationToken cancellationToken) { @@ -570,13 +593,16 @@ async ValueTask SendRequestWithProgressAsync( return default; }).ConfigureAwait(false); + var metaWithProgress = meta is not null ? new JsonObject(meta) : new JsonObject(); + metaWithProgress["progressToken"] = JsonValue.Create(progressToken.Token as string); + return await SendRequestAsync( RequestMethods.ToolsCall, new() { Name = toolName, Arguments = ToArgumentsDictionary(arguments, serializerOptions), - ProgressToken = progressToken, + Meta = metaWithProgress, }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, McpJsonUtilities.JsonContext.Default.CallToolResult, @@ -588,13 +614,14 @@ async ValueTask SendRequestWithProgressAsync( /// Sets the logging level for the server to control which log messages are sent to the client. /// /// The minimum severity level of log messages to receive from the server. + /// 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, CancellationToken cancellationToken = default) + public Task SetLoggingLevel(LoggingLevel level, RequestOptions? options = null, CancellationToken cancellationToken = default) { return SendRequestAsync( RequestMethods.LoggingSetLevel, - new() { Level = level }, + new() { Level = level, Meta = options?.Meta }, McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, McpJsonUtilities.JsonContext.Default.EmptyResult, cancellationToken: cancellationToken).AsTask(); @@ -604,10 +631,11 @@ public Task SetLoggingLevel(LoggingLevel level, CancellationToken cancellationTo /// Sets the logging level for the server to control which log messages are sent to the client. /// /// The minimum severity level of log messages to receive from the server. + /// 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, CancellationToken cancellationToken = default) => - SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), cancellationToken); + public Task SetLoggingLevel(LogLevel level, RequestOptions? options = null, CancellationToken cancellationToken = default) => + SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), options, cancellationToken); /// Converts a dictionary with values to a dictionary with values. private static Dictionary? ToArgumentsDictionary( diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 4f46b749..b90d153f 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -39,7 +39,7 @@ public static class McpClientExtensions [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 [EditorBrowsable(EditorBrowsableState.Never)] public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).PingAsync(cancellationToken); + => AsClientOrThrow(client).PingAsync(null, cancellationToken).AsTask(); /// /// Retrieves a list of available tools from the server. @@ -86,7 +86,7 @@ public static ValueTask> ListToolsAsync( this IMcpClient client, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListToolsAsync(serializerOptions, cancellationToken); + => AsClientOrThrow(client).ListToolsAsync(new RequestOptions { JsonSerializerOptions = serializerOptions }, cancellationToken); /// /// Creates an enumerable for asynchronously enumerating all available tools from the server. @@ -126,7 +126,7 @@ public static IAsyncEnumerable EnumerateToolsAsync( this IMcpClient client, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumerateToolsAsync(serializerOptions, cancellationToken); + => AsClientOrThrow(client).EnumerateToolsAsync(new RequestOptions { JsonSerializerOptions = serializerOptions }, cancellationToken); /// /// Retrieves a list of available prompts from the server. @@ -149,7 +149,7 @@ public static IAsyncEnumerable EnumerateToolsAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask> ListPromptsAsync( this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListPromptsAsync(cancellationToken); + => AsClientOrThrow(client).ListPromptsAsync(null, cancellationToken); /// /// Creates an enumerable for asynchronously enumerating all available prompts from the server. @@ -182,7 +182,7 @@ public static ValueTask> ListPromptsAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncEnumerable EnumeratePromptsAsync( this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumeratePromptsAsync(cancellationToken); + => AsClientOrThrow(client).EnumeratePromptsAsync(null, cancellationToken); /// /// Retrieves a specific prompt from the MCP server. @@ -217,7 +217,7 @@ public static ValueTask GetPromptAsync( IReadOnlyDictionary? arguments = null, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).GetPromptAsync(name, arguments, serializerOptions, cancellationToken); + => AsClientOrThrow(client).GetPromptAsync(name, arguments, new RequestOptions { JsonSerializerOptions = serializerOptions }, cancellationToken); /// /// Retrieves a list of available resource templates from the server. @@ -240,7 +240,7 @@ public static ValueTask GetPromptAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask> ListResourceTemplatesAsync( this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListResourceTemplatesAsync(cancellationToken); + => AsClientOrThrow(client).ListResourceTemplatesAsync(null, cancellationToken); /// /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. @@ -273,7 +273,7 @@ public static ValueTask> ListResourceTemplatesA [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncEnumerable EnumerateResourceTemplatesAsync( this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumerateResourceTemplatesAsync(cancellationToken); + => AsClientOrThrow(client).EnumerateResourceTemplatesAsync(null, cancellationToken); /// /// Retrieves a list of available resources from the server. @@ -308,7 +308,7 @@ public static IAsyncEnumerable EnumerateResourceTempl [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask> ListResourcesAsync( this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ListResourcesAsync(cancellationToken); + => AsClientOrThrow(client).ListResourcesAsync(null, cancellationToken); /// /// Creates an enumerable for asynchronously enumerating all available resources from the server. @@ -341,7 +341,7 @@ public static ValueTask> ListResourcesAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncEnumerable EnumerateResourcesAsync( this IMcpClient client, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).EnumerateResourcesAsync(cancellationToken); + => AsClientOrThrow(client).EnumerateResourcesAsync(null, cancellationToken); /// /// Reads a resource from the server. @@ -356,7 +356,7 @@ public static IAsyncEnumerable EnumerateResourcesAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask ReadResourceAsync( this IMcpClient client, string uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); + => AsClientOrThrow(client).ReadResourceAsync(uri, null, cancellationToken); /// /// Reads a resource from the server. @@ -370,7 +370,7 @@ public static ValueTask ReadResourceAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask ReadResourceAsync( this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); + => AsClientOrThrow(client).ReadResourceAsync(uri, null, cancellationToken); /// /// Reads a resource from the server. @@ -386,7 +386,7 @@ public static ValueTask ReadResourceAsync( [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask ReadResourceAsync( this IMcpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, cancellationToken); + => AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, null, cancellationToken); /// /// Requests completion suggestions for a prompt argument or resource reference. @@ -450,7 +450,7 @@ public static ValueTask CompleteAsync(this IMcpClient client, Re [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 [EditorBrowsable(EditorBrowsableState.Never)] public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); + => AsClientOrThrow(client).SubscribeToResourceAsync(uri, null, cancellationToken); /// /// Subscribes to a resource on the server to receive notifications when it changes. @@ -479,7 +479,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 [EditorBrowsable(EditorBrowsableState.Never)] public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); + => AsClientOrThrow(client).SubscribeToResourceAsync(uri, null, cancellationToken); /// /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. @@ -508,7 +508,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, Can [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 [EditorBrowsable(EditorBrowsableState.Never)] public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); + => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, null, cancellationToken); /// /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. @@ -536,7 +536,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string u [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 [EditorBrowsable(EditorBrowsableState.Never)] public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); + => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, null, cancellationToken); /// /// Invokes a tool on the server. @@ -582,7 +582,7 @@ public static ValueTask CallToolAsync( IProgress? progress = null, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) - => AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, serializerOptions, cancellationToken); + => AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, new RequestOptions { JsonSerializerOptions = serializerOptions }, cancellationToken); [MethodImpl(MethodImplOptions.AggressiveInlining)] #pragma warning disable CS0618 // Type or member is obsolete diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs index 354cd314..ccd335d5 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs @@ -97,6 +97,6 @@ public async ValueTask GetAsync( arguments as IReadOnlyDictionary ?? arguments?.ToDictionary(); - return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, serializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, new RequestOptions(){JsonSerializerOptions = serializerOptions}, cancellationToken).ConfigureAwait(false); } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs index fe3fb1c1..e8695864 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs @@ -79,10 +79,10 @@ public McpClientResource(McpClient client, Resource resource) /// A containing the resource's result with content and messages. /// /// - /// This is a convenience method that internally calls . + /// This is a convenience method that internally calls . /// /// public ValueTask ReadAsync( CancellationToken cancellationToken = default) => - _client.ReadResourceAsync(Uri, cancellationToken); + _client.ReadResourceAsync(Uri, cancellationToken: cancellationToken); } diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs index ddbdfe15..93202758 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs @@ -84,5 +84,5 @@ public McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemp public ValueTask ReadAsync( IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) => - _client.ReadResourceAsync(UriTemplate, arguments, cancellationToken); + _client.ReadResourceAsync(UriTemplate, arguments, cancellationToken: cancellationToken); } diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index d0760821..b6244e6c 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -193,7 +193,15 @@ public ValueTask CallAsync( IProgress? progress = null, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) => - _client.CallToolAsync(ProtocolTool.Name, arguments, progress, serializerOptions, cancellationToken); + _client.CallToolAsync( + ProtocolTool.Name, + arguments, + progress, + serializerOptions is null ? null : new RequestOptions + { + JsonSerializerOptions = serializerOptions + }, + cancellationToken); /// /// Creates a new instance of the tool but modified to return the specified name from its property. diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs index 160b31c0..e148783e 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -126,7 +126,7 @@ public static Task NotifyProgressAsync( ProgressToken progressToken, ProgressNotificationValue progress, CancellationToken cancellationToken = default) - => AsSessionOrThrow(endpoint).NotifyProgressAsync(progressToken, progress, cancellationToken); + => AsSessionOrThrow(endpoint).NotifyProgressAsync(progressToken, progress, cancellationToken: cancellationToken); [MethodImpl(MethodImplOptions.AggressiveInlining)] #pragma warning disable CS0618 // Type or member is obsolete diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs index 1915c761..ab89bb23 100644 --- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs +++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs @@ -132,6 +132,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element) [JsonSerializable(typeof(ListRootsResult))] [JsonSerializable(typeof(ListToolsRequestParams))] [JsonSerializable(typeof(ListToolsResult))] + [JsonSerializable(typeof(PingRequestParams))] [JsonSerializable(typeof(PingResult))] [JsonSerializable(typeof(ReadResourceRequestParams))] [JsonSerializable(typeof(ReadResourceResult))] diff --git a/src/ModelContextProtocol.Core/McpSession.Methods.cs b/src/ModelContextProtocol.Core/McpSession.Methods.cs index be3128cb..87a643e7 100644 --- a/src/ModelContextProtocol.Core/McpSession.Methods.cs +++ b/src/ModelContextProtocol.Core/McpSession.Methods.cs @@ -152,6 +152,7 @@ internal Task SendNotificationAsync( /// /// The token that identifies the operation for which progress is being reported. /// The progress update to send, containing information such as percentage complete or status message. + /// 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 . @@ -168,6 +169,7 @@ internal Task SendNotificationAsync( public Task NotifyProgressAsync( ProgressToken progressToken, ProgressNotificationValue progress, + RequestOptions? options = null, CancellationToken cancellationToken = default) { return SendNotificationAsync( @@ -176,6 +178,7 @@ public Task NotifyProgressAsync( { ProgressToken = progressToken, Progress = progress, + Meta = options?.Meta, }, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, cancellationToken); diff --git a/src/ModelContextProtocol.Core/Protocol/PingRequestParams.cs b/src/ModelContextProtocol.Core/Protocol/PingRequestParams.cs new file mode 100644 index 00000000..e8557f55 --- /dev/null +++ b/src/ModelContextProtocol.Core/Protocol/PingRequestParams.cs @@ -0,0 +1,11 @@ +namespace ModelContextProtocol.Protocol; + +/// +/// Represents the parameters used with a request to verify +/// server connectivity. +/// +/// +/// The server responds with a . +/// See the schema for details. +/// +public sealed class PingRequestParams : RequestParams; \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Protocol/RequestParams.cs b/src/ModelContextProtocol.Core/Protocol/RequestParams.cs index b45f55f7..0a0586a7 100644 --- a/src/ModelContextProtocol.Core/Protocol/RequestParams.cs +++ b/src/ModelContextProtocol.Core/Protocol/RequestParams.cs @@ -26,7 +26,7 @@ private protected RequestParams() public JsonObject? Meta { get; set; } /// - /// Gets or sets an opaque token that will be attached to any subsequent progress notifications. + /// Gets the opaque token that will be attached to any subsequent progress notifications. /// [JsonIgnore] public ProgressToken? ProgressToken @@ -48,21 +48,5 @@ public ProgressToken? ProgressToken return null; } - set - { - if (value is null) - { - Meta?.Remove("progressToken"); - } - else - { - (Meta ??= [])["progressToken"] = value.Value.Token switch - { - string s => JsonValue.Create(s), - long l => JsonValue.Create(l), - _ => throw new InvalidOperationException("ProgressToken must be a string or a long.") - }; - } - } } } diff --git a/src/ModelContextProtocol.Core/README.md b/src/ModelContextProtocol.Core/README.md index 111c3e76..69913a15 100644 --- a/src/ModelContextProtocol.Core/README.md +++ b/src/ModelContextProtocol.Core/README.md @@ -50,7 +50,7 @@ foreach (var tool in await client.ListToolsAsync()) var result = await client.CallToolAsync( "echo", new Dictionary() { ["message"] = "Hello MCP!" }, - cancellationToken:CancellationToken.None); + cancellationToken: CancellationToken.None); // echo always returns one and only one text content object Console.WriteLine(result.Content.First(c => c.Type == "text").Text); @@ -83,13 +83,13 @@ using System.ComponentModel; var serverOptions = new McpServerOptions(); // Add tools directly -serverOptions.Capabilities.Tools = new() +serverOptions.Capabilities.Tools = new() { ListChanged = true, ToolCollection = [ McpServerTool.Create((string message) => $"hello {message}", new() { - Name = "echo", + Name = "echo", Description = "Echoes the message back to the client." }) ] diff --git a/src/ModelContextProtocol.Core/RequestOptions.cs b/src/ModelContextProtocol.Core/RequestOptions.cs new file mode 100644 index 00000000..b5f60241 --- /dev/null +++ b/src/ModelContextProtocol.Core/RequestOptions.cs @@ -0,0 +1,62 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol; + +/// +/// Contains optional parameters for MCP requests. +/// +public sealed class RequestOptions +{ + /// + /// Optional metadata to include in the request. + /// + private JsonObject? _meta; + + /// + /// Optional metadata to include in the request. + /// When getting, automatically includes the progress token if set. + /// + public JsonObject? Meta + { + get + { + if (ProgressToken == null) + { + return _meta; + } + + // Clone existing metadata or create a new one + var meta = _meta?.DeepClone() as JsonObject ?? new JsonObject(); + + // Add progress token to metadata + meta["progressToken"] = ProgressToken.Value.Token switch + { + string s => JsonValue.Create(s), + long l => JsonValue.Create(l), + _ => null + }; + + return meta; + } + set => _meta = value; + } + + /// + /// The serializer options governing tool parameter serialization. If null, the default options are used. + /// + public JsonSerializerOptions? JsonSerializerOptions { get; set; } + + /// + /// The progress token for tracking long-running operations. + /// + public ProgressToken? ProgressToken { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public RequestOptions() + { + } +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs index 604f2158..957b262c 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -70,19 +70,19 @@ public ValueTask SampleAsync( /// Requests to sample an LLM via the client using the provided chat messages and options. /// /// The messages to send as part of the request. - /// The options to use for the request, including model parameters and constraints. + /// The options to use for the request, including model parameters and constraints. /// The to monitor for cancellation requests. The default is . /// A task containing the chat response from the model. /// is . /// The client does not support sampling. public async Task SampleAsync( - IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) + IEnumerable messages, ChatOptions? chatOptions = default, CancellationToken cancellationToken = default) { Throw.IfNull(messages); StringBuilder? systemPrompt = null; - if (options?.Instructions is { } instructions) + if (chatOptions?.Instructions is { } instructions) { (systemPrompt ??= new()).Append(instructions); } @@ -128,15 +128,15 @@ public async Task SampleAsync( } ModelPreferences? modelPreferences = null; - if (options?.ModelId is { } modelId) + if (chatOptions?.ModelId is { } modelId) { modelPreferences = new() { Hints = [new() { Name = modelId }] }; } IList? tools = null; - if (options?.Tools is { Count: > 0 }) + if (chatOptions?.Tools is { Count: > 0 }) { - foreach (var tool in options.Tools) + foreach (var tool in chatOptions.Tools) { if (tool is AIFunctionDeclaration af) { @@ -151,7 +151,7 @@ public async Task SampleAsync( } } - ToolChoice? toolChoice = options?.ToolMode switch + ToolChoice? toolChoice = chatOptions?.ToolMode switch { NoneChatToolMode => new() { Mode = ToolChoice.ModeNone }, AutoChatToolMode => new() { Mode = ToolChoice.ModeAuto }, @@ -161,15 +161,15 @@ public async Task SampleAsync( var result = await SampleAsync(new() { - MaxTokens = options?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, + MaxTokens = chatOptions?.MaxOutputTokens ?? ServerOptions.MaxSamplingOutputTokens, Messages = samplingMessages, ModelPreferences = modelPreferences, - StopSequences = options?.StopSequences?.ToArray(), + StopSequences = chatOptions?.StopSequences?.ToArray(), SystemPrompt = systemPrompt?.ToString(), - Temperature = options?.Temperature, + Temperature = chatOptions?.Temperature, ToolChoice = toolChoice, Tools = tools, - Meta = options?.AdditionalProperties?.ToJsonObject(), + Meta = chatOptions?.AdditionalProperties?.ToJsonObject(), }, cancellationToken).ConfigureAwait(false); List responseContents = []; @@ -474,14 +474,14 @@ private sealed class SamplingChatClient(McpServer server) : IChatClient private readonly McpServer _server = server; /// - public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => - _server.SampleAsync(messages, options, cancellationToken); + public Task GetResponseAsync(IEnumerable messages, ChatOptions? chatOptions = null, CancellationToken cancellationToken = default) => + _server.SampleAsync(messages, chatOptions, cancellationToken); /// async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) + IEnumerable messages, ChatOptions? chatOptions, [EnumeratorCancellation] CancellationToken cancellationToken) { - var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + var response = await GetResponseAsync(messages, chatOptions, cancellationToken).ConfigureAwait(false); foreach (var update in response.ToChatResponseUpdates()) { yield return update; diff --git a/src/ModelContextProtocol.Core/TokenProgress.cs b/src/ModelContextProtocol.Core/TokenProgress.cs index 6b7a91e0..d41822c2 100644 --- a/src/ModelContextProtocol.Core/TokenProgress.cs +++ b/src/ModelContextProtocol.Core/TokenProgress.cs @@ -11,6 +11,6 @@ internal sealed class TokenProgress(McpSession session, ProgressToken progressTo /// public void Report(ProgressNotificationValue value) { - _ = session.NotifyProgressAsync(progressToken, value, CancellationToken.None); + _ = session.NotifyProgressAsync(progressToken, value, cancellationToken: CancellationToken.None); } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index acf5d469..c2ac09c7 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(TestContext.Current.CancellationToken); + await client.PingAsync(null, 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(TestContext.Current.CancellationToken); + IList allResources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); // The everything server provides 100 test resources Assert.Equal(100, allResources.Count); @@ -155,7 +155,7 @@ public async Task ReadResource_Sse_TextResource() // Odd numbered resources are text in the everything server (despite the docs saying otherwise) // 1 is index 0, which is "even" in the 0-based index // We copied this oddity to the test server - var result = await client.ReadResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Single(result.Contents); @@ -174,7 +174,7 @@ public async Task ReadResource_Sse_BinaryResource() // Even numbered resources are binary in the everything server (despite the docs saying otherwise) // 2 is index 1, which is "odd" in the 0-based index // We copied this oddity to the test server - var result = await client.ReadResourceAsync("test://static/resource/2", TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync("test://static/resource/2", null, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Single(result.Contents); @@ -190,7 +190,7 @@ public async Task ListPrompts_Sse_TestServer() // act await using var client = await GetClientAsync(); - var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); // assert Assert.NotNull(prompts); diff --git a/tests/ModelContextProtocol.ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs index 41993751..e9c810fb 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -47,7 +47,7 @@ await ctx.Server.SampleAsync([ new ChatMessage(ChatRole.System, "You are a helpful test server"), new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), ], - options: new ChatOptions + chatOptions: new ChatOptions { MaxOutputTokens = 100, Temperature = 0.7f, diff --git a/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs b/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs index f624e5ca..177de5c6 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs @@ -141,7 +141,7 @@ public static async Task ToolWithProgress( { Progress = 0, Total = 100, - }, cancellationToken); + }, cancellationToken: cancellationToken); await Task.Delay(50, cancellationToken); @@ -149,7 +149,7 @@ public static async Task ToolWithProgress( { Progress = 50, Total = 100, - }, cancellationToken); + }, cancellationToken: cancellationToken); await Task.Delay(50, cancellationToken); @@ -157,7 +157,7 @@ public static async Task ToolWithProgress( { Progress = 100, Total = 100, - }, cancellationToken); + }, cancellationToken: cancellationToken); } return progressToken?.ToString() ?? "No progress token provided"; diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index fe5c725a..66fef57b 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -193,7 +193,7 @@ private static void ConfigureTools(McpServerOptions options, string? cliArg) throw new McpProtocolException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); } var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())), - cancellationToken); + cancellationToken: cancellationToken); return new CallToolResult { diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index 2b6c9b85..0eb799ed 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -187,7 +187,7 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st throw new McpProtocolException("Missing required arguments 'prompt' and 'maxTokens'", McpErrorCode.InvalidParams); } var sampleResult = await request.Server.SampleAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), - cancellationToken); + cancellationToken: cancellationToken); return new CallToolResult { diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index 5a281600..e94468d5 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -342,7 +342,7 @@ public async Task ReadResourceAsync_String_Forwards_To_McpClient_SendRequestAsyn IMcpClient client = mockClient.Object; - var result = await client.ReadResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync("mcp://resource/1", cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(result); mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs new file mode 100644 index 00000000..df18b6e3 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Client/McpClientMetaTests.cs @@ -0,0 +1,141 @@ +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Text.Json.Nodes; + +namespace ModelContextProtocol.Tests.Client; + +public class McpClientMetaTests : ClientServerTestBase +{ + public McpClientMetaTests(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) + { + services.Configure(o => + { + o.ServerInfo = new Implementation + { + Name = "test-server", + Version = "1.0.0", + Description = "A test server for unit testing", + WebsiteUrl = "https://example.com", + }; + o.ToolCollection = new (); + o.ResourceCollection = new (); + o.PromptCollection = new (); + }); + } + + [Fact] + public async Task ToolCallWithMetaFields() + { + Server.ServerOptions.ToolCollection?.Add(McpServerTool.Create( + async (RequestContext context) => + { + // Access the foo property of _meta field from the request parameters + var metaFoo = context.Params?.Meta?["foo"]?.ToString(); + + // Assert that the meta foo is correctly passed + Assert.NotNull(metaFoo); + + return $"Meta foo is {metaFoo}"; + }, + new () { Name = "meta_tool" })); + + await using McpClient client = await CreateMcpClientForServer(); + + var requestOptions = new RequestOptions() + { + Meta = new JsonObject() + { + { "foo", "bar baz" } + } + }; + + var result = await client.CallToolAsync("meta_tool", options: requestOptions, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.Null(result.IsError); + + var textContent = result.Content.OfType().FirstOrDefault(); + Assert.NotNull(textContent); + Assert.Contains("bar baz", textContent.Text); + } + + [Fact] + public async Task ResourceReadWithMetaFields() + { + Server.ServerOptions.ResourceCollection?.Add(McpServerResource.Create( + (RequestContext context) => + { + // Access the foo property of _meta field from the request parameters + var metaFoo = context.Params?.Meta?["foo"]?.ToString(); + + // Assert that the meta foo is correctly passed + Assert.NotNull(metaFoo); + + return $"Resource with Meta foo is {metaFoo}"; + }, + new () { UriTemplate = "test://meta_resource" })); + + await using McpClient client = await CreateMcpClientForServer(); + + var requestOptions = new RequestOptions() + { + Meta = new JsonObject() + { + { "foo", "bar baz" } + } + }; + + var result = await client.ReadResourceAsync("test://meta_resource", options: requestOptions, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(result); + + var textContent = result.Contents.OfType().FirstOrDefault(); + Assert.NotNull(textContent); + Assert.Contains("bar baz", textContent.Text); + } + + + [Fact] + public async Task PromptGettWithMetaFields() + { + Server.ServerOptions.PromptCollection?.Add(McpServerPrompt.Create( + (RequestContext context) => + { + // Access the foo property of _meta field from the request parameters + var metaFoo = context.Params?.Meta?["foo"]?.ToString(); + + // Assert that the meta foo is correctly passed + Assert.NotNull(metaFoo); + + return $"Prompt with Meta foo is {metaFoo}"; + }, + new () { Name = "meta_prompt" })); + + await using McpClient client = await CreateMcpClientForServer(); + + var requestOptions = new RequestOptions() + { + Meta = new JsonObject() + { + { "foo", "bar baz" } + } + }; + + var result = await client.GetPromptAsync("meta_prompt", options: requestOptions, cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(result); + Assert.NotEmpty(result.Messages); + var message = result.Messages.First(); + Assert.NotNull(message.Content); + var textContent = message.Content as TextContentBlock; + Assert.NotNull(textContent); + Assert.Contains("bar baz", textContent.Text); + } +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs index 5014022c..693dab28 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs @@ -75,7 +75,7 @@ public async Task UriTemplate_InputsProduceExpectedOutputs( { await using McpClient client = await CreateMcpClientForServer(); - var result = await client.ReadResourceAsync(uriTemplate, variables, TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync(uriTemplate, variables, null, TestContext.Current.CancellationToken); Assert.NotNull(result); var actualUri = Assert.IsType(Assert.Single(result.Contents)).Text; @@ -110,13 +110,13 @@ internal partial class JsonContext7 : JsonSerializerContext; // The JSON from the test case files has been extracted below. // Copyright 2011- The Authors - // + // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at - // + // // http://www.apache.org/licenses/LICENSE-2.0 - // + // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -255,7 +255,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["X{.var:3}", "X.val"], ["X{.list}", "X.red,green,blue"], ["X{.list*}", "X.red.green.blue"], - ["X{.keys}", [ + ["X{.keys}", [ "X.comma,%2C,dot,.,semi,%3B", "X.comma,%2C,semi,%3B,dot,.", "X.dot,.,comma,%2C,semi,%3B", @@ -275,7 +275,7 @@ internal partial class JsonContext7 : JsonSerializerContext; "/semi,%3B,comma,%2C,dot,.", "/semi,%3B,dot,.,comma,%2C" ]], - ["{/keys*}", [ + ["{/keys*}", [ "/comma=%2C/dot=./semi=%3B", "/comma=%2C/semi=%3B/dot=.", "/dot=./comma=%2C/semi=%3B", @@ -286,7 +286,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["{;hello:5}", ";hello=Hello"], ["{;list}", ";list=red,green,blue"], ["{;list*}", ";list=red;list=green;list=blue"], - ["{;keys}", [ + ["{;keys}", [ ";keys=comma,%2C,dot,.,semi,%3B", ";keys=comma,%2C,semi,%3B,dot,.", ";keys=dot,.,comma,%2C,semi,%3B", @@ -294,7 +294,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ";keys=semi,%3B,comma,%2C,dot,.", ";keys=semi,%3B,dot,.,comma,%2C" ]], - ["{;keys*}", [ + ["{;keys*}", [ ";comma=%2C;dot=.;semi=%3B", ";comma=%2C;semi=%3B;dot=.", ";dot=.;comma=%2C;semi=%3B", @@ -305,7 +305,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["{?var:3}", "?var=val"], ["{?list}", "?list=red,green,blue"], ["{?list*}", "?list=red&list=green&list=blue"], - ["{?keys}", [ + ["{?keys}", [ "?keys=comma,%2C,dot,.,semi,%3B", "?keys=comma,%2C,semi,%3B,dot,.", "?keys=dot,.,comma,%2C,semi,%3B", @@ -313,7 +313,7 @@ internal partial class JsonContext7 : JsonSerializerContext; "?keys=semi,%3B,comma,%2C,dot,.", "?keys=semi,%3B,dot,.,comma,%2C" ]], - ["{?keys*}", [ + ["{?keys*}", [ "?comma=%2C&dot=.&semi=%3B", "?comma=%2C&semi=%3B&dot=.", "?dot=.&comma=%2C&semi=%3B", @@ -324,7 +324,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["{&var:3}", "&var=val"], ["{&list}", "&list=red,green,blue"], ["{&list*}", "&list=red&list=green&list=blue"], - ["{&keys}", [ + ["{&keys}", [ "&keys=comma,%2C,dot,.,semi,%3B", "&keys=comma,%2C,semi,%3B,dot,.", "&keys=dot,.,comma,%2C,semi,%3B", @@ -332,7 +332,7 @@ internal partial class JsonContext7 : JsonSerializerContext; "&keys=semi,%3B,comma,%2C,dot,.", "&keys=semi,%3B,dot,.,comma,%2C" ]], - ["{&keys*}", [ + ["{&keys*}", [ "&comma=%2C&dot=.&semi=%3B", "&comma=%2C&semi=%3B&dot=.", "&dot=.&comma=%2C&semi=%3B", @@ -636,7 +636,7 @@ internal partial class JsonContext7 : JsonSerializerContext; "/semi,%3B,comma,%2C,dot,.", "/semi,%3B,dot,.,comma,%2C" ]], - ["{/keys*}", [ + ["{/keys*}", [ "/comma=%2C/dot=./semi=%3B", "/comma=%2C/semi=%3B/dot=.", "/dot=./comma=%2C/semi=%3B", @@ -679,7 +679,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["{;x,y,undef}", ";x=1024;y=768"], ["{;list}", ";list=red,green,blue"], ["{;list*}", ";list=red;list=green;list=blue"], - ["{;keys}", [ + ["{;keys}", [ ";keys=comma,%2C,dot,.,semi,%3B", ";keys=comma,%2C,semi,%3B,dot,.", ";keys=dot,.,comma,%2C,semi,%3B", @@ -687,7 +687,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ";keys=semi,%3B,comma,%2C,dot,.", ";keys=semi,%3B,dot,.,comma,%2C" ]], - ["{;keys*}", [ + ["{;keys*}", [ ";comma=%2C;dot=.;semi=%3B", ";comma=%2C;semi=%3B;dot=.", ";dot=.;comma=%2C;semi=%3B", @@ -727,7 +727,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["{?var:3}", "?var=val"], ["{?list}", "?list=red,green,blue"], ["{?list*}", "?list=red&list=green&list=blue"], - ["{?keys}", [ + ["{?keys}", [ "?keys=comma,%2C,dot,.,semi,%3B", "?keys=comma,%2C,semi,%3B,dot,.", "?keys=dot,.,comma,%2C,semi,%3B", @@ -735,7 +735,7 @@ internal partial class JsonContext7 : JsonSerializerContext; "?keys=semi,%3B,comma,%2C,dot,.", "?keys=semi,%3B,dot,.,comma,%2C" ]], - ["{?keys*}", [ + ["{?keys*}", [ "?comma=%2C&dot=.&semi=%3B", "?comma=%2C&semi=%3B&dot=.", "?dot=.&comma=%2C&semi=%3B", @@ -775,7 +775,7 @@ internal partial class JsonContext7 : JsonSerializerContext; ["{&x,y,undef}", "&x=1024&y=768"], ["{&list}", "&list=red,green,blue"], ["{&list*}", "&list=red&list=green&list=blue"], - ["{&keys}", [ + ["{&keys}", [ "&keys=comma,%2C,dot,.,semi,%3B", "&keys=comma,%2C,semi,%3B,dot,.", "&keys=dot,.,comma,%2C,semi,%3B", @@ -783,7 +783,7 @@ internal partial class JsonContext7 : JsonSerializerContext; "&keys=semi,%3B,comma,%2C,dot,.", "&keys=semi,%3B,dot,.,comma,%2C" ]], - ["{&keys*}", [ + ["{&keys*}", [ "&comma=%2C&dot=.&semi=%3B", "&comma=%2C&semi=%3B&dot=.", "&dot=.&comma=%2C&semi=%3B", diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index 604c31b8..308f0abe 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -311,13 +311,13 @@ public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() await using McpClient client = await CreateMcpClientForServer(); bool hasTools = false; - await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken)) + await foreach (var tool in client.EnumerateToolsAsync(new RequestOptions { JsonSerializerOptions = options }, TestContext.Current.CancellationToken)) { Assert.Same(options, tool.JsonSerializerOptions); hasTools = true; } - foreach (var tool in await client.ListToolsAsync(options, TestContext.Current.CancellationToken)) + foreach (var tool in await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = options }, TestContext.Current.CancellationToken)) { Assert.Same(options, tool.JsonSerializerOptions); } @@ -331,7 +331,7 @@ public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; await using McpClient client = await CreateMcpClientForServer(); - var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); + var tool = (await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = emptyOptions }, TestContext.Current.CancellationToken)).First(); await Assert.ThrowsAsync(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken)); } @@ -359,7 +359,7 @@ public async Task GetPromptsAsync_HonorsJsonSerializerOptions() JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; await using McpClient client = await CreateMcpClientForServer(); - await Assert.ThrowsAsync(async () => await client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, new RequestOptions { JsonSerializerOptions = emptyOptions }, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -368,7 +368,7 @@ public async Task WithName_ChangesToolName() JsonSerializerOptions options = new(JsonSerializerOptions.Default); await using McpClient client = await CreateMcpClientForServer(); - var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First(); + var tool = (await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = options }, TestContext.Current.CancellationToken)).First(); var originalName = tool.Name; var renamedTool = tool.WithName("RenamedTool"); @@ -382,7 +382,7 @@ public async Task WithDescription_ChangesToolDescription() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); await using McpClient client = await CreateMcpClientForServer(); - var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault(); + var tool = (await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = options }, TestContext.Current.CancellationToken)).FirstOrDefault(); var originalDescription = tool?.Description; var redescribedTool = tool?.WithDescription("ToolWithNewDescription"); Assert.NotNull(redescribedTool); @@ -456,7 +456,7 @@ public async Task AsClientLoggerProvider_MessagesSentToClient() Assert.False(logger.IsEnabled(LogLevel.Error)); Assert.False(logger.IsEnabled(LogLevel.Critical)); - await client.SetLoggingLevel(LoggingLevel.Info, TestContext.Current.CancellationToken); + await client.SetLoggingLevel(LoggingLevel.Info, options: null, TestContext.Current.CancellationToken); DateTime start = DateTime.UtcNow; while (Server.LoggingLevel is null) diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index ff6f56e2..0b29b178 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(TestContext.Current.CancellationToken); + await client.PingAsync(null, 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(TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(null, TestContext.Current.CancellationToken); // assert Assert.NotEmpty(prompts); @@ -193,7 +193,7 @@ public async Task GetPrompt_NonExistent_ThrowsException(string clientId) // act await using var client = await _fixture.CreateClientAsync(clientId); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken)); } @@ -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(TestContext.Current.CancellationToken); + IList allResourceTemplates = await client.ListResourceTemplatesAsync(null, 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(TestContext.Current.CancellationToken); + IList allResources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); // The server provides 100 test resources Assert.Equal(100, allResources.Count); @@ -237,7 +237,7 @@ public async Task ReadResource_Stdio_TextResource(string clientId) await using var client = await _fixture.CreateClientAsync(clientId); // Odd numbered resources are text in the everything server (despite the docs saying otherwise) // 1 is index 0, which is "even" in the 0-based index - var result = await client.ReadResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Single(result.Contents); @@ -256,7 +256,7 @@ public async Task ReadResource_Stdio_BinaryResource(string clientId) await using var client = await _fixture.CreateClientAsync(clientId); // Even numbered resources are binary in the everything server (despite the docs saying otherwise) // 2 is index 1, which is "odd" in the 0-based index - var result = await client.ReadResourceAsync("test://static/resource/2", TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync("test://static/resource/2", null, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Single(result.Contents); @@ -290,7 +290,7 @@ public async Task SubscribeResource_Stdio() } }); - await client.SubscribeToResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); + await client.SubscribeToResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken); await tcs.Task; } @@ -319,13 +319,13 @@ public async Task UnsubscribeResource_Stdio() ] } }); - await client.SubscribeToResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); + await client.SubscribeToResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken); // wait until we received a notification await receivedNotification.Task; // unsubscribe - await client.UnsubscribeFromResourceAsync("test://static/resource/1", TestContext.Current.CancellationToken); + await client.UnsubscribeFromResourceAsync("test://static/resource/1", null, TestContext.Current.CancellationToken); } [Theory] @@ -408,7 +408,7 @@ public async Task Sampling_Stdio(string clientId) //[Theory] //[MemberData(nameof(GetClients))] //public async Task Roots_Stdio_EverythingServer(string clientId) - //{ + //{ // var rootsHandlerCalls = 0; // var testRoots = new List // { @@ -573,7 +573,7 @@ public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId) }); // act - await client.SetLoggingLevel(LoggingLevel.Debug, TestContext.Current.CancellationToken); + await client.SetLoggingLevel(LoggingLevel.Debug, options: null, TestContext.Current.CancellationToken); // assert await receivedNotification.Task; diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 8fdeacb9..40aa343b 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(TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(null, 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(TestContext.Current.CancellationToken); + var prompts = await client.ListPromptsAsync(null, 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(TestContext.Current.CancellationToken); + prompts = await client.ListPromptsAsync(null, 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(TestContext.Current.CancellationToken); + prompts = await client.ListPromptsAsync(null, 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 4dd2c3d4..da536164 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(TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(null, 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(TestContext.Current.CancellationToken); + var resources = await client.ListResourceTemplatesAsync(null, 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(TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(null, 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(TestContext.Current.CancellationToken); + resources = await client.ListResourcesAsync(null, 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(TestContext.Current.CancellationToken); + resources = await client.ListResourcesAsync(null, 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 3739b7ae..4fe51fc8 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -12,6 +12,7 @@ using System.ComponentModel; using System.IO.Pipelines; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading.Channels; @@ -686,7 +687,11 @@ public async Task HandlesIProgressParameter() new CallToolRequestParams { Name = progressTool.ProtocolTool.Name, - ProgressToken = new("abc123"), + // Set the progress token in Meta + Meta = new JsonObject + { + ["progressToken"] = "abc123", + } }, cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceCapabilityIntegrationTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceCapabilityIntegrationTests.cs index c518b703..0f457aae 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(TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(null, 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(TestContext.Current.CancellationToken); + var resources = await client.ListResourcesAsync(null, TestContext.Current.CancellationToken); Assert.NotEmpty(resources); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs index 63eb9fb5..ca82c648 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerResourceRoutingTests.cs @@ -23,19 +23,19 @@ public async Task MultipleTemplatedResources_MatchesCorrectResource() // Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/821. await using McpClient client = await CreateMcpClientForServer(); - var nonTemplatedResult = await client.ReadResourceAsync("test://resource/non-templated", TestContext.Current.CancellationToken); + var nonTemplatedResult = await client.ReadResourceAsync("test://resource/non-templated", null, TestContext.Current.CancellationToken); Assert.Equal("static", ((TextResourceContents)nonTemplatedResult.Contents[0]).Text); - var templatedResult = await client.ReadResourceAsync("test://resource/12345", TestContext.Current.CancellationToken); + var templatedResult = await client.ReadResourceAsync("test://resource/12345", null, TestContext.Current.CancellationToken); Assert.Equal("template: 12345", ((TextResourceContents)templatedResult.Contents[0]).Text); - var exactTemplatedResult = await client.ReadResourceAsync("test://resource/{id}", TestContext.Current.CancellationToken); + var exactTemplatedResult = await client.ReadResourceAsync("test://resource/{id}", null, TestContext.Current.CancellationToken); Assert.Equal("template: {id}", ((TextResourceContents)exactTemplatedResult.Contents[0]).Text); - var paramsResult = await client.ReadResourceAsync("test://params?a1=a&a2=b&a3=c", TestContext.Current.CancellationToken); + var paramsResult = await client.ReadResourceAsync("test://params?a1=a&a2=b&a3=c", null, TestContext.Current.CancellationToken); Assert.Equal("params: a, b, c", ((TextResourceContents)paramsResult.Contents[0]).Text); - var mcpEx = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", TestContext.Current.CancellationToken)); + var mcpEx = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync("test://params{?a1,a2,a3}", null, TestContext.Current.CancellationToken)); Assert.Equal(McpErrorCode.InvalidParams, mcpEx.ErrorCode); Assert.Equal("Request failed (remote): Unknown resource URI: 'test://params{?a1,a2,a3}'", mcpEx.Message); } diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs index 5ddc3c54..574eec3e 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs @@ -24,7 +24,7 @@ public async Task InjectScopedServiceAsArgument() { await using McpClient client = await CreateMcpClientForServer(); - var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(new RequestOptions { JsonSerializerOptions = McpServerScopedTestsJsonContext.Default.Options }, TestContext.Current.CancellationToken); var tool = tools.First(t => t.Name == "echo_complex"); Assert.DoesNotContain("\"complex\"", JsonSerializer.Serialize(tool.JsonSchema, McpJsonUtilities.DefaultOptions)); diff --git a/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs b/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs index 882fd504..2618f010 100644 --- a/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/EmptyCollectionTests.cs @@ -31,7 +31,7 @@ public async Task EmptyResourceCollection_CanAddResourcesDynamically() var client = await CreateMcpClientForServer(); // Initially, the resource collection is empty - var initialResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken); + var initialResources = await client.ListResourcesAsync(options: null, TestContext.Current.CancellationToken); Assert.Empty(initialResources); // Add a resource dynamically @@ -40,7 +40,7 @@ public async Task EmptyResourceCollection_CanAddResourcesDynamically() new() { UriTemplate = "test://resource/1" })); // The resource should now be listed - var updatedResources = await client.ListResourcesAsync(TestContext.Current.CancellationToken); + var updatedResources = await client.ListResourcesAsync(options: null, TestContext.Current.CancellationToken); Assert.Single(updatedResources); Assert.Equal("test://resource/1", updatedResources[0].Uri); } @@ -71,7 +71,7 @@ public async Task EmptyPromptCollection_CanAddPromptsDynamically() var client = await CreateMcpClientForServer(); // Initially, the prompt collection is empty - var initialPrompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); + var initialPrompts = await client.ListPromptsAsync(options: null, TestContext.Current.CancellationToken); Assert.Empty(initialPrompts); // Add a prompt dynamically @@ -80,7 +80,7 @@ public async Task EmptyPromptCollection_CanAddPromptsDynamically() new() { Name = "test_prompt", Description = "A test prompt" })); // The prompt should now be listed - var updatedPrompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); + var updatedPrompts = await client.ListPromptsAsync(options: null, TestContext.Current.CancellationToken); Assert.Single(updatedPrompts); Assert.Equal("test_prompt", updatedPrompts[0].Name); } @@ -96,7 +96,7 @@ public async Task EmptyResourceCollection_CanCallReadResourceAfterAddingDynamica new() { UriTemplate = "test://resource/dynamic" })); // Read the resource - var result = await client.ReadResourceAsync("test://resource/dynamic", TestContext.Current.CancellationToken); + var result = await client.ReadResourceAsync("test://resource/dynamic", options: null, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Single(result.Contents); Assert.IsType(result.Contents[0]); @@ -159,7 +159,7 @@ public async Task ListFails() var client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)); - await Assert.ThrowsAsync(async () => await client.ListPromptsAsync(TestContext.Current.CancellationToken)); - await Assert.ThrowsAsync(async () => await client.ListResourcesAsync(TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(async () => await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken)); } }