Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
159 changes: 159 additions & 0 deletions tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -854,4 +854,163 @@ public async Task CallToolAsync_WithAnonymousTypeArguments_Works()
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
Assert.Contains("coordinates", textBlock.Text);
}

[Fact]
public async Task CallAsync_WithProgress_ProgressTokenInMeta()
{
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 progressValues = new List<ProgressNotificationValue>();
var progress = new Progress<ProgressNotificationValue>(p => progressValues.Add(p));

// Pass progress directly to CallAsync
var result = await tool.CallAsync(progress: progress, cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result);
Assert.Single(result.Content);

var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
var receivedMetadata = JsonNode.Parse(textBlock.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
}

[Fact]
public async Task CallAsync_WithMeta_WithProgress_BothMetaAndProgressTokenPresent()
{
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 progressValues = new List<ProgressNotificationValue>();
var progress = new Progress<ProgressNotificationValue>(p => progressValues.Add(p));

// WithMeta on the tool, progress passed to CallAsync
var result = await tool
.WithMeta(new() { ["traceId"] = "trace-123" })
.CallAsync(progress: progress, cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result);
Assert.Single(result.Content);

var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
var receivedMetadata = JsonNode.Parse(textBlock.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.Equal("trace-123", receivedMetadata["traceId"]?.GetValue<string>());
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
}

[Fact]
public async Task InvokeAsync_WithMeta_WithProgress_BothMetaAndProgressTokenPresent()
{
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 progressValues = new List<ProgressNotificationValue>();
var progress = new Progress<ProgressNotificationValue>(p => progressValues.Add(p));

// Both WithMeta and WithProgress on the tool, invoked via InvokeAsync (the AIFunction path)
var result = await tool
.WithMeta(new() { ["traceId"] = "trace-456" })
.WithProgress(progress)
.InvokeAsync(cancellationToken: TestContext.Current.CancellationToken);

// InvokeAsync returns a JsonElement for results with meta
Assert.NotNull(result);
}
Comment thread
stephentoub marked this conversation as resolved.
Outdated

[Fact]
public async Task CallAsync_WithMeta_WithProgress_DoesNotMutateOriginalMeta()
{
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 progressValues = new List<ProgressNotificationValue>();
var progress = new Progress<ProgressNotificationValue>(p => progressValues.Add(p));

JsonObject originalMeta = new()
{
["traceId"] = "trace-789"
};

var toolWithMeta = tool.WithMeta(originalMeta);

// Call multiple times with progress to ensure original meta is not mutated
await toolWithMeta.CallAsync(progress: progress, cancellationToken: TestContext.Current.CancellationToken);
await toolWithMeta.CallAsync(progress: progress, cancellationToken: TestContext.Current.CancellationToken);

// Original meta should not contain progressToken
Assert.Single(originalMeta);
Assert.Equal("trace-789", originalMeta["traceId"]?.GetValue<string>());
Assert.False(originalMeta.ContainsKey("progressToken"));
}

[Fact]
public async Task CallAsync_WithMeta_WithProgress_WithName_WithDescription_AllChained()
{
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 progressValues = new List<ProgressNotificationValue>();
var progress = new Progress<ProgressNotificationValue>(p => progressValues.Add(p));

var modifiedTool = tool
.WithName("custom_name")
.WithDescription("Custom description")
.WithMeta(new() { ["chainedKey"] = "chainedValue" });

Assert.Equal("custom_name", modifiedTool.Name);
Assert.Equal("Custom description", modifiedTool.Description);

var result = await modifiedTool.CallAsync(progress: progress, cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result);
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
var receivedMetadata = JsonNode.Parse(textBlock.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.Equal("chainedValue", receivedMetadata["chainedKey"]?.GetValue<string>());
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
}

[Fact]
public async Task CallAsync_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");

var progressValues = new List<ProgressNotificationValue>();
var progress = new Progress<ProgressNotificationValue>(p => progressValues.Add(p));

RequestOptions requestOptions = new()
{
Meta = new()
{
["requestKey"] = "requestValue"
}
};

var result = await tool
.WithMeta(new() { ["toolKey"] = "toolValue" })
.CallAsync(progress: progress, options: requestOptions, cancellationToken: TestContext.Current.CancellationToken);

Assert.NotNull(result);
var textBlock = Assert.IsType<TextContentBlock>(result.Content[0]);
var receivedMetadata = JsonNode.Parse(textBlock.Text)?.AsObject();
Assert.NotNull(receivedMetadata);
Assert.Equal("toolValue", receivedMetadata["toolKey"]?.GetValue<string>());
Assert.Equal("requestValue", receivedMetadata["requestKey"]?.GetValue<string>());
Assert.NotNull(receivedMetadata["progressToken"]?.GetValue<string>());
}
}
Loading