Skip to content

Commit 49bf170

Browse files
Copilotstephentoub
andauthored
Fix WithMeta + WithProgress causing tool invocation failure (#1464)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent cf63f0e commit 49bf170

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ async ValueTask<CallToolResult> SendRequestWithProgressAsync(
877877
return default;
878878
}).ConfigureAwait(false);
879879

880-
JsonObject metaWithProgress = meta is not null ? new(meta) : [];
880+
JsonObject metaWithProgress = meta is not null ? (JsonObject)meta.DeepClone() : [];
881881
metaWithProgress["progressToken"] = progressToken.ToString();
882882

883883
return await CallToolAsync(
@@ -1007,7 +1007,7 @@ async ValueTask<McpTask> SendTaskAugmentedCallToolRequestWithProgressAsync(
10071007
return default;
10081008
}).ConfigureAwait(false);
10091009

1010-
JsonObject metaWithProgress = meta is not null ? new(meta) : [];
1010+
JsonObject metaWithProgress = meta is not null ? (JsonObject)meta.DeepClone() : [];
10111011
metaWithProgress["progressToken"] = progressToken.ToString();
10121012

10131013
var result = await SendRequestAsync(

tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,4 +854,99 @@ public async Task CallToolAsync_WithAnonymousTypeArguments_Works()
854854
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
855855
Assert.Contains("coordinates", textBlock.Text);
856856
}
857+
858+
[Theory]
859+
[InlineData(false)]
860+
[InlineData(true)]
861+
public async Task WithProgress_ProgressTokenInMeta(bool useInvokeAsync)
862+
{
863+
await using McpClient client = await CreateMcpClientForServer();
864+
865+
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
866+
var tool = tools.Single(t => t.Name == "metadata_echo_tool");
867+
868+
var receivedMetadata = await CallMetadataEchoToolWithProgressAsync(tool, useInvokeAsync);
869+
Assert.NotNull(receivedMetadata);
870+
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
871+
}
872+
873+
[Theory]
874+
[InlineData(false)]
875+
[InlineData(true)]
876+
public async Task WithMeta_WithProgress_BothMetaAndProgressTokenPresent(bool useInvokeAsync)
877+
{
878+
await using McpClient client = await CreateMcpClientForServer();
879+
880+
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
881+
var tool = tools.Single(t => t.Name == "metadata_echo_tool")
882+
.WithMeta(new() { ["traceId"] = "trace-123" });
883+
884+
var receivedMetadata = await CallMetadataEchoToolWithProgressAsync(tool, useInvokeAsync);
885+
Assert.NotNull(receivedMetadata);
886+
Assert.Equal("trace-123", receivedMetadata["traceId"]?.GetValue<string>());
887+
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
888+
}
889+
890+
[Theory]
891+
[InlineData(false)]
892+
[InlineData(true)]
893+
public async Task WithMeta_WithProgress_DoesNotMutateOriginalMeta(bool useInvokeAsync)
894+
{
895+
await using McpClient client = await CreateMcpClientForServer();
896+
897+
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
898+
var tool = tools.Single(t => t.Name == "metadata_echo_tool");
899+
900+
JsonObject originalMeta = new() { ["traceId"] = "trace-789" };
901+
var toolWithMeta = tool.WithMeta(originalMeta);
902+
903+
await CallMetadataEchoToolWithProgressAsync(toolWithMeta, useInvokeAsync);
904+
await CallMetadataEchoToolWithProgressAsync(toolWithMeta, useInvokeAsync);
905+
906+
Assert.Single(originalMeta);
907+
Assert.Equal("trace-789", originalMeta["traceId"]?.GetValue<string>());
908+
Assert.False(originalMeta.ContainsKey("progressToken"));
909+
}
910+
911+
[Fact]
912+
public async Task WithMeta_WithProgress_WithRequestOptionsMeta_AllMerged()
913+
{
914+
await using McpClient client = await CreateMcpClientForServer();
915+
916+
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
917+
var tool = tools.Single(t => t.Name == "metadata_echo_tool")
918+
.WithMeta(new() { ["toolKey"] = "toolValue" });
919+
920+
RequestOptions requestOptions = new()
921+
{
922+
Meta = new() { ["requestKey"] = "requestValue" }
923+
};
924+
925+
var receivedMetadata = await CallMetadataEchoToolWithProgressAsync(tool, useInvokeAsync: false, requestOptions);
926+
Assert.NotNull(receivedMetadata);
927+
Assert.Equal("toolValue", receivedMetadata["toolKey"]?.GetValue<string>());
928+
Assert.Equal("requestValue", receivedMetadata["requestKey"]?.GetValue<string>());
929+
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
930+
}
931+
932+
private static async Task<JsonObject?> CallMetadataEchoToolWithProgressAsync(
933+
McpClientTool tool, bool useInvokeAsync, RequestOptions? options = null)
934+
{
935+
var progress = new Progress<ProgressNotificationValue>();
936+
string text;
937+
938+
if (useInvokeAsync)
939+
{
940+
tool = tool.WithProgress(progress);
941+
var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
942+
text = Assert.IsType<TextContent>(result).Text;
943+
}
944+
else
945+
{
946+
var result = await tool.CallAsync(progress: progress, options: options, cancellationToken: TestContext.Current.CancellationToken);
947+
text = Assert.IsType<TextContentBlock>(result.Content.Single()).Text;
948+
}
949+
950+
return JsonNode.Parse(text)?.AsObject();
951+
}
857952
}

0 commit comments

Comments
 (0)