From fcd2be5cc3c7c82685aae7d5bc538b62e1010eab Mon Sep 17 00:00:00 2001 From: anuchandy Date: Mon, 21 Jul 2025 10:01:51 -0700 Subject: [PATCH 1/4] Dictionary_string_JsonElement overload for CallToolAsync --- .../Client/McpClientExtensions.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 60a9c3a6..b7c37e49 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -789,6 +789,98 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, return UnsubscribeFromResourceAsync(client, uri.ToString(), cancellationToken); } + /// + /// Invokes a tool on the server. + /// + /// The client instance used to communicate with the MCP server. + /// The name of the tool to call on the server. + /// A dictionary of arguments to pass to the tool. Each key represents a parameter name, + /// and its associated value represents the argument value as a . + /// + /// + /// An optional to have progress notifications reported to it. Setting this to a non- + /// value will result in a progress token being included in the call, and any resulting progress notifications during the operation + /// routed to this instance. + /// + /// The to monitor for cancellation requests. The default is . + /// + /// A task containing the from the tool execution. The response includes + /// the tool's output content, which may be structured data, text, or an error message. + /// + /// is . + /// is . + /// The server could not find the requested tool, or the server encountered an error while processing the request. + /// + /// + /// // Call a tool with JsonElement arguments + /// var arguments = new Dictionary<string, JsonElement> + /// { + /// ["message"] = JsonSerializer.SerializeToElement("Hello MCP!") + /// }; + /// var result = await client.CallToolAsync("echo", arguments); + /// + /// + public static ValueTask CallToolAsync( + this IMcpClient client, + string toolName, + IReadOnlyDictionary arguments, + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(client); + Throw.IfNull(toolName); + + if (progress is not null) + { + return SendRequestWithProgressAsync(client, toolName, arguments, progress, cancellationToken); + } + + return client.SendRequestAsync( + RequestMethods.ToolsCall, + new() + { + Name = toolName, + Arguments = arguments, + }, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken); + + static async ValueTask SendRequestWithProgressAsync( + IMcpClient client, + string toolName, + IReadOnlyDictionary arguments, + IProgress progress, + CancellationToken cancellationToken) + { + ProgressToken progressToken = new(Guid.NewGuid().ToString("N")); + + await using var _ = client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, + (notification, cancellationToken) => + { + if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn && + pn.ProgressToken == progressToken) + { + progress.Report(pn.Progress); + } + + return default; + }).ConfigureAwait(false); + + return await client.SendRequestAsync( + RequestMethods.ToolsCall, + new() + { + Name = toolName, + Arguments = arguments, + ProgressToken = progressToken, + }, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + /// /// Invokes a tool on the server. /// From 5820adb50e6f904de0f36f1dbc6dc04acf4d3228 Mon Sep 17 00:00:00 2001 From: anuchandy Date: Mon, 21 Jul 2025 11:13:59 -0700 Subject: [PATCH 2/4] Adding tests for CallToolAsync taking Dictionary of JsonElement --- .../ClientIntegrationTests.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 3e4361a5..229a8e4c 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -96,6 +96,30 @@ public async Task CallTool_Stdio_EchoServer(string clientId) Assert.Equal("Echo: Hello MCP!", textContent.Text); } + [Theory] + [MemberData(nameof(GetClients))] + public async Task CallTool_Stdio_EchoServer_WithJsonElementArguments(string clientId) + { + // arrange + + // act + await using var client = await _fixture.CreateClientAsync(clientId); + var result = await client.CallToolAsync( + "echo", + new Dictionary + { + ["message"] = JsonDocument.Parse("\"Hello MCP with JsonElement!\"").RootElement + }, + cancellationToken: TestContext.Current.CancellationToken + ); + + // assert + Assert.NotNull(result); + Assert.Null(result.IsError); + var textContent = Assert.Single(result.Content.OfType()); + Assert.Equal("Echo: Hello MCP with JsonElement!", textContent.Text); + } + [Fact] public async Task CallTool_Stdio_EchoSessionId_ReturnsEmpty() { From abd810488ef9eaa24754c69559cd646235dd6b74 Mon Sep 17 00:00:00 2001 From: anuchandy Date: Wed, 23 Jul 2025 00:05:34 -0700 Subject: [PATCH 3/4] updating the CallToolAsync to take JsonElement --- .../Client/McpClientExtensions.cs | 15 ++++++--------- .../ClientIntegrationTests.cs | 10 ++++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index b7c37e49..9a70e80a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -794,7 +794,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// /// The client instance used to communicate with the MCP server. /// The name of the tool to call on the server. - /// A dictionary of arguments to pass to the tool. Each key represents a parameter name, + /// A containing arguments to pass to the tool. Each property represents a tool parameter name, /// and its associated value represents the argument value as a . /// /// @@ -813,17 +813,14 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// /// /// // Call a tool with JsonElement arguments - /// var arguments = new Dictionary<string, JsonElement> - /// { - /// ["message"] = JsonSerializer.SerializeToElement("Hello MCP!") - /// }; + /// var arguments = JsonDocument.Parse("""{"message": "Hello MCP!"}""").RootElement; /// var result = await client.CallToolAsync("echo", arguments); /// /// public static ValueTask CallToolAsync( this IMcpClient client, string toolName, - IReadOnlyDictionary arguments, + JsonElement arguments, IProgress? progress = null, CancellationToken cancellationToken = default) { @@ -840,7 +837,7 @@ public static ValueTask CallToolAsync( new() { Name = toolName, - Arguments = arguments, + Arguments = arguments.EnumerateObject().ToDictionary(prop => prop.Name, prop => prop.Value) ?? [] }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, McpJsonUtilities.JsonContext.Default.CallToolResult, @@ -849,7 +846,7 @@ public static ValueTask CallToolAsync( static async ValueTask SendRequestWithProgressAsync( IMcpClient client, string toolName, - IReadOnlyDictionary arguments, + JsonElement arguments, IProgress progress, CancellationToken cancellationToken) { @@ -872,7 +869,7 @@ static async ValueTask SendRequestWithProgressAsync( new() { Name = toolName, - Arguments = arguments, + Arguments = arguments.EnumerateObject().ToDictionary(prop => prop.Name, prop => prop.Value) ?? [], ProgressToken = progressToken, }, McpJsonUtilities.JsonContext.Default.CallToolRequestParams, diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 229a8e4c..f9187690 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -101,15 +101,17 @@ public async Task CallTool_Stdio_EchoServer(string clientId) public async Task CallTool_Stdio_EchoServer_WithJsonElementArguments(string clientId) { // arrange + JsonElement arguments = JsonDocument.Parse(""" + { + "message": "Hello MCP with JsonElement!" + } + """).RootElement; // act await using var client = await _fixture.CreateClientAsync(clientId); var result = await client.CallToolAsync( "echo", - new Dictionary - { - ["message"] = JsonDocument.Parse("\"Hello MCP with JsonElement!\"").RootElement - }, + arguments, cancellationToken: TestContext.Current.CancellationToken ); From f7fd3ec27359308f4a11af374c891965ed48050b Mon Sep 17 00:00:00 2001 From: anuchandy Date: Wed, 23 Jul 2025 00:27:29 -0700 Subject: [PATCH 4/4] review feedback: add validation check for non-object JsonElement --- .../Client/McpClientExtensions.cs | 6 +++++ .../ClientIntegrationTests.cs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 9a70e80a..02716ca6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -809,6 +809,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// /// is . /// is . + /// does not represent a JSON object. /// The server could not find the requested tool, or the server encountered an error while processing the request. /// /// @@ -827,6 +828,11 @@ public static ValueTask CallToolAsync( Throw.IfNull(client); Throw.IfNull(toolName); + if (arguments.ValueKind != JsonValueKind.Object) + { + throw new ArgumentException($"The arguments parameter must represent a JSON object, but was {arguments.ValueKind}.", nameof(arguments)); + } + if (progress is not null) { return SendRequestWithProgressAsync(client, toolName, arguments, progress, cancellationToken); diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index f9187690..14a305a7 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -122,6 +122,30 @@ public async Task CallTool_Stdio_EchoServer_WithJsonElementArguments(string clie Assert.Equal("Echo: Hello MCP with JsonElement!", textContent.Text); } + [Theory] + [MemberData(nameof(GetClients))] + public async Task CallTool_Stdio_EchoServer_WithJsonElementArguments_ThrowsForNonObject(string clientId) + { + // arrange - JsonElement representing a string, not an object + JsonElement stringArguments = JsonDocument.Parse(""" + "Hello MCP!" + """).RootElement; + + // act & assert + await using var client = await _fixture.CreateClientAsync(clientId); + var exception = await Assert.ThrowsAsync(async () => + await client.CallToolAsync( + "echo", + stringArguments, + cancellationToken: TestContext.Current.CancellationToken + ) + ); + + Assert.Contains("arguments parameter must represent a JSON object", exception.Message); + Assert.Contains("String", exception.Message); + Assert.Equal("arguments", exception.ParamName); + } + [Fact] public async Task CallTool_Stdio_EchoSessionId_ReturnsEmpty() {