Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ async ValueTask<CallToolResult> 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(
Expand Down Expand Up @@ -1007,7 +1007,7 @@ async ValueTask<McpTask> 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(
Expand Down
95 changes: 95 additions & 0 deletions tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -854,4 +854,99 @@ public async Task CallToolAsync_WithAnonymousTypeArguments_Works()
var textBlock = Assert.IsType<TextContentBlock>(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<string>());
}

[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<string>());
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
}

[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<string>());
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<string>());
Assert.Equal("requestValue", receivedMetadata["requestKey"]?.GetValue<string>());
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
}

private static async Task<JsonObject?> CallMetadataEchoToolWithProgressAsync(
McpClientTool tool, bool useInvokeAsync, RequestOptions? options = null)
{
var progress = new Progress<ProgressNotificationValue>();
string text;

if (useInvokeAsync)
{
tool = tool.WithProgress(progress);
var result = await tool.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);
text = Assert.IsType<TextContent>(result).Text;
}
else
{
var result = await tool.CallAsync(progress: progress, options: options, cancellationToken: TestContext.Current.CancellationToken);
text = Assert.IsType<TextContentBlock>(result.Content.Single()).Text;
}

return JsonNode.Parse(text)?.AsObject();
}
}
Loading