diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
index 6397d8e7..9f75d972 100644
--- a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs
@@ -4,6 +4,7 @@
using ModelContextProtocol.Server;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Text.Json.Nodes;
namespace ModelContextProtocol.Client;
@@ -491,6 +492,7 @@ public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellation
/// An optional dictionary of arguments to pass to the tool.
/// Optional progress reporter for server notifications.
/// JSON serializer options.
+ /// Optional metadata to include in the request. This will be serialized as the _meta field in the JSON-RPC request parameters.
/// A cancellation token.
/// The from the tool execution.
public ValueTask CallToolAsync(
@@ -498,6 +500,7 @@ public ValueTask CallToolAsync(
IReadOnlyDictionary? arguments = null,
IProgress? progress = null,
JsonSerializerOptions? serializerOptions = null,
+ JsonObject? meta = null,
CancellationToken cancellationToken = default)
{
Throw.IfNull(toolName);
@@ -506,7 +509,7 @@ public ValueTask CallToolAsync(
if (progress is not null)
{
- return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, cancellationToken);
+ return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, meta, cancellationToken);
}
return SendRequestAsync(
@@ -515,6 +518,7 @@ public ValueTask CallToolAsync(
{
Name = toolName,
Arguments = ToArgumentsDictionary(arguments, serializerOptions),
+ Meta = meta,
},
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
McpJsonUtilities.JsonContext.Default.CallToolResult,
@@ -525,6 +529,7 @@ async ValueTask SendRequestWithProgressAsync(
IReadOnlyDictionary? arguments,
IProgress progress,
JsonSerializerOptions serializerOptions,
+ JsonObject? meta,
CancellationToken cancellationToken)
{
ProgressToken progressToken = new(Guid.NewGuid().ToString("N"));
@@ -541,14 +546,22 @@ async ValueTask SendRequestWithProgressAsync(
return default;
}).ConfigureAwait(false);
+ // Clone the meta object if provided, as we need to add the progress token to it without mutating the original
+ JsonObject? metaWithProgress = meta is not null ? JsonNode.Parse(meta.ToJsonString())?.AsObject() : null;
+
+ var requestParams = new CallToolRequestParams
+ {
+ Name = toolName,
+ Arguments = ToArgumentsDictionary(arguments, serializerOptions),
+ Meta = metaWithProgress,
+ };
+
+ // Use the ProgressToken property setter which handles creating Meta if needed
+ requestParams.ProgressToken = progressToken;
+
return await SendRequestAsync(
RequestMethods.ToolsCall,
- new()
- {
- Name = toolName,
- Arguments = ToArgumentsDictionary(arguments, serializerOptions),
- ProgressToken = progressToken,
- },
+ requestParams,
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
McpJsonUtilities.JsonContext.Default.CallToolResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs
index de2d0071..57b56e48 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs
@@ -4,6 +4,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
+using System.Text.Json.Nodes;
namespace ModelContextProtocol.Client;
@@ -556,6 +557,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri,
///
/// The JSON serialization options governing argument serialization. If , the default serialization options will be used.
///
+ /// Optional metadata to include in the request. This will be serialized as the _meta field in the JSON-RPC request parameters.
/// The to monitor for cancellation requests. The default is .
///
/// A task containing the from the tool execution. The response includes
@@ -583,8 +585,9 @@ public static ValueTask CallToolAsync(
IReadOnlyDictionary? arguments = null,
IProgress? progress = null,
JsonSerializerOptions? serializerOptions = null,
+ JsonObject? meta = null,
CancellationToken cancellationToken = default)
- => AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, serializerOptions, cancellationToken);
+ => AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, serializerOptions, meta, cancellationToken);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable CS0618 // Type or member is obsolete
diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
index 012bc813..0306fc9b 100644
--- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs
+++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs
@@ -2,6 +2,7 @@
using ModelContextProtocol.Protocol;
using System.Collections.ObjectModel;
using System.Text.Json;
+using System.Text.Json.Nodes;
namespace ModelContextProtocol.Client;
@@ -36,6 +37,7 @@ public sealed class McpClientTool : AIFunction
private readonly string _name;
private readonly string _description;
private readonly IProgress? _progress;
+ private readonly JsonObject? _meta;
///
/// Initializes a new instance of the class.
@@ -74,6 +76,7 @@ public McpClientTool(
_name = tool.Name;
_description = tool.Description ?? string.Empty;
_progress = null;
+ _meta = null;
}
internal McpClientTool(
@@ -82,7 +85,8 @@ internal McpClientTool(
JsonSerializerOptions serializerOptions,
string? name = null,
string? description = null,
- IProgress? progress = null)
+ IProgress? progress = null,
+ JsonObject? metadata = null)
{
_client = client;
ProtocolTool = tool;
@@ -90,6 +94,7 @@ internal McpClientTool(
_name = name ?? tool.Name;
_description = description ?? tool.Description ?? string.Empty;
_progress = progress;
+ _meta = metadata;
}
///
@@ -194,7 +199,7 @@ public ValueTask CallAsync(
IProgress? progress = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default) =>
- _client.CallToolAsync(ProtocolTool.Name, arguments, progress, serializerOptions, cancellationToken);
+ _client.CallToolAsync(ProtocolTool.Name, arguments, progress, serializerOptions, _meta, cancellationToken);
///
/// Creates a new instance of the tool but modified to return the specified name from its property.
@@ -221,7 +226,7 @@ public ValueTask CallAsync(
///
///
public McpClientTool WithName(string name) =>
- new(_client, ProtocolTool, JsonSerializerOptions, name, _description, _progress);
+ new(_client, ProtocolTool, JsonSerializerOptions, name, _description, _progress, _meta);
///
/// Creates a new instance of the tool but modified to return the specified description from its property.
@@ -245,7 +250,7 @@ public McpClientTool WithName(string name) =>
///
/// A new instance of with the provided description.
public McpClientTool WithDescription(string description) =>
- new(_client, ProtocolTool, JsonSerializerOptions, _name, description, _progress);
+ new(_client, ProtocolTool, JsonSerializerOptions, _name, description, _progress, _meta);
///
/// Creates a new instance of the tool but modified to report progress via the specified .
@@ -268,6 +273,35 @@ public McpClientTool WithProgress(IProgress progress)
{
Throw.IfNull(progress);
- return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, _description, progress);
+ return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, _description, progress, _meta);
+ }
+
+ ///
+ /// Creates a new instance of the tool but modified to include the specified metadata in tool call requests.
+ ///
+ ///
+ /// The metadata to include in tool call requests. This will be serialized as the _meta field
+ /// in the JSON-RPC request parameters.
+ ///
+ ///
+ ///
+ /// Adding metadata to the tool allows you to pass additional protocol-level information with each tool call.
+ /// This can be useful for tracing, logging, or passing context information to the server.
+ ///
+ ///
+ /// Only one metadata object can be specified at a time. Calling again
+ /// will overwrite any previously specified metadata.
+ ///
+ ///
+ /// The metadata is passed through to the server as-is, merged with any protocol-level metadata
+ /// such as progress tokens when is also used.
+ ///
+ ///
+ /// A new instance of , configured with the provided metadata.
+ public McpClientTool WithMeta(JsonObject metadata)
+ {
+ Throw.IfNull(metadata);
+
+ return new McpClientTool(_client, ProtocolTool, JsonSerializerOptions, _name, _description, _progress, metadata);
}
}
\ No newline at end of file
diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
index 7f1cc368..880d7e49 100644
--- a/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
+++ b/tests/ModelContextProtocol.Tests/Client/McpClientToolTests.cs
@@ -151,6 +151,15 @@ public static EmbeddedResourceBlock BinaryResourceTool() =>
MimeType = "application/octet-stream"
}
};
+
+ // Tool that echoes back the metadata it receives
+ [McpServerTool]
+ public static TextContentBlock MetadataEchoTool(RequestContext context)
+ {
+ var meta = context.Params?.Meta;
+ var metaJson = meta?.ToJsonString() ?? "{}";
+ return new TextContentBlock { Text = metaJson };
+ }
}
[Fact]
@@ -234,10 +243,10 @@ public async Task MixedContentTool_ReturnsAIContentArray()
var aiContents = Assert.IsType(result);
Assert.Equal(2, aiContents.Length);
-
+
var textContent = Assert.IsType(aiContents[0]);
Assert.Equal("Description of the image", textContent.Text);
-
+
var dataContent = Assert.IsType(aiContents[1]);
Assert.Equal("image/png", dataContent.MediaType);
}
@@ -253,11 +262,11 @@ public async Task MultipleImagesTool_ReturnsAIContentArray()
var aiContents = Assert.IsType(result);
Assert.Equal(2, aiContents.Length);
-
+
var dataContent0 = Assert.IsType(aiContents[0]);
Assert.Equal("image/png", dataContent0.MediaType);
Assert.Equal("image1", Encoding.UTF8.GetString(dataContent0.Data.ToArray()));
-
+
var dataContent1 = Assert.IsType(aiContents[1]);
Assert.Equal("image/jpeg", dataContent1.MediaType);
Assert.Equal("image2", Encoding.UTF8.GetString(dataContent1.Data.ToArray()));
@@ -274,10 +283,10 @@ public async Task AudioWithTextTool_ReturnsAIContentArray()
var aiContents = Assert.IsType(result);
Assert.Equal(2, aiContents.Length);
-
+
var textContent = Assert.IsType(aiContents[0]);
Assert.Equal("Audio transcription", textContent.Text);
-
+
var dataContent = Assert.IsType(aiContents[1]);
Assert.Equal("audio/wav", dataContent.MediaType);
}
@@ -293,10 +302,10 @@ public async Task ResourceWithTextTool_ReturnsAIContentArray()
var aiContents = Assert.IsType(result);
Assert.Equal(2, aiContents.Length);
-
+
var textContent0 = Assert.IsType(aiContents[0]);
Assert.Equal("Resource description", textContent0.Text);
-
+
var textContent1 = Assert.IsType(aiContents[1]);
Assert.Equal("File content", textContent1.Text);
}
@@ -312,16 +321,16 @@ public async Task AllContentTypesTool_ReturnsAIContentArray()
var aiContents = Assert.IsType(result);
Assert.Equal(4, aiContents.Length);
-
+
var textContent = Assert.IsType(aiContents[0]);
Assert.Equal("Mixed content", textContent.Text);
-
+
var dataContent1 = Assert.IsType(aiContents[1]);
Assert.Equal("image/png", dataContent1.MediaType);
-
+
var dataContent2 = Assert.IsType(aiContents[2]);
Assert.Equal("audio/mp3", dataContent2.MediaType);
-
+
var dataContent3 = Assert.IsType(aiContents[3]);
Assert.Equal("application/octet-stream", dataContent3.MediaType);
}
@@ -354,7 +363,7 @@ public async Task ResourceLinkTool_ReturnsJsonElement()
var jsonElement = (JsonElement)result!;
Assert.True(jsonElement.TryGetProperty("content", out var contentValue));
Assert.Equal(JsonValueKind.Array, contentValue.ValueKind);
-
+
Assert.Equal(1, contentValue.GetArrayLength());
}
@@ -371,11 +380,11 @@ public async Task MixedWithNonConvertibleTool_ReturnsJsonElement()
Assert.True(jsonElement.TryGetProperty("content", out var contentArray));
Assert.Equal(JsonValueKind.Array, contentArray.ValueKind);
Assert.Equal(2, contentArray.GetArrayLength());
-
+
var firstContent = contentArray[0];
Assert.True(firstContent.TryGetProperty("type", out var type1));
Assert.Equal("image", type1.GetString());
-
+
var secondContent = contentArray[1];
Assert.True(secondContent.TryGetProperty("type", out var type2));
Assert.Equal("resource_link", type2.GetString());
@@ -486,4 +495,110 @@ public async Task MultipleAIContent_PreservesRawRepresentation()
Assert.NotNull(dataContent.RawRepresentation);
Assert.IsType(dataContent.RawRepresentation);
}
+
+ [Fact]
+ public async Task WithMeta_MetaIsPassedToServer()
+ {
+ // Arrange
+ 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 metadata = new JsonObject
+ {
+ ["traceId"] = "test-trace-123",
+ ["customKey"] = "customValue"
+ };
+
+ // Act - use tool with metadata
+ var toolWithMeta = tool.WithMeta(metadata);
+ var result = await toolWithMeta.CallAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Single(result.Content);
+ var textBlock = Assert.IsType(result.Content[0]);
+
+ // The tool echoes back the metadata it received
+ var receivedMetadata = JsonNode.Parse(textBlock.Text)?.AsObject();
+ Assert.NotNull(receivedMetadata);
+ Assert.Equal("test-trace-123", receivedMetadata["traceId"]?.GetValue());
+ Assert.Equal("customValue", receivedMetadata["customKey"]?.GetValue());
+ }
+
+ [Fact]
+ public async Task WithMeta_CreatesNewInstance()
+ {
+ // Arrange
+ await using McpClient client = await CreateMcpClientForServer();
+ var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var tool = tools.Single(t => t.Name == "text_only_tool");
+
+ var metadata = new JsonObject { ["key"] = "value" };
+
+ // Act
+ var toolWithMeta = tool.WithMeta(metadata);
+
+ // Assert - should be a different instance
+ Assert.NotSame(tool, toolWithMeta);
+ Assert.Equal(tool.Name, toolWithMeta.Name);
+ Assert.Equal(tool.Description, toolWithMeta.Description);
+ }
+
+ [Fact]
+ public async Task WithMeta_ChainsWithOtherWithMethods()
+ {
+ // Arrange
+ 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 metadata = new JsonObject { ["chainedKey"] = "chainedValue" };
+
+ // Act - chain WithName, WithDescription, and WithMeta
+ var modifiedTool = tool
+ .WithName("custom_name")
+ .WithDescription("Custom description")
+ .WithMeta(metadata);
+
+ var result = await modifiedTool.CallAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ // Assert - name and description should be modified
+ Assert.Equal("custom_name", modifiedTool.Name);
+ Assert.Equal("Custom description", modifiedTool.Description);
+
+ // Assert - metadata should still be passed
+ var textBlock = Assert.IsType(result.Content[0]);
+ var receivedMetadata = JsonNode.Parse(textBlock.Text)?.AsObject();
+ Assert.NotNull(receivedMetadata);
+ Assert.Equal("chainedValue", receivedMetadata["chainedKey"]?.GetValue());
+ }
+
+ [Fact]
+ public async Task WithMeta_MultipleToolInstancesWithDifferentMetadata()
+ {
+ // Arrange
+ 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 metadata1 = new JsonObject { ["clientId"] = "client-1" };
+ var metadata2 = new JsonObject { ["clientId"] = "client-2" };
+
+ // Act - create two tool instances with different metadata
+ var tool1 = tool.WithMeta(metadata1);
+ var tool2 = tool.WithMeta(metadata2);
+
+ var result1 = await tool1.CallAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var result2 = await tool2.CallAsync(cancellationToken: TestContext.Current.CancellationToken);
+
+ // Assert - each call should have its own metadata
+ var textBlock1 = Assert.IsType(result1.Content[0]);
+ var receivedMetadata1 = JsonNode.Parse(textBlock1.Text)?.AsObject();
+ Assert.Equal("client-1", receivedMetadata1?["clientId"]?.GetValue());
+
+ var textBlock2 = Assert.IsType(result2.Content[0]);
+ var receivedMetadata2 = JsonNode.Parse(textBlock2.Text)?.AsObject();
+ Assert.Equal("client-2", receivedMetadata2?["clientId"]?.GetValue());
+ }
}