Allow JsonSerializerOptions parameters for user-defined and inputs and hardcode everything else to the source generator. (#182)

* Allow user-defined JsonSerializerOptions for user-defined and inputs and hardcode everything else to the source generator.

* Add validation for user-defined JsonSerializerOptions.

* Remove out of date comment.

* Address feedback and add tests
This commit is contained in:
Eirik Tsarpalis 2025-04-02 18:43:54 +01:00 • committed by GitHub
commit f1af251c32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 632 additions and 372 deletions

View file

@ -40,12 +40,14 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler."); throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
} }
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>( SetRequestHandler(
RequestMethods.SamplingCreateMessage, RequestMethods.SamplingCreateMessage,
(request, cancellationToken) => samplingHandler( (request, cancellationToken) => samplingHandler(
request, request,
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
cancellationToken)); cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
} }
if (options.Capabilities?.Roots is { } rootsCapability) if (options.Capabilities?.Roots is { } rootsCapability)
@ -55,9 +57,11 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler."); throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
} }
SetRequestHandler<ListRootsRequestParams, ListRootsResult>( SetRequestHandler(
RequestMethods.RootsList, RequestMethods.RootsList,
(request, cancellationToken) => rootsHandler(request, cancellationToken)); rootsHandler,
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult);
} }
} }
@ -88,21 +92,20 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
initializationCts.CancelAfter(_options.InitializationTimeout); initializationCts.CancelAfter(_options.InitializationTimeout);
try try
{ {
// Send initialize request // Send initialize request
var initializeResponse = await SendRequestAsync<InitializeResult>( var initializeResponse = await this.SendRequestAsync(
new JsonRpcRequest RequestMethods.Initialize,
{ new InitializeRequestParams
Method = RequestMethods.Initialize, {
Params = new InitializeRequestParams() ProtocolVersion = _options.ProtocolVersion,
{ Capabilities = _options.Capabilities ?? new ClientCapabilities(),
ProtocolVersion = _options.ProtocolVersion, ClientInfo = _options.ClientInfo
Capabilities = _options.Capabilities ?? new ClientCapabilities(), },
ClientInfo = _options.ClientInfo McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
} McpJsonUtilities.JsonContext.Default.InitializeResult,
}, cancellationToken: initializationCts.Token).ConfigureAwait(false);
initializationCts.Token).ConfigureAwait(false);
// Store server information // Store server information
_logger.ServerCapabilitiesReceived(EndpointName, _logger.ServerCapabilitiesReceived(EndpointName,

View file

@ -1,33 +1,16 @@
using ModelContextProtocol.Protocol.Messages; using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json; using ModelContextProtocol.Utils.Json;
using Microsoft.Extensions.AI;
using System.Text.Json;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json;
namespace ModelContextProtocol.Client; namespace ModelContextProtocol.Client;
/// <summary>Provides extension methods for interacting with an <see cref="IMcpClient"/>.</summary> /// <summary>Provides extension methods for interacting with an <see cref="IMcpClient"/>.</summary>
public static class McpClientExtensions public static class McpClientExtensions
{ {
/// <summary>
/// Sends a notification to the server with parameters.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="method">The notification method name.</param>
/// <param name="parameters">The parameters to send with the notification.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(method);
return client.SendMessageAsync(
new JsonRpcNotification { Method = method, Params = parameters },
cancellationToken);
}
/// <summary> /// <summary>
/// Sends a ping request to verify server connectivity. /// Sends a ping request to verify server connectivity.
/// </summary> /// </summary>
@ -38,34 +21,46 @@ public static class McpClientExtensions
{ {
Throw.IfNull(client); Throw.IfNull(client);
return client.SendRequestAsync<dynamic>( return client.SendRequestAsync(
CreateRequest(RequestMethods.Ping, null), RequestMethods.Ping,
cancellationToken); parameters: null,
McpJsonUtilities.JsonContext.Default.Object,
McpJsonUtilities.JsonContext.Default.Object,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
/// Retrieves a list of available tools from the server. /// Retrieves a list of available tools from the server.
/// </summary> /// </summary>
/// <param name="client">The client.</param> /// <param name="client">The client.</param>
/// <param name="serializerOptions">The serializer options governing tool parameter serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list of all available tools.</returns> /// <returns>A list of all available tools.</returns>
public static async Task<IList<McpClientTool>> ListToolsAsync( public static async Task<IList<McpClientTool>> ListToolsAsync(
this IMcpClient client, CancellationToken cancellationToken = default) this IMcpClient client,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{ {
Throw.IfNull(client); Throw.IfNull(client);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
List<McpClientTool>? tools = null; List<McpClientTool>? tools = null;
string? cursor = null; string? cursor = null;
do do
{ {
var toolResults = await client.SendRequestAsync<ListToolsResult>( var toolResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)), RequestMethods.ToolsList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListToolsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
tools ??= new List<McpClientTool>(toolResults.Tools.Count); tools ??= new List<McpClientTool>(toolResults.Tools.Count);
foreach (var tool in toolResults.Tools) foreach (var tool in toolResults.Tools)
{ {
tools.Add(new McpClientTool(client, tool)); tools.Add(new McpClientTool(client, tool, serializerOptions));
} }
cursor = toolResults.NextCursor; cursor = toolResults.NextCursor;
@ -79,6 +74,7 @@ public static class McpClientExtensions
/// Creates an enumerable for asynchronously enumerating all available tools from the server. /// Creates an enumerable for asynchronously enumerating all available tools from the server.
/// </summary> /// </summary>
/// <param name="client">The client.</param> /// <param name="client">The client.</param>
/// <param name="serializerOptions">The serializer options governing tool parameter serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>An asynchronous sequence of all available tools.</returns> /// <returns>An asynchronous sequence of all available tools.</returns>
/// <remarks> /// <remarks>
@ -86,20 +82,28 @@ public static class McpClientExtensions
/// will result in requerying the server and yielding the sequence of available tools. /// will result in requerying the server and yielding the sequence of available tools.
/// </remarks> /// </remarks>
public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync( public static async IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) this IMcpClient client,
JsonSerializerOptions? serializerOptions = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{ {
Throw.IfNull(client); Throw.IfNull(client);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
string? cursor = null; string? cursor = null;
do do
{ {
var toolResults = await client.SendRequestAsync<ListToolsResult>( var toolResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)), RequestMethods.ToolsList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListToolsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var tool in toolResults.Tools) foreach (var tool in toolResults.Tools)
{ {
yield return new McpClientTool(client, tool); yield return new McpClientTool(client, tool, serializerOptions);
} }
cursor = toolResults.NextCursor; cursor = toolResults.NextCursor;
@ -122,9 +126,12 @@ public static class McpClientExtensions
string? cursor = null; string? cursor = null;
do do
{ {
var promptResults = await client.SendRequestAsync<ListPromptsResult>( var promptResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)), RequestMethods.PromptsList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count); prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
foreach (var prompt in promptResults.Prompts) foreach (var prompt in promptResults.Prompts)
@ -157,9 +164,12 @@ public static class McpClientExtensions
string? cursor = null; string? cursor = null;
do do
{ {
var promptResults = await client.SendRequestAsync<ListPromptsResult>( var promptResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)), RequestMethods.PromptsList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var prompt in promptResults.Prompts) foreach (var prompt in promptResults.Prompts)
{ {
@ -177,17 +187,29 @@ public static class McpClientExtensions
/// <param name="client">The client.</param> /// <param name="client">The client.</param>
/// <param name="name">The name of the prompt to retrieve</param> /// <param name="name">The name of the prompt to retrieve</param>
/// <param name="arguments">Optional arguments for the prompt</param> /// <param name="arguments">Optional arguments for the prompt</param>
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the prompt's content and messages.</returns> /// <returns>A task containing the prompt's content and messages.</returns>
public static Task<GetPromptResult> GetPromptAsync( public static Task<GetPromptResult> GetPromptAsync(
this IMcpClient client, string name, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default) this IMcpClient client,
string name,
IReadOnlyDictionary<string, object?>? arguments = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{ {
Throw.IfNull(client); Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(name); Throw.IfNullOrWhiteSpace(name);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
return client.SendRequestAsync<GetPromptResult>( var parametersTypeInfo = serializerOptions.GetTypeInfo<IReadOnlyDictionary<string, object?>>();
CreateRequest(RequestMethods.PromptsGet, CreateParametersDictionary(name, arguments)),
cancellationToken); return client.SendRequestAsync(
RequestMethods.PromptsGet,
CreateParametersDictionary(name, arguments),
parametersTypeInfo,
McpJsonUtilities.JsonContext.Default.GetPromptResult,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -206,9 +228,12 @@ public static class McpClientExtensions
string? cursor = null; string? cursor = null;
do do
{ {
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>( var templateResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)), RequestMethods.ResourcesTemplatesList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (templates is null) if (templates is null)
{ {
@ -244,9 +269,12 @@ public static class McpClientExtensions
string? cursor = null; string? cursor = null;
do do
{ {
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>( var templateResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)), RequestMethods.ResourcesTemplatesList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var template in templateResults.ResourceTemplates) foreach (var template in templateResults.ResourceTemplates)
{ {
@ -274,9 +302,12 @@ public static class McpClientExtensions
string? cursor = null; string? cursor = null;
do do
{ {
var resourceResults = await client.SendRequestAsync<ListResourcesResult>( var resourceResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)), RequestMethods.ResourcesList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (resources is null) if (resources is null)
{ {
@ -312,9 +343,12 @@ public static class McpClientExtensions
string? cursor = null; string? cursor = null;
do do
{ {
var resourceResults = await client.SendRequestAsync<ListResourcesResult>( var resourceResults = await client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)), RequestMethods.ResourcesList,
cancellationToken).ConfigureAwait(false); CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var resource in resourceResults.Resources) foreach (var resource in resourceResults.Resources)
{ {
@ -338,9 +372,12 @@ public static class McpClientExtensions
Throw.IfNull(client); Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(uri); Throw.IfNullOrWhiteSpace(uri);
return client.SendRequestAsync<ReadResourceResult>( return client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesRead, new Dictionary<string, object?>() { ["uri"] = uri }), RequestMethods.ResourcesRead,
cancellationToken); new Dictionary<string, object?> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -362,13 +399,16 @@ public static class McpClientExtensions
throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference)); throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference));
} }
return client.SendRequestAsync<CompleteResult>( return client.SendRequestAsync(
CreateRequest(RequestMethods.CompletionComplete, new Dictionary<string, object?>() RequestMethods.CompletionComplete,
new Dictionary<string, object?>
{ {
["ref"] = reference, ["ref"] = reference,
["argument"] = new Argument { Name = argumentName, Value = argumentValue } ["argument"] = new Argument { Name = argumentName, Value = argumentValue }
}), },
cancellationToken); McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.CompleteResult,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -382,9 +422,12 @@ public static class McpClientExtensions
Throw.IfNull(client); Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(uri); Throw.IfNullOrWhiteSpace(uri);
return client.SendRequestAsync<EmptyResult>( return client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesSubscribe, new Dictionary<string, object?>() { ["uri"] = uri }), RequestMethods.ResourcesSubscribe,
cancellationToken); new Dictionary<string, object?> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -398,9 +441,12 @@ public static class McpClientExtensions
Throw.IfNull(client); Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(uri); Throw.IfNullOrWhiteSpace(uri);
return client.SendRequestAsync<EmptyResult>( return client.SendRequestAsync(
CreateRequest(RequestMethods.ResourcesUnsubscribe, new Dictionary<string, object?>() { ["uri"] = uri }), RequestMethods.ResourcesUnsubscribe,
cancellationToken); new Dictionary<string, object?> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -409,17 +455,29 @@ public static class McpClientExtensions
/// <param name="client">The client.</param> /// <param name="client">The client.</param>
/// <param name="toolName">The name of the tool to call.</param> /// <param name="toolName">The name of the tool to call.</param>
/// <param name="arguments">Optional arguments for the tool.</param> /// <param name="arguments">Optional arguments for the tool.</param>
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the tool's response.</returns> /// <returns>A task containing the tool's response.</returns>
public static Task<CallToolResponse> CallToolAsync( public static Task<CallToolResponse> CallToolAsync(
this IMcpClient client, string toolName, IReadOnlyDictionary<string, object?>? arguments = null, CancellationToken cancellationToken = default) this IMcpClient client,
string toolName,
IReadOnlyDictionary<string, object?>? arguments = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{ {
Throw.IfNull(client); Throw.IfNull(client);
Throw.IfNull(toolName); Throw.IfNull(toolName);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
return client.SendRequestAsync<CallToolResponse>( var parametersTypeInfo = serializerOptions.GetTypeInfo<IReadOnlyDictionary<string, object?>>();
CreateRequest(RequestMethods.ToolsCall, CreateParametersDictionary(toolName, arguments)),
cancellationToken); return client.SendRequestAsync(
RequestMethods.ToolsCall,
CreateParametersDictionary(toolName, arguments),
parametersTypeInfo,
McpJsonUtilities.JsonContext.Default.CallToolResponse,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -569,18 +627,14 @@ public static class McpClientExtensions
{ {
Throw.IfNull(client); Throw.IfNull(client);
return client.SendRequestAsync<EmptyResult>( return client.SendRequestAsync(
CreateRequest(RequestMethods.LoggingSetLevel, new Dictionary<string, object?>() { ["level"] = level }), RequestMethods.LoggingSetLevel,
cancellationToken); new Dictionary<string, object?> { ["level"] = level },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
} }
private static JsonRpcRequest CreateRequest(string method, IReadOnlyDictionary<string, object?>? parameters) =>
new()
{
Method = method,
Params = parameters
};
private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) => private static Dictionary<string, object?>? CreateCursorDictionary(string? cursor) =>
cursor != null ? new() { ["cursor"] = cursor } : null; cursor != null ? new() { ["cursor"] = cursor } : null;
@ -599,36 +653,4 @@ public static class McpClientExtensions
return parameters; return parameters;
} }
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
{
/// <inheritdoc/>
public override string Name => tool.Name;
/// <inheritdoc/>
public override string Description => tool.Description ?? string.Empty;
/// <inheritdoc/>
public override JsonElement JsonSchema => tool.InputSchema;
/// <inheritdoc/>
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions;
/// <inheritdoc/>
protected async override Task<object?> InvokeCoreAsync(
IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
{
IReadOnlyDictionary<string, object?> argDict =
arguments as IReadOnlyDictionary<string, object?> ??
#if NET
arguments.ToDictionary();
#else
arguments.ToDictionary(kv => kv.Key, kv => kv.Value);
#endif
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
}
}
} }

View file

@ -1,4 +1,5 @@
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using System.Text.Json;
namespace ModelContextProtocol.Client; namespace ModelContextProtocol.Client;
@ -20,17 +21,19 @@ public sealed class McpClientPrompt
/// Retrieves a specific prompt with optional arguments. /// Retrieves a specific prompt with optional arguments.
/// </summary> /// </summary>
/// <param name="arguments">Optional arguments for the prompt</param> /// <param name="arguments">Optional arguments for the prompt</param>
/// <param name="serializerOptions">The serialization options governing argument serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the prompt's content and messages.</returns> /// <returns>A task containing the prompt's content and messages.</returns>
public async ValueTask<GetPromptResult> GetAsync( public async ValueTask<GetPromptResult> GetAsync(
IEnumerable<KeyValuePair<string, object?>>? arguments = null, IEnumerable<KeyValuePair<string, object?>>? arguments = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
IReadOnlyDictionary<string, object?>? argDict = IReadOnlyDictionary<string, object?>? argDict =
arguments as IReadOnlyDictionary<string, object?> ?? arguments as IReadOnlyDictionary<string, object?> ??
arguments?.ToDictionary(); arguments?.ToDictionary();
return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, cancellationToken).ConfigureAwait(false); return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, serializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
} }
/// <summary>Gets the name of the prompt.</summary> /// <summary>Gets the name of the prompt.</summary>

View file

@ -10,10 +10,11 @@ public sealed class McpClientTool : AIFunction
{ {
private readonly IMcpClient _client; private readonly IMcpClient _client;
internal McpClientTool(IMcpClient client, Tool tool) internal McpClientTool(IMcpClient client, Tool tool, JsonSerializerOptions serializerOptions)
{ {
_client = client; _client = client;
ProtocolTool = tool; ProtocolTool = tool;
JsonSerializerOptions = serializerOptions;
} }
/// <summary>Gets the protocol <see cref="Tool"/> type for this instance.</summary> /// <summary>Gets the protocol <see cref="Tool"/> type for this instance.</summary>
@ -29,7 +30,7 @@ public sealed class McpClientTool : AIFunction
public override JsonElement JsonSchema => ProtocolTool.InputSchema; public override JsonElement JsonSchema => ProtocolTool.InputSchema;
/// <inheritdoc/> /// <inheritdoc/>
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions; public override JsonSerializerOptions JsonSerializerOptions { get; }
/// <inheritdoc/> /// <inheritdoc/>
protected async override Task<object?> InvokeCoreAsync( protected async override Task<object?> InvokeCoreAsync(
@ -39,7 +40,7 @@ public sealed class McpClientTool : AIFunction
arguments as IReadOnlyDictionary<string, object?> ?? arguments as IReadOnlyDictionary<string, object?> ??
arguments.ToDictionary(); arguments.ToDictionary();
CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, cancellationToken).ConfigureAwait(false); CallToolResponse result = await _client.CallToolAsync(ProtocolTool.Name, argDict, JsonSerializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse); return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse);
} }
} }

View file

@ -5,12 +5,11 @@ namespace ModelContextProtocol;
/// <summary>Represents a client or server MCP endpoint.</summary> /// <summary>Represents a client or server MCP endpoint.</summary>
public interface IMcpEndpoint : IAsyncDisposable public interface IMcpEndpoint : IAsyncDisposable
{ {
/// <summary>Sends a generic JSON-RPC request to the connected endpoint.</summary> /// <summary>Sends a JSON-RPC request to the connected endpoint.</summary>
/// <typeparam name="TResult">The expected response type.</typeparam>
/// <param name="request">The JSON-RPC request to send.</param> /// <param name="request">The JSON-RPC request to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the client's response.</returns> /// <returns>A task containing the client's response.</returns>
Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class; Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default);
/// <summary>Sends a message to the connected endpoint.</summary> /// <summary>Sends a message to the connected endpoint.</summary>
/// <param name="message">The message.</param> /// <param name="message">The message.</param>

View file

@ -77,9 +77,6 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Information, Message = "Request response received for {endpointName} with method {method}")] [LoggerMessage(Level = LogLevel.Information, Message = "Request response received for {endpointName} with method {method}")]
internal static partial void RequestResponseReceived(this ILogger logger, string endpointName, string method); internal static partial void RequestResponseReceived(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Error, Message = "Request response type conversion error for {endpointName} with method {method}: expected {expectedType}")]
internal static partial void RequestResponseTypeConversionError(this ILogger logger, string endpointName, string method, Type expectedType);
[LoggerMessage(Level = LogLevel.Error, Message = "Request invalid response type for {endpointName} with method {method}")] [LoggerMessage(Level = LogLevel.Error, Message = "Request invalid response type for {endpointName} with method {method}")]
internal static partial void RequestInvalidResponseType(this ILogger logger, string endpointName, string method); internal static partial void RequestInvalidResponseType(this ILogger logger, string endpointName, string method);

View file

@ -1,13 +1,147 @@
using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol; namespace ModelContextProtocol;
/// <summary>Provides extension methods for interacting with an <see cref="IMcpEndpoint"/>.</summary> /// <summary>Provides extension methods for interacting with an <see cref="IMcpEndpoint"/>.</summary>
public static class McpEndpointExtensions public static class McpEndpointExtensions
{ {
/// <summary>
/// Sends a JSON-RPC request and attempts to deserialize the result to <typeparamref name="TResult"/>.
/// </summary>
/// <typeparam name="TParameters">The type of the request parameters to serialize from.</typeparam>
/// <typeparam name="TResult">The type of the result to deserialize to.</typeparam>
/// <param name="endpoint">The MCP client or server instance.</param>
/// <param name="method">The JSON-RPC method name to invoke.</param>
/// <param name="parameters">Object representing the request parameters.</param>
/// <param name="requestId">The request id for the request.</param>
/// <param name="serializerOptions">The options governing request serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
public static Task<TResult> SendRequestAsync<TParameters, TResult>(
this IMcpEndpoint endpoint,
string method,
TParameters parameters,
JsonSerializerOptions? serializerOptions = null,
RequestId? requestId = null,
CancellationToken cancellationToken = default)
where TResult : notnull
{
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
JsonTypeInfo<TParameters> paramsTypeInfo = serializerOptions.GetTypeInfo<TParameters>();
JsonTypeInfo<TResult> resultTypeInfo = serializerOptions.GetTypeInfo<TResult>();
return SendRequestAsync(endpoint, method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken);
}
/// <summary>
/// Sends a JSON-RPC request and attempts to deserialize the result to <typeparamref name="TResult"/>.
/// </summary>
/// <typeparam name="TParameters">The type of the request parameters to serialize from.</typeparam>
/// <typeparam name="TResult">The type of the result to deserialize to.</typeparam>
/// <param name="endpoint">The MCP client or server instance.</param>
/// <param name="method">The JSON-RPC method name to invoke.</param>
/// <param name="parameters">Object representing the request parameters.</param>
/// <param name="parametersTypeInfo">The type information for request parameter serialization.</param>
/// <param name="resultTypeInfo">The type information for request parameter deserialization.</param>
/// <param name="requestId">The request id for the request.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the deserialized result.</returns>
internal static async Task<TResult> SendRequestAsync<TParameters, TResult>(
this IMcpEndpoint endpoint,
string method,
TParameters parameters,
JsonTypeInfo<TParameters> parametersTypeInfo,
JsonTypeInfo<TResult> resultTypeInfo,
RequestId? requestId = null,
CancellationToken cancellationToken = default)
where TResult : notnull
{
Throw.IfNull(endpoint);
Throw.IfNullOrWhiteSpace(method);
Throw.IfNull(parametersTypeInfo);
Throw.IfNull(resultTypeInfo);
JsonRpcRequest jsonRpcRequest = new()
{
Method = method,
Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo),
};
if (requestId is { } id)
{
jsonRpcRequest.Id = id;
}
JsonRpcResponse response = await endpoint.SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false);
return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response.");
}
/// <summary>
/// Sends a notification to the server with parameters.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="method">The notification method name.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SendNotificationAsync(this IMcpEndpoint client, string method, CancellationToken cancellationToken = default)
{
Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(method);
return client.SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken);
}
/// <summary>
/// Sends a notification to the server with parameters.
/// </summary>
/// <param name="endpoint">The MCP client or server instance.</param>
/// <param name="method">The JSON-RPC method name to invoke.</param>
/// <param name="parameters">Object representing the request parameters.</param>
/// <param name="serializerOptions">The options governing request serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SendNotificationAsync<TParameters>(
this IMcpEndpoint endpoint,
string method,
TParameters parameters,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
JsonTypeInfo<TParameters> parametersTypeInfo = serializerOptions.GetTypeInfo<TParameters>();
return SendNotificationAsync(endpoint, method, parameters, parametersTypeInfo, cancellationToken);
}
/// <summary>
/// Sends a notification to the server with parameters.
/// </summary>
/// <param name="endpoint">The MCP client or server instance.</param>
/// <param name="method">The JSON-RPC method name to invoke.</param>
/// <param name="parameters">Object representing the request parameters.</param>
/// <param name="parametersTypeInfo">The type information for request parameter serialization.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
internal static Task SendNotificationAsync<TParameters>(
this IMcpEndpoint endpoint,
string method,
TParameters parameters,
JsonTypeInfo<TParameters> parametersTypeInfo,
CancellationToken cancellationToken = default)
{
Throw.IfNull(endpoint);
Throw.IfNullOrWhiteSpace(method);
Throw.IfNull(parametersTypeInfo);
JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo);
return endpoint.SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken);
}
/// <summary>Notifies the connected endpoint of progress.</summary> /// <summary>Notifies the connected endpoint of progress.</summary>
/// <param name="endpoint">The endpoint issueing the notification.</param> /// <param name="endpoint">The endpoint issuing the notification.</param>
/// <param name="progressToken">The <see cref="ProgressToken"/> identifying the operation.</param> /// <param name="progressToken">The <see cref="ProgressToken"/> identifying the operation.</param>
/// <param name="progress">The progress update to send.</param> /// <param name="progress">The progress update to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
@ -24,11 +158,11 @@ public static class McpEndpointExtensions
return endpoint.SendMessageAsync(new JsonRpcNotification() return endpoint.SendMessageAsync(new JsonRpcNotification()
{ {
Method = NotificationMethods.ProgressNotification, Method = NotificationMethods.ProgressNotification,
Params = new ProgressNotification() Params = JsonSerializer.SerializeToNode(new ProgressNotification
{ {
ProgressToken = progressToken, ProgressToken = progressToken,
Progress = progress, Progress = progress,
}, }, McpJsonUtilities.JsonContext.Default.ProgressNotification),
}, cancellationToken); }, cancellationToken);
} }
} }

View file

@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Messages; namespace ModelContextProtocol.Protocol.Messages;
@ -23,5 +24,5 @@ public record JsonRpcNotification : IJsonRpcMessage
/// Optional parameters for the notification. /// Optional parameters for the notification.
/// </summary> /// </summary>
[JsonPropertyName("params")] [JsonPropertyName("params")]
public object? Params { get; init; } public JsonNode? Params { get; init; }
} }

View file

@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Messages; namespace ModelContextProtocol.Protocol.Messages;
@ -29,5 +30,5 @@ public record JsonRpcRequest : IJsonRpcMessageWithId
/// Optional parameters for the method. /// Optional parameters for the method.
/// </summary> /// </summary>
[JsonPropertyName("params")] [JsonPropertyName("params")]
public object? Params { get; init; } public JsonNode? Params { get; init; }
} }

View file

@ -1,4 +1,4 @@
 using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace ModelContextProtocol.Protocol.Messages; namespace ModelContextProtocol.Protocol.Messages;
@ -23,5 +23,5 @@ public record JsonRpcResponse : IJsonRpcMessageWithId
/// The result of the method invocation. /// The result of the method invocation.
/// </summary> /// </summary>
[JsonPropertyName("result")] [JsonPropertyName("result")]
public required object? Result { get; init; } public required JsonNode? Result { get; init; }
} }

View file

@ -24,7 +24,6 @@ internal sealed class SseClientSessionTransport : TransportBase
private Task? _receiveTask; private Task? _receiveTask;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly McpServerConfig _serverConfig; private readonly McpServerConfig _serverConfig;
private readonly JsonSerializerOptions _jsonOptions;
private readonly TaskCompletionSource<bool> _connectionEstablished; private readonly TaskCompletionSource<bool> _connectionEstablished;
private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})"; private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})";
@ -50,7 +49,6 @@ internal sealed class SseClientSessionTransport : TransportBase
_httpClient = httpClient; _httpClient = httpClient;
_connectionCts = new CancellationTokenSource(); _connectionCts = new CancellationTokenSource();
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance; _logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
_jsonOptions = McpJsonUtilities.DefaultOptions;
_connectionEstablished = new TaskCompletionSource<bool>(); _connectionEstablished = new TaskCompletionSource<bool>();
} }
@ -94,7 +92,7 @@ internal sealed class SseClientSessionTransport : TransportBase
throw new InvalidOperationException("Transport not connected"); throw new InvalidOperationException("Transport not connected");
using var content = new StringContent( using var content = new StringContent(
JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()), JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage),
Encoding.UTF8, Encoding.UTF8,
"application/json" "application/json"
); );
@ -127,7 +125,7 @@ internal sealed class SseClientSessionTransport : TransportBase
} }
else else
{ {
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, _jsonOptions.GetTypeInfo<JsonRpcResponse>()) ?? JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, McpJsonUtilities.JsonContext.Default.JsonRpcResponse) ??
throw new McpTransportException("Failed to initialize client"); throw new McpTransportException("Failed to initialize client");
_logger.TransportReceivedMessageParsed(EndpointName, messageId); _logger.TransportReceivedMessageParsed(EndpointName, messageId);
@ -259,7 +257,7 @@ internal sealed class SseClientSessionTransport : TransportBase
try try
{ {
var message = JsonSerializer.Deserialize(data, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()); var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
if (message == null) if (message == null)
{ {
_logger.TransportMessageParseUnexpectedType(EndpointName, data); _logger.TransportMessageParseUnexpectedType(EndpointName, data);

View file

@ -1,4 +1,6 @@
namespace ModelContextProtocol.Protocol.Types; using System.Text.Json;
namespace ModelContextProtocol.Protocol.Types;
/// <summary> /// <summary>
/// Used by the client to invoke a tool provided by the server. /// Used by the client to invoke a tool provided by the server.
@ -16,5 +18,5 @@ public class CallToolRequestParams : RequestParams
/// Optional arguments to pass to the tool. /// Optional arguments to pass to the tool.
/// </summary> /// </summary>
[System.Text.Json.Serialization.JsonPropertyName("arguments")] [System.Text.Json.Serialization.JsonPropertyName("arguments")]
public Dictionary<string,object>? Arguments { get; init; } public Dictionary<string, JsonElement>? Arguments { get; init; }
} }

View file

@ -24,7 +24,7 @@ public class LoggingMessageNotificationParams
public string? Logger { get; init; } public string? Logger { get; init; }
/// <summary> /// <summary>
/// The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. /// The data to be logged, such as a string message or an object.
/// </summary> /// </summary>
[JsonPropertyName("data")] [JsonPropertyName("data")]
public JsonElement? Data { get; init; } public JsonElement? Data { get; init; }

View file

@ -61,6 +61,8 @@ public abstract class ResourceContents
} }
string? propertyName = reader.GetString(); string? propertyName = reader.GetString();
bool success = reader.Read();
Debug.Assert(success, "STJ must have buffered the entire object for us.");
switch (propertyName) switch (propertyName)
{ {

View file

@ -255,8 +255,8 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context. // TODO: Once we shift to the real AIFunctionFactory, the request should be passed via AIFunctionArguments.Context.
Dictionary<string, object?> arguments = request.Params?.Arguments is IDictionary<string, object?> existingArgs ? Dictionary<string, object?> arguments = request.Params?.Arguments is { } paramArgs ?
new(existingArgs) : paramArgs.ToDictionary(entry => entry.Key, entry => entry.Value.AsObject()) :
[]; [];
arguments[RequestContextKey] = request; arguments[RequestContextKey] = request;

View file

@ -4,7 +4,7 @@ using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Shared; using ModelContextProtocol.Shared;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using System.Text.Json.Nodes; using ModelContextProtocol.Utils.Json;
namespace ModelContextProtocol.Server; namespace ModelContextProtocol.Server;
@ -131,13 +131,15 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
private void SetPingHandler() private void SetPingHandler()
{ {
SetRequestHandler<JsonNode, PingResult>(RequestMethods.Ping, SetRequestHandler(RequestMethods.Ping,
(request, _) => Task.FromResult(new PingResult())); (request, _) => Task.FromResult(new PingResult()),
McpJsonUtilities.JsonContext.Default.JsonNode,
McpJsonUtilities.JsonContext.Default.PingResult);
} }
private void SetInitializeHandler(McpServerOptions options) private void SetInitializeHandler(McpServerOptions options)
{ {
SetRequestHandler<InitializeRequestParams, InitializeResult>(RequestMethods.Initialize, SetRequestHandler(RequestMethods.Initialize,
(request, _) => (request, _) =>
{ {
ClientCapabilities = request?.Capabilities ?? new(); ClientCapabilities = request?.Capabilities ?? new();
@ -147,23 +149,27 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
_endpointName = $"{_endpointName}, Client ({ClientInfo?.Name} {ClientInfo?.Version})"; _endpointName = $"{_endpointName}, Client ({ClientInfo?.Name} {ClientInfo?.Version})";
GetSessionOrThrow().EndpointName = _endpointName; GetSessionOrThrow().EndpointName = _endpointName;
return Task.FromResult(new InitializeResult() return Task.FromResult(new InitializeResult
{ {
ProtocolVersion = options.ProtocolVersion, ProtocolVersion = options.ProtocolVersion,
Instructions = options.ServerInstructions, Instructions = options.ServerInstructions,
ServerInfo = options.ServerInfo, ServerInfo = options.ServerInfo,
Capabilities = ServerCapabilities ?? new(), Capabilities = ServerCapabilities ?? new(),
}); });
}); },
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
McpJsonUtilities.JsonContext.Default.InitializeResult);
} }
private void SetCompletionHandler(McpServerOptions options) private void SetCompletionHandler(McpServerOptions options)
{ {
// This capability is not optional, so return an empty result if there is no handler. // This capability is not optional, so return an empty result if there is no handler.
SetRequestHandler<CompleteRequestParams, CompleteResult>(RequestMethods.CompletionComplete, SetRequestHandler(RequestMethods.CompletionComplete,
options.GetCompletionHandler is { } handler ? options.GetCompletionHandler is { } handler ?
(request, ct) => handler(new(this, request), ct) : (request, ct) => handler(new(this, request), ct) :
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } })); (request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }),
McpJsonUtilities.JsonContext.Default.CompleteRequestParams,
McpJsonUtilities.JsonContext.Default.CompleteResult);
} }
private void SetResourcesHandler(McpServerOptions options) private void SetResourcesHandler(McpServerOptions options)
@ -184,11 +190,24 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult())); listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct)); SetRequestHandler(
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>(RequestMethods.ResourcesRead, (request, ct) => readResourceHandler(new(this, request), ct)); RequestMethods.ResourcesList,
(request, ct) => listResourcesHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourcesResult);
SetRequestHandler(
RequestMethods.ResourcesRead,
(request, ct) => readResourceHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams,
McpJsonUtilities.JsonContext.Default.ReadResourceResult);
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult())); listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>(RequestMethods.ResourcesTemplatesList, (request, ct) => listResourceTemplatesHandler(new(this, request), ct)); SetRequestHandler(
RequestMethods.ResourcesTemplatesList,
(request, ct) => listResourceTemplatesHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult);
if (resourcesCapability.Subscribe is not true) if (resourcesCapability.Subscribe is not true)
{ {
@ -202,8 +221,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
throw new McpServerException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified."); throw new McpServerException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
} }
SetRequestHandler<SubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct)); SetRequestHandler(
SetRequestHandler<UnsubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesUnsubscribe, (request, ct) => unsubscribeHandler(new(this, request), ct)); RequestMethods.ResourcesSubscribe,
(request, ct) => subscribeHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.SubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
SetRequestHandler(
RequestMethods.ResourcesUnsubscribe,
(request, ct) => unsubscribeHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
} }
private void SetPromptsHandler(McpServerOptions options) private void SetPromptsHandler(McpServerOptions options)
@ -286,8 +314,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
} }
} }
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct)); SetRequestHandler(
SetRequestHandler<GetPromptRequestParams, GetPromptResult>(RequestMethods.PromptsGet, (request, ct) => getPromptHandler(new(this, request), ct)); RequestMethods.PromptsList,
(request, ct) => listPromptsHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams,
McpJsonUtilities.JsonContext.Default.ListPromptsResult);
SetRequestHandler(
RequestMethods.PromptsGet,
(request, ct) => getPromptHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.GetPromptRequestParams,
McpJsonUtilities.JsonContext.Default.GetPromptResult);
} }
private void SetToolsHandler(McpServerOptions options) private void SetToolsHandler(McpServerOptions options)
@ -370,8 +407,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
} }
} }
SetRequestHandler<ListToolsRequestParams, ListToolsResult>(RequestMethods.ToolsList, (request, ct) => listToolsHandler(new(this, request), ct)); SetRequestHandler(
SetRequestHandler<CallToolRequestParams, CallToolResponse>(RequestMethods.ToolsCall, (request, ct) => callToolHandler(new(this, request), ct)); RequestMethods.ToolsList,
(request, ct) => listToolsHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.ListToolsRequestParams,
McpJsonUtilities.JsonContext.Default.ListToolsResult);
SetRequestHandler(
RequestMethods.ToolsCall,
(request, ct) => callToolHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.CallToolRequestParams,
McpJsonUtilities.JsonContext.Default.CallToolResponse);
} }
private void SetSetLoggingLevelHandler(McpServerOptions options) private void SetSetLoggingLevelHandler(McpServerOptions options)
@ -386,6 +432,10 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified."); throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
} }
SetRequestHandler<SetLevelRequestParams, EmptyResult>(RequestMethods.LoggingSetLevel, (request, ct) => setLoggingLevelHandler(new(this, request), ct)); SetRequestHandler(
RequestMethods.LoggingSetLevel,
(request, ct) => setLoggingLevelHandler(new(this, request), ct),
McpJsonUtilities.JsonContext.Default.SetLevelRequestParams,
McpJsonUtilities.JsonContext.Default.EmptyResult);
} }
} }

View file

@ -1,7 +1,9 @@
using ModelContextProtocol.Protocol.Messages; using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using Microsoft.Extensions.AI; using ModelContextProtocol.Utils.Json;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
@ -25,9 +27,12 @@ public static class McpServerExtensions
throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server)); throw new ArgumentException("Client connected to the server does not support sampling.", nameof(server));
} }
return server.SendRequestAsync<CreateMessageResult>( return server.SendRequestAsync(
new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage, Params = request }, RequestMethods.SamplingCreateMessage,
cancellationToken); request,
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
cancellationToken: cancellationToken);
} }
/// <summary> /// <summary>
@ -164,9 +169,12 @@ public static class McpServerExtensions
throw new ArgumentException("Client connected to the server does not support roots.", nameof(server)); throw new ArgumentException("Client connected to the server does not support roots.", nameof(server));
} }
return server.SendRequestAsync<ListRootsResult>( return server.SendRequestAsync(
new JsonRpcRequest { Method = RequestMethods.RootsList, Params = request }, RequestMethods.RootsList,
cancellationToken); request,
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult,
cancellationToken: cancellationToken);
} }
/// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary> /// <summary>Provides an <see cref="IChatClient"/> implementation that's implemented via client sampling.</summary>

View file

@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
@ -476,7 +477,7 @@ internal static partial class TemporaryAIFunctionFactory
description: parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description, description: parameter.GetCustomAttribute<DescriptionAttribute>(inherit: true)?.Description,
hasDefaultValue: parameter.HasDefaultValue, hasDefaultValue: parameter.HasDefaultValue,
defaultValue: parameter.HasDefaultValue ? parameter.DefaultValue : null, defaultValue: parameter.HasDefaultValue ? parameter.DefaultValue : null,
serializerOptions), AIJsonUtilities.DefaultOptions.GetTypeInfo<JsonElement>()); serializerOptions), McpJsonUtilities.JsonContext.Default.JsonElement);
parameterSchemas.Add(parameter.Name, parameterSchema); parameterSchemas.Add(parameter.Name, parameterSchema);
if (!parameter.IsOptional) if (!parameter.IsOptional)

View file

@ -6,6 +6,7 @@ using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Shared; namespace ModelContextProtocol.Shared;
@ -38,14 +39,19 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
} }
protected void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest?, CancellationToken, Task<TResponse>> handler) protected void SetRequestHandler<TRequest, TResponse>(
=> _requestHandlers.Set(method, handler); string method,
Func<TRequest?, CancellationToken, Task<TResponse>> handler,
JsonTypeInfo<TRequest> requestTypeInfo,
JsonTypeInfo<TResponse> responseTypeInfo)
=> _requestHandlers.Set(method, handler, requestTypeInfo, responseTypeInfo);
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler) public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler)
=> _notificationHandlers.Add(method, handler); => _notificationHandlers.Add(method, handler);
public Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default)
=> GetSessionOrThrow().SendRequestAsync<TResult>(request, cancellationToken); => GetSessionOrThrow().SendRequestAsync(request, cancellationToken);
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
=> GetSessionOrThrow().SendMessageAsync(message, cancellationToken); => GetSessionOrThrow().SendMessageAsync(message, cancellationToken);

View file

@ -11,6 +11,7 @@ using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.Metrics; using System.Diagnostics.Metrics;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Shared; namespace ModelContextProtocol.Shared;
@ -43,7 +44,6 @@ internal sealed class McpSession : IDisposable
/// that can be used to request cancellation of the in-flight handler. /// that can be used to request cancellation of the in-flight handler.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<RequestId, CancellationTokenSource> _handlingRequests = new(); private readonly ConcurrentDictionary<RequestId, CancellationTokenSource> _handlingRequests = new();
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly string _id = Guid.NewGuid().ToString("N"); private readonly string _id = Guid.NewGuid().ToString("N");
@ -81,7 +81,6 @@ internal sealed class McpSession : IDisposable
EndpointName = endpointName; EndpointName = endpointName;
_requestHandlers = requestHandlers; _requestHandlers = requestHandlers;
_notificationHandlers = notificationHandlers; _notificationHandlers = notificationHandlers;
_jsonOptions = McpJsonUtilities.DefaultOptions;
_logger = logger ?? NullLogger.Instance; _logger = logger ?? NullLogger.Instance;
} }
@ -155,7 +154,7 @@ internal sealed class McpSession : IDisposable
} }
else if (ex is not OperationCanceledException) else if (ex is not OperationCanceledException)
{ {
var payload = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()); var payload = JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
_logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex); _logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex);
} }
} }
@ -295,7 +294,7 @@ internal sealed class McpSession : IDisposable
} }
_logger.RequestHandlerCalled(EndpointName, request.Method); _logger.RequestHandlerCalled(EndpointName, request.Method);
var result = await handler(request, cancellationToken).ConfigureAwait(false); JsonNode? result = await handler(request, cancellationToken).ConfigureAwait(false);
_logger.RequestHandlerCompleted(EndpointName, request.Method); _logger.RequestHandlerCompleted(EndpointName, request.Method);
await _transport.SendMessageAsync(new JsonRpcResponse await _transport.SendMessageAsync(new JsonRpcResponse
{ {
@ -306,15 +305,14 @@ internal sealed class McpSession : IDisposable
} }
/// <summary> /// <summary>
/// Sends a generic JSON-RPC request to the server. /// Sends a JSON-RPC request to the server.
/// It is strongly recommended use the capability-specific methods instead of this one. /// It is strongly recommended use the capability-specific methods instead of this one.
/// Use this method for custom requests or those not yet covered explicitly by the endpoint implementation. /// Use this method for custom requests or those not yet covered explicitly by the endpoint implementation.
/// </summary> /// </summary>
/// <typeparam name="TResult">The expected response type.</typeparam>
/// <param name="request">The JSON-RPC request to send.</param> /// <param name="request">The JSON-RPC request to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task containing the server's response.</returns> /// <returns>A task containing the server's response.</returns>
public async Task<TResult> SendRequestAsync<TResult>(JsonRpcRequest request, CancellationToken cancellationToken) where TResult : class public async Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken)
{ {
if (!_transport.IsConnected) if (!_transport.IsConnected)
{ {
@ -352,7 +350,7 @@ internal sealed class McpSession : IDisposable
// Expensive logging, use the logging framework to check if the logger is enabled // Expensive logging, use the logging framework to check if the logger is enabled
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
_logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request, _jsonOptions.GetTypeInfo<JsonRpcRequest>())); _logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request, McpJsonUtilities.JsonContext.Default.JsonRpcRequest));
} }
// Less expensive information logging // Less expensive information logging
@ -371,22 +369,9 @@ internal sealed class McpSession : IDisposable
if (response is JsonRpcResponse success) if (response is JsonRpcResponse success)
{ {
// Convert the Result object to JSON and back to get our strongly-typed result _logger.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null");
var resultJson = JsonSerializer.Serialize(success.Result, _jsonOptions.GetTypeInfo<object?>());
var resultObject = JsonSerializer.Deserialize(resultJson, _jsonOptions.GetTypeInfo<TResult>());
// Not expensive logging because we're already converting to JSON in order to get the result object
_logger.RequestResponseReceivedPayload(EndpointName, resultJson);
_logger.RequestResponseReceived(EndpointName, request.Method); _logger.RequestResponseReceived(EndpointName, request.Method);
return success;
if (resultObject != null)
{
return resultObject;
}
// Result object was null, this is unexpected
_logger.RequestResponseTypeConversionError(EndpointName, request.Method, typeof(TResult));
throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result, _jsonOptions.GetTypeInfo<TResult>())}, expected {typeof(TResult)}");
} }
// Unexpected response type // Unexpected response type
@ -435,7 +420,7 @@ internal sealed class McpSession : IDisposable
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
_logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>())); _logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage));
} }
await _transport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false); await _transport.SendMessageAsync(message, cancellationToken).ConfigureAwait(false);
@ -461,26 +446,11 @@ internal sealed class McpSession : IDisposable
} }
} }
private static CancelledNotification? GetCancelledNotificationParams(object? notificationParams) private static CancelledNotification? GetCancelledNotificationParams(JsonNode? notificationParams)
{ {
try try
{ {
switch (notificationParams) return JsonSerializer.Deserialize(notificationParams, McpJsonUtilities.JsonContext.Default.CancelledNotification);
{
case null:
return null;
case CancelledNotification cn:
return cn;
case JsonElement je:
return JsonSerializer.Deserialize(je, McpJsonUtilities.DefaultOptions.GetTypeInfo<CancelledNotification>());
default:
return JsonSerializer.Deserialize(
JsonSerializer.Serialize(notificationParams, McpJsonUtilities.DefaultOptions.GetTypeInfo<object?>()),
McpJsonUtilities.DefaultOptions.GetTypeInfo<CancelledNotification>());
}
} }
catch catch
{ {
@ -515,15 +485,15 @@ internal sealed class McpSession : IDisposable
{ {
tags.Add("rpc.jsonrpc.request_id", request.Id.ToString()); tags.Add("rpc.jsonrpc.request_id", request.Id.ToString());
if (request.Params is JsonElement je) if (request.Params is JsonObject paramsObj)
{ {
switch (request.Method) switch (request.Method)
{ {
case RequestMethods.ToolsCall: case RequestMethods.ToolsCall:
case RequestMethods.PromptsGet: case RequestMethods.PromptsGet:
if (je.TryGetProperty("name", out var prop) && prop.ValueKind == JsonValueKind.String) if (paramsObj.TryGetPropertyValue("name", out var prop) && prop?.GetValueKind() is JsonValueKind.String)
{ {
string name = prop.GetString()!; string name = prop.GetValue<string>();
tags.Add("mcp.request.params.name", name); tags.Add("mcp.request.params.name", name);
if (activity is not null) if (activity is not null)
{ {
@ -533,9 +503,9 @@ internal sealed class McpSession : IDisposable
break; break;
case RequestMethods.ResourcesRead: case RequestMethods.ResourcesRead:
if (je.TryGetProperty("uri", out prop) && prop.ValueKind == JsonValueKind.String) if (paramsObj.TryGetPropertyValue("uri", out prop) && prop?.GetValueKind() is JsonValueKind.String)
{ {
string uri = prop.GetString()!; string uri = prop.GetValue<string>();
tags.Add("mcp.request.params.uri", uri); tags.Add("mcp.request.params.uri", uri);
if (activity is not null) if (activity is not null)
{ {

Some files were not shown because too many files have changed in this diff Show more