Skip to content
Open
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
2 changes: 1 addition & 1 deletion samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ await ctx.Server.SampleAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
],
options: new ChatOptions
chatOptions: new ChatOptions
{
MaxOutputTokens = 100,
Temperature = 0.7f,
Expand Down
2 changes: 1 addition & 1 deletion samples/EverythingServer/Tools/SampleLlmTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static async Task<string> SampleLLM(
CancellationToken cancellationToken)
{
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken);
var sampleResult = await server.SampleAsync(samplingParams, cancellationToken: cancellationToken);

return $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}";
}
Expand Down
2 changes: 1 addition & 1 deletion samples/TestServerWithHosting/Tools/SampleLlmTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static async Task<string> SampleLLM(
CancellationToken cancellationToken)
{
var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens);
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken);
var sampleResult = await thisServer.SampleAsync(samplingParams, cancellationToken: cancellationToken);

return $"LLM sampling result: {sampleResult.Content.OfType<TextContentBlock>().FirstOrDefault()?.Text}";
}
Expand Down
120 changes: 73 additions & 47 deletions src/ModelContextProtocol.Core/Client/McpClient.Methods.cs

Large diffs are not rendered by default.

56 changes: 28 additions & 28 deletions src/ModelContextProtocol.Core/Client/McpClientExtensions.cs

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions src/ModelContextProtocol.Core/Client/McpClientPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ public sealed class McpClientPrompt
/// <remarks>
/// <para>
/// This constructor enables reusing cached prompt definitions across different <see cref="McpClient"/> instances
/// without needing to call <see cref="McpClient.ListPromptsAsync"/> on every reconnect. This is particularly useful
/// without needing to call <see cref="McpClient.ListPromptsAsync"/> on every reconnect. This is particularly useful
/// in scenarios where prompt definitions are stable and network round-trips should be minimized.
/// </para>
/// <para>
/// The provided <paramref name="prompt"/> must represent a prompt that is actually available on the server
/// associated with the <paramref name="client"/>. Attempting to invoke a prompt that doesn't exist on the
/// The provided <paramref name="prompt"/> must represent a prompt that is actually available on the server
/// associated with the <paramref name="client"/>. Attempting to invoke a prompt that doesn't exist on the
/// server will result in an <see cref="McpException"/>.
/// </para>
/// </remarks>
Expand All @@ -57,7 +57,7 @@ public McpClientPrompt(McpClient client, Prompt prompt)
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
/// </para>
/// <para>
/// For most common use cases, you can use the more convenient <see cref="Name"/> and
/// For most common use cases, you can use the more convenient <see cref="Name"/> and
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolPrompt"/> directly.
/// </para>
/// </remarks>
Expand Down Expand Up @@ -85,7 +85,7 @@ public McpClientPrompt(McpClient client, Prompt prompt)
/// The server will process the request and return a result containing messages or other content.
/// </para>
/// <para>
/// This is a convenience method that internally calls <see cref="McpClient.GetPromptAsync"/>
/// This is a convenience method that internally calls <see cref="McpClient.GetPromptAsync"/>
/// with this prompt's name and arguments.
/// </para>
/// </remarks>
Expand All @@ -98,6 +98,6 @@ public async ValueTask<GetPromptResult> GetAsync(
arguments as IReadOnlyDictionary<string, object?> ??
arguments?.ToDictionary();

return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, serializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, new RequestOptions(){Meta = null, JsonSerializerOptions = serializerOptions}, cancellationToken).ConfigureAwait(false);
}
}
6 changes: 3 additions & 3 deletions src/ModelContextProtocol.Core/Client/McpClientResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public McpClientResource(McpClient client, Resource resource)
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
/// </para>
/// <para>
/// For most common use cases, you can use the more convenient <see cref="Name"/> and
/// For most common use cases, you can use the more convenient <see cref="Name"/> and
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolResource"/> directly.
/// </para>
/// </remarks>
Expand Down Expand Up @@ -80,10 +80,10 @@ public McpClientResource(McpClient client, Resource resource)
/// <returns>A <see cref="ValueTask{ReadResourceResult}"/> containing the resource's result with content and messages.</returns>
/// <remarks>
/// <para>
/// This is a convenience method that internally calls <see cref="McpClient.ReadResourceAsync(string, CancellationToken)"/>.
/// This is a convenience method that internally calls <see cref="McpClient.ReadResourceAsync(string, RequestOptions, CancellationToken)"/>.
/// </para>
/// </remarks>
public ValueTask<ReadResourceResult> ReadAsync(
CancellationToken cancellationToken = default) =>
_client.ReadResourceAsync(Uri, cancellationToken);
_client.ReadResourceAsync(Uri, null, cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemp
/// which can be useful for advanced scenarios or when implementing custom MCP client extensions.
/// </para>
/// <para>
/// For most common use cases, you can use the more convenient <see cref="UriTemplate"/> and
/// For most common use cases, you can use the more convenient <see cref="UriTemplate"/> and
/// <see cref="Description"/> properties instead of accessing the <see cref="ProtocolResourceTemplate"/> directly.
/// </para>
/// </remarks>
Expand Down Expand Up @@ -85,5 +85,5 @@ public McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemp
public ValueTask<ReadResourceResult> ReadAsync(
IReadOnlyDictionary<string, object?> arguments,
CancellationToken cancellationToken = default) =>
_client.ReadResourceAsync(UriTemplate, arguments, cancellationToken);
_client.ReadResourceAsync(UriTemplate, arguments, null, cancellationToken);
}
18 changes: 13 additions & 5 deletions src/ModelContextProtocol.Core/Client/McpClientTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Client;
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="McpClientTool"/> class encapsulates an <see cref="McpClient"/> along with a description of
/// The <see cref="McpClientTool"/> class encapsulates an <see cref="McpClient"/> along with a description of
/// a tool available via that client, allowing it to be invoked as an <see cref="AIFunction"/>. This enables integration
/// with AI models that support function calling capabilities.
/// </para>
Expand Down Expand Up @@ -194,7 +194,15 @@ public ValueTask<CallToolResult> CallAsync(
IProgress<ProgressNotificationValue>? progress = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default) =>
_client.CallToolAsync(ProtocolTool.Name, arguments, progress, serializerOptions, cancellationToken);
_client.CallToolAsync(
ProtocolTool.Name,
arguments,
progress,
serializerOptions is null ? null : new RequestOptions
{
JsonSerializerOptions = serializerOptions
},
cancellationToken);

/// <summary>
/// Creates a new instance of the tool but modified to return the specified name from its <see cref="Name"/> property.
Expand All @@ -203,7 +211,7 @@ public ValueTask<CallToolResult> CallAsync(
/// <returns>A new instance of <see cref="McpClientTool"/> with the provided name.</returns>
/// <remarks>
/// <para>
/// This is useful for optimizing the tool name for specific models or for prefixing the tool name
/// This is useful for optimizing the tool name for specific models or for prefixing the tool name
/// with a namespace to avoid conflicts.
/// </para>
/// <para>
Expand All @@ -215,7 +223,7 @@ public ValueTask<CallToolResult> CallAsync(
/// <item>Creating specialized versions of a general tool for specific contexts</item>
/// </list>
/// <para>
/// When invoking <see cref="AIFunction.InvokeAsync"/>, the MCP server will still be called with
/// When invoking <see cref="AIFunction.InvokeAsync"/>, the MCP server will still be called with
/// the original tool name, so no mapping is required on the server side. This new name only affects
/// the value returned from this instance's <see cref="AITool.Name"/>.
/// </para>
Expand All @@ -238,7 +246,7 @@ public McpClientTool WithName(string name) =>
/// <item>You need to tailor the tool's description for specific model requirements</item>
/// </list>
/// <para>
/// When invoking <see cref="AIFunction.InvokeAsync"/>, the MCP server will still be called with
/// When invoking <see cref="AIFunction.InvokeAsync"/>, the MCP server will still be called with
/// the original tool description, so no mapping is required on the server side. This new description only affects
/// the value returned from this instance's <see cref="AITool.Description"/>.
/// </para>
Expand Down
10 changes: 5 additions & 5 deletions src/ModelContextProtocol.Core/McpEndpointExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ public static ValueTask<TResult> SendRequestAsync<TParameters, TResult>(
/// <returns>A task that represents the asynchronous send operation.</returns>
/// <remarks>
/// <para>
/// This method sends a notification without any parameters. Notifications are one-way messages
/// that don't expect a response. They are commonly used for events, status updates, or to signal
/// This method sends a notification without any parameters. Notifications are one-way messages
/// that don't expect a response. They are commonly used for events, status updates, or to signal
/// changes in state.
/// </para>
/// </remarks>
Expand All @@ -78,11 +78,11 @@ public static Task SendNotificationAsync(this IMcpEndpoint client, string method
/// <returns>A task that represents the asynchronous send operation.</returns>
/// <remarks>
/// <para>
/// This method sends a notification with parameters to the connected endpoint. Notifications are one-way
/// This method sends a notification with parameters to the connected endpoint. Notifications are one-way
/// messages that don't expect a response, commonly used for events, status updates, or signaling changes.
/// </para>
/// <para>
/// The parameters object is serialized to JSON according to the provided serializer options or the default
/// The parameters object is serialized to JSON according to the provided serializer options or the default
/// options if none are specified.
/// </para>
/// <para>
Expand Down Expand Up @@ -126,7 +126,7 @@ public static Task NotifyProgressAsync(
ProgressToken progressToken,
ProgressNotificationValue progress,
CancellationToken cancellationToken = default)
=> AsSessionOrThrow(endpoint).NotifyProgressAsync(progressToken, progress, cancellationToken);
=> AsSessionOrThrow(endpoint).NotifyProgressAsync(progressToken, progress, null, cancellationToken);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
#pragma warning disable CS0618 // Type or member is obsolete
Expand Down
5 changes: 3 additions & 2 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static partial class McpJsonUtilities
/// </summary>
/// <remarks>
/// <para>
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// includes source generated contracts for all common exchange types contained in the ModelContextProtocol library.
/// </para>
/// <para>
Expand Down Expand Up @@ -88,7 +88,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]

// JSON-RPC
[JsonSerializable(typeof(JsonRpcMessage))]
[JsonSerializable(typeof(JsonRpcMessage[]))]
Expand Down Expand Up @@ -132,6 +132,7 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(ListRootsResult))]
[JsonSerializable(typeof(ListToolsRequestParams))]
[JsonSerializable(typeof(ListToolsResult))]
[JsonSerializable(typeof(PingRequestParams))]
[JsonSerializable(typeof(PingResult))]
[JsonSerializable(typeof(ReadResourceRequestParams))]
[JsonSerializable(typeof(ReadResourceResult))]
Expand Down
11 changes: 7 additions & 4 deletions src/ModelContextProtocol.Core/McpSession.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ internal async ValueTask<TResult> SendRequestAsync<TParameters, TResult>(
/// <returns>A task that represents the asynchronous send operation.</returns>
/// <remarks>
/// <para>
/// This method sends a notification without any parameters. Notifications are one-way messages
/// that don't expect a response. They are commonly used for events, status updates, or to signal
/// This method sends a notification without any parameters. Notifications are one-way messages
/// that don't expect a response. They are commonly used for events, status updates, or to signal
/// changes in state.
/// </para>
/// </remarks>
Expand All @@ -102,11 +102,11 @@ public Task SendNotificationAsync(string method, CancellationToken cancellationT
/// <returns>A task that represents the asynchronous send operation.</returns>
/// <remarks>
/// <para>
/// This method sends a notification with parameters to the connected session. Notifications are one-way
/// This method sends a notification with parameters to the connected session. Notifications are one-way
/// messages that don't expect a response, commonly used for events, status updates, or signaling changes.
/// </para>
/// <para>
/// The parameters object is serialized to JSON according to the provided serializer options or the default
/// The parameters object is serialized to JSON according to the provided serializer options or the default
/// options if none are specified.
/// </para>
/// <para>
Expand Down Expand Up @@ -152,6 +152,7 @@ internal Task SendNotificationAsync<TParameters>(
/// </summary>
/// <param name="progressToken">The <see cref="ProgressToken"/> identifying the operation for which progress is being reported.</param>
/// <param name="progress">The progress update to send, containing information such as percentage complete or status message.</param>
/// <param name="options">Optional request options including metadata, serialization settings, and progress tracking.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A task representing the completion of the notification operation (not the operation being tracked).</returns>
/// <exception cref="ArgumentNullException">The current session instance is <see langword="null"/>.</exception>
Expand All @@ -168,6 +169,7 @@ internal Task SendNotificationAsync<TParameters>(
public Task NotifyProgressAsync(
ProgressToken progressToken,
ProgressNotificationValue progress,
RequestOptions? options = null,
CancellationToken cancellationToken = default)
{
return SendNotificationAsync(
Expand All @@ -176,6 +178,7 @@ public Task NotifyProgressAsync(
{
ProgressToken = progressToken,
Progress = progress,
Meta = options?.Meta,
},
McpJsonUtilities.JsonContext.Default.ProgressNotificationParams,
cancellationToken);
Expand Down
11 changes: 11 additions & 0 deletions src/ModelContextProtocol.Core/Protocol/PingRequestParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace ModelContextProtocol.Protocol;

/// <summary>
/// Represents the parameters used with a <see cref="RequestMethods.Ping"/> request to verify
/// server connectivity.
/// </summary>
/// <remarks>
/// The server responds with a <see cref="PingResult"/>.
/// See the <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/">schema</see> for details.
/// </remarks>
public sealed class PingRequestParams : RequestParams;
19 changes: 18 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/RequestParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,24 @@ private protected RequestParams()
/// Implementations must not make assumptions about its contents.
/// </remarks>
[JsonPropertyName("_meta")]
public JsonObject? Meta { get; set; }
public JsonObject? Meta {
get;
set
{
// If progressToken is already set in Meta and not present in the new value, preserve it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it help or hurt if we removed the setter from ProgressToken? Setting the PT would require setting Meta, and the ProgressToken getter would just be there as an accelerator. Presumably the 99.9% case of using the setter is in our own infrastructure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard to know if there are uses of the setter outside of the SDK. Within the SDK there wre only two places I had to change.

Even though this might break some users, I think this is a much better change as it eliminates the footgun that currently exists. I'll push this up shortly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for removing the ProgressToken setter. Having multiple property setters conditionally configuring the same fields is a recipe for confusion. With the current code, the following wouldn't reset _meta back to null even though it looks like it should.

callToolParams.Meta = new JsonObject()
{
    ["progressToken"] = "foo",
};
// The following is surprisingly a no-op. The "progressToken" stays.
callToolParams.Meta = null;

Another option would be to store ProgressToken in its own field and only merge it into the _meta object or read it from the _meta object during JSON (de)serialization using a custom converter, but the downside is that you couldn't see the real value of _meta after setting the ProgressToken, and the ProgressToken property might be null even after someone manually sets it via the Meta property.

I think I prefer just removing the ProgressToken setter. In the unlikely event that non-SDK code is broken by the setter removal, at least it should be obvious what's going on with the compile time failure.

if (field?["progressToken"] is JsonValue existingProgressToken &&
(value is null || !value.ContainsKey("progressToken")))
{
// Create a copy to avoid modifying the input parameter
field = value is null ? [] : new JsonObject(value);
field["progressToken"] = existingProgressToken;
}
else
{
field = value;
}
}
}

/// <summary>
/// Gets or sets an opaque token that will be attached to any subsequent progress notifications.
Expand Down
6 changes: 3 additions & 3 deletions src/ModelContextProtocol.Core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ foreach (var tool in await client.ListToolsAsync())
var result = await client.CallToolAsync(
"echo",
new Dictionary<string, object?>() { ["message"] = "Hello MCP!" },
cancellationToken:CancellationToken.None);
cancellationToken: CancellationToken.None);

// echo always returns one and only one text content object
Console.WriteLine(result.Content.First(c => c.Type == "text").Text);
Expand Down Expand Up @@ -83,13 +83,13 @@ using System.ComponentModel;
var serverOptions = new McpServerOptions();

// Add tools directly
serverOptions.Capabilities.Tools = new()
serverOptions.Capabilities.Tools = new()
{
ListChanged = true,
ToolCollection = [
McpServerTool.Create((string message) => $"hello {message}", new()
{
Name = "echo",
Name = "echo",
Description = "Echoes the message back to the client."
})
]
Expand Down
33 changes: 33 additions & 0 deletions src/ModelContextProtocol.Core/RequestOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using ModelContextProtocol.Protocol;

namespace ModelContextProtocol;

/// <summary>
/// Contains optional parameters for MCP requests.
/// </summary>
public sealed class RequestOptions
{
/// <summary>
/// Gets or sets optional metadata to include in the request.
/// </summary>
public JsonObject? Meta { get; set; }

/// <summary>
/// Gets or sets the JSON serializer options to use for serialization and deserialization.
/// </summary>
public JsonSerializerOptions? JsonSerializerOptions { get; set; }

/// <summary>
/// Gets or sets the progress token for tracking long-running operations.
/// </summary>
public ProgressToken? ProgressToken { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="RequestOptions"/> class.
/// </summary>
public RequestOptions()
{
}
}
Loading
Loading