diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs index 057831a4e..6490967d0 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -877,7 +877,7 @@ async ValueTask SendRequestWithProgressAsync( return default; }).ConfigureAwait(false); - JsonObject metaWithProgress = meta is not null ? new(meta) : []; + JsonObject metaWithProgress = meta is not null ? (JsonObject)meta.DeepClone() : []; metaWithProgress["progressToken"] = progressToken.ToString(); return await CallToolAsync( @@ -1007,7 +1007,7 @@ async ValueTask SendTaskAugmentedCallToolRequestWithProgressAsync( return default; }).ConfigureAwait(false); - JsonObject metaWithProgress = meta is not null ? new(meta) : []; + JsonObject metaWithProgress = meta is not null ? (JsonObject)meta.DeepClone() : []; metaWithProgress["progressToken"] = progressToken.ToString(); var result = await SendRequestAsync( diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs index e17fb6bc9..6d241f6dc 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs @@ -854,4 +854,99 @@ public async Task CallToolAsync_WithAnonymousTypeArguments_Works() var textBlock = Assert.IsType(result.Content[0]); Assert.Contains("coordinates", textBlock.Text); } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task WithProgress_ProgressTokenInMeta(bool useInvokeAsync) + { + await using McpClient client = await CreateMcpClientForServer(); + + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + var tool = tools.Single(t => t.Name == "metadata_echo_tool"); + + var receivedMetadata = await CallMetadataEchoToolWithProgressAsync(tool, useInvokeAsync); + Assert.NotNull(receivedMetadata); + Assert.NotNull(receivedMetadata["progressToken"]?.GetValue()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task WithMeta_WithProgress_BothMetaAndProgressTokenPresent(bool useInvokeAsync) + { + await using McpClient client = await CreateMcpClientForServer(); + + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + var tool = tools.Single(t => t.Name == "metadata_echo_tool") + .WithMeta(new() { ["traceId"] = "trace-123" }); + + var receivedMetadata = await CallMetadataEchoToolWithProgressAsync(tool, useInvokeAsync); + Assert.NotNull(receivedMetadata); + Assert.Equal("trace-123", receivedMetadata["traceId"]?.GetValue()); + Assert.NotNull(receivedMetadata["progressToken"]?.GetValue()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task WithMeta_WithProgress_DoesNotMutateOriginalMeta(bool useInvokeAsync) + { + await using McpClient client = await CreateMcpClientForServer(); + + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + var tool = tools.Single(t => t.Name == "metadata_echo_tool"); + + JsonObject originalMeta = new() { ["traceId"] = "trace-789" }; + var toolWithMeta = tool.WithMeta(originalMeta); + + await CallMetadataEchoToolWithProgressAsync(toolWithMeta, useInvokeAsync); + await CallMetadataEchoToolWithProgressAsync(toolWithMeta, useInvokeAsync); + + Assert.Single(originalMeta); + Assert.Equal("trace-789", originalMeta["traceId"]?.GetValue()); + Assert.False(originalMeta.ContainsKey("progressToken")); + } + + [Fact] + public async Task WithMeta_WithProgress_WithRequestOptionsMeta_AllMerged() + { + await using McpClient client = await CreateMcpClientForServer(); + + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + var tool = tools.Single(t => t.Name == "metadata_echo_tool") + .WithMeta(new() { ["toolKey"] = "toolValue" }); + + RequestOptions requestOptions = new() + { + Meta = new() { ["requestKey"] = "requestValue" } + }; + + var receivedMetadata = await CallMetadataEchoToolWithProgressAsync(tool, useInvokeAsync: false, requestOptions); + Assert.NotNull(receivedMetadata); + Assert.Equal("toolValue", receivedMetadata["toolKey"]?.GetValue()); + Assert.Equal("requestValue", receivedMetadata["requestKey"]?.GetValue()); + Assert.NotNull(receivedMetadata["progressToken"]?.GetValue()); + } + + private static async Task CallMetadataEchoToolWithProgressAsync( + McpClientTool tool, bool useInvokeAsync, RequestOptions? options = null) + { + var progress = new Progress(); + string text; + + if (useInvokeAsync) + { + tool = tool.WithProgress(progress); + var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken); + text = Assert.IsType(result).Text; + } + else + { + var result = await tool.CallAsync(progress: progress, options: options, cancellationToken: TestContext.Current.CancellationToken); + text = Assert.IsType(result.Content.Single()).Text; + } + + return JsonNode.Parse(text)?.AsObject(); + } }