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.");
}
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
SetRequestHandler(
RequestMethods.SamplingCreateMessage,
(request, cancellationToken) => samplingHandler(
request,
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)
@ -55,9 +57,11 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
}
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
SetRequestHandler(
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);
initializationCts.CancelAfter(_options.InitializationTimeout);
try
{
// Send initialize request
var initializeResponse = await SendRequestAsync<InitializeResult>(
new JsonRpcRequest
{
Method = RequestMethods.Initialize,
Params = new InitializeRequestParams()
{
ProtocolVersion = _options.ProtocolVersion,
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
ClientInfo = _options.ClientInfo
}
},
initializationCts.Token).ConfigureAwait(false);
try
{
// Send initialize request
var initializeResponse = await this.SendRequestAsync(
RequestMethods.Initialize,
new InitializeRequestParams
{
ProtocolVersion = _options.ProtocolVersion,
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
ClientInfo = _options.ClientInfo
},
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
McpJsonUtilities.JsonContext.Default.InitializeResult,
cancellationToken: initializationCts.Token).ConfigureAwait(false);
// Store server information
_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.Utils;
using ModelContextProtocol.Utils.Json;
using Microsoft.Extensions.AI;
using System.Text.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
namespace ModelContextProtocol.Client;
/// <summary>Provides extension methods for interacting with an <see cref="IMcpClient"/>.</summary>
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>
/// Sends a ping request to verify server connectivity.
/// </summary>
@ -38,34 +21,46 @@ public static class McpClientExtensions
{
Throw.IfNull(client);
return client.SendRequestAsync<dynamic>(
CreateRequest(RequestMethods.Ping, null),
cancellationToken);
return client.SendRequestAsync(
RequestMethods.Ping,
parameters: null,
McpJsonUtilities.JsonContext.Default.Object,
McpJsonUtilities.JsonContext.Default.Object,
cancellationToken: cancellationToken);
}
/// <summary>
/// Retrieves a list of available tools from the server.
/// </summary>
/// <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>
/// <returns>A list of all available tools.</returns>
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);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
List<McpClientTool>? tools = null;
string? cursor = null;
do
{
var toolResults = await client.SendRequestAsync<ListToolsResult>(
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var toolResults = await client.SendRequestAsync(
RequestMethods.ToolsList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListToolsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
tools ??= new List<McpClientTool>(toolResults.Tools.Count);
foreach (var tool in toolResults.Tools)
{
tools.Add(new McpClientTool(client, tool));
tools.Add(new McpClientTool(client, tool, serializerOptions));
}
cursor = toolResults.NextCursor;
@ -79,6 +74,7 @@ public static class McpClientExtensions
/// Creates an enumerable for asynchronously enumerating all available tools from the server.
/// </summary>
/// <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>
/// <returns>An asynchronous sequence of all available tools.</returns>
/// <remarks>
@ -86,20 +82,28 @@ public static class McpClientExtensions
/// will result in requerying the server and yielding the sequence of available tools.
/// </remarks>
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);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
string? cursor = null;
do
{
var toolResults = await client.SendRequestAsync<ListToolsResult>(
CreateRequest(RequestMethods.ToolsList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var toolResults = await client.SendRequestAsync(
RequestMethods.ToolsList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListToolsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var tool in toolResults.Tools)
{
yield return new McpClientTool(client, tool);
yield return new McpClientTool(client, tool, serializerOptions);
}
cursor = toolResults.NextCursor;
@ -122,9 +126,12 @@ public static class McpClientExtensions
string? cursor = null;
do
{
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var promptResults = await client.SendRequestAsync(
RequestMethods.PromptsList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
prompts ??= new List<McpClientPrompt>(promptResults.Prompts.Count);
foreach (var prompt in promptResults.Prompts)
@ -157,9 +164,12 @@ public static class McpClientExtensions
string? cursor = null;
do
{
var promptResults = await client.SendRequestAsync<ListPromptsResult>(
CreateRequest(RequestMethods.PromptsList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var promptResults = await client.SendRequestAsync(
RequestMethods.PromptsList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListPromptsResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var prompt in promptResults.Prompts)
{
@ -177,17 +187,29 @@ public static class McpClientExtensions
/// <param name="client">The client.</param>
/// <param name="name">The name of the prompt to retrieve</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>
/// <returns>A task containing the prompt's content and messages.</returns>
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.IfNullOrWhiteSpace(name);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
return client.SendRequestAsync<GetPromptResult>(
CreateRequest(RequestMethods.PromptsGet, CreateParametersDictionary(name, arguments)),
cancellationToken);
var parametersTypeInfo = serializerOptions.GetTypeInfo<IReadOnlyDictionary<string, object?>>();
return client.SendRequestAsync(
RequestMethods.PromptsGet,
CreateParametersDictionary(name, arguments),
parametersTypeInfo,
McpJsonUtilities.JsonContext.Default.GetPromptResult,
cancellationToken: cancellationToken);
}
/// <summary>
@ -206,9 +228,12 @@ public static class McpClientExtensions
string? cursor = null;
do
{
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var templateResults = await client.SendRequestAsync(
RequestMethods.ResourcesTemplatesList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (templates is null)
{
@ -244,9 +269,12 @@ public static class McpClientExtensions
string? cursor = null;
do
{
var templateResults = await client.SendRequestAsync<ListResourceTemplatesResult>(
CreateRequest(RequestMethods.ResourcesTemplatesList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var templateResults = await client.SendRequestAsync(
RequestMethods.ResourcesTemplatesList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var template in templateResults.ResourceTemplates)
{
@ -274,9 +302,12 @@ public static class McpClientExtensions
string? cursor = null;
do
{
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var resourceResults = await client.SendRequestAsync(
RequestMethods.ResourcesList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (resources is null)
{
@ -312,9 +343,12 @@ public static class McpClientExtensions
string? cursor = null;
do
{
var resourceResults = await client.SendRequestAsync<ListResourcesResult>(
CreateRequest(RequestMethods.ResourcesList, CreateCursorDictionary(cursor)),
cancellationToken).ConfigureAwait(false);
var resourceResults = await client.SendRequestAsync(
RequestMethods.ResourcesList,
CreateCursorDictionary(cursor),
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ListResourcesResult,
cancellationToken: cancellationToken).ConfigureAwait(false);
foreach (var resource in resourceResults.Resources)
{
@ -338,9 +372,12 @@ public static class McpClientExtensions
Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(uri);
return client.SendRequestAsync<ReadResourceResult>(
CreateRequest(RequestMethods.ResourcesRead, new Dictionary<string, object?>() { ["uri"] = uri }),
cancellationToken);
return client.SendRequestAsync(
RequestMethods.ResourcesRead,
new Dictionary<string, object?> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.ReadResourceResult,
cancellationToken: cancellationToken);
}
/// <summary>
@ -362,13 +399,16 @@ public static class McpClientExtensions
throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference));
}
return client.SendRequestAsync<CompleteResult>(
CreateRequest(RequestMethods.CompletionComplete, new Dictionary<string, object?>()
return client.SendRequestAsync(
RequestMethods.CompletionComplete,
new Dictionary<string, object?>
{
["ref"] = reference,
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
}),
cancellationToken);
},
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.CompleteResult,
cancellationToken: cancellationToken);
}
/// <summary>
@ -382,9 +422,12 @@ public static class McpClientExtensions
Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(uri);
return client.SendRequestAsync<EmptyResult>(
CreateRequest(RequestMethods.ResourcesSubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
cancellationToken);
return client.SendRequestAsync(
RequestMethods.ResourcesSubscribe,
new Dictionary<string, object?> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
}
/// <summary>
@ -398,9 +441,12 @@ public static class McpClientExtensions
Throw.IfNull(client);
Throw.IfNullOrWhiteSpace(uri);
return client.SendRequestAsync<EmptyResult>(
CreateRequest(RequestMethods.ResourcesUnsubscribe, new Dictionary<string, object?>() { ["uri"] = uri }),
cancellationToken);
return client.SendRequestAsync(
RequestMethods.ResourcesUnsubscribe,
new Dictionary<string, object?> { ["uri"] = uri },
McpJsonUtilities.JsonContext.Default.DictionaryStringObject,
McpJsonUtilities.JsonContext.Default.EmptyResult,
cancellationToken: cancellationToken);
}
/// <summary>
@ -409,17 +455,29 @@ public static class McpClientExtensions
/// <param name="client">The client.</param>
/// <param name="toolName">The name of the tool to call.</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>
/// <returns>A task containing the tool's response.</returns>
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(toolName);
serializerOptions ??= McpJsonUtilities.DefaultOptions;
serializerOptions.MakeReadOnly();
return client.SendRequestAsync<CallToolResponse>(
CreateRequest(RequestMethods.ToolsCall, CreateParametersDictionary(toolName, arguments)),
cancellationToken);
var parametersTypeInfo = serializerOptions.GetTypeInfo<IReadOnlyDictionary<string, object?>>();
return client.SendRequestAsync(
RequestMethods.ToolsCall,
CreateParametersDictionary(toolName, arguments),
parametersTypeInfo,
McpJsonUtilities.JsonContext.Default.CallToolResponse,
cancellationToken: cancellationToken);
}
/// <summary>
@ -569,18 +627,14 @@ public static class McpClientExtensions
{
Throw.IfNull(client);
return client.SendRequestAsync<EmptyResult>(
CreateRequest(RequestMethods.LoggingSetLevel, new Dictionary<string, object?>() { ["level"] = level }),
cancellationToken);
return client.SendRequestAsync(
RequestMethods.LoggingSetLevel,
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) =>
cursor != null ? new() { ["cursor"] = cursor } : null;
@ -599,36 +653,4 @@ public static class McpClientExtensions
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 System.Text.Json;
namespace ModelContextProtocol.Client;
@ -20,17 +21,19 @@ public sealed class McpClientPrompt
/// Retrieves a specific prompt with optional arguments.
/// </summary>
/// <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>
/// <returns>A task containing the prompt's content and messages.</returns>
public async ValueTask<GetPromptResult> GetAsync(
IEnumerable<KeyValuePair<string, object?>>? arguments = null,
JsonSerializerOptions? serializerOptions = null,
CancellationToken cancellationToken = default)
{
IReadOnlyDictionary<string, object?>? argDict =
arguments as IReadOnlyDictionary<string, object?> ??
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>

View file

@ -10,10 +10,11 @@ public sealed class McpClientTool : AIFunction
{
private readonly IMcpClient _client;
internal McpClientTool(IMcpClient client, Tool tool)
internal McpClientTool(IMcpClient client, Tool tool, JsonSerializerOptions serializerOptions)
{
_client = client;
ProtocolTool = tool;
JsonSerializerOptions = serializerOptions;
}
/// <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;
/// <inheritdoc/>
public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions;
public override JsonSerializerOptions JsonSerializerOptions { get; }
/// <inheritdoc/>
protected async override Task<object?> InvokeCoreAsync(
@ -39,7 +40,7 @@ public sealed class McpClientTool : AIFunction
arguments as IReadOnlyDictionary<string, object?> ??
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);
}
}

View file

@ -5,12 +5,11 @@ namespace ModelContextProtocol;
/// <summary>Represents a client or server MCP endpoint.</summary>
public interface IMcpEndpoint : IAsyncDisposable
{
/// <summary>Sends a generic JSON-RPC request to the connected endpoint.</summary>
/// <typeparam name="TResult">The expected response type.</typeparam>
/// <summary>Sends a JSON-RPC request to the connected endpoint.</summary>
/// <param name="request">The JSON-RPC request to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <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>
/// <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}")]
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}")]
internal static partial void RequestInvalidResponseType(this ILogger logger, string endpointName, string method);

View file

@ -1,13 +1,147 @@
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Utils;
using ModelContextProtocol.Utils.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol;
/// <summary>Provides extension methods for interacting with an <see cref="IMcpEndpoint"/>.</summary>
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>
/// <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="progress">The progress update to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
@ -24,11 +158,11 @@ public static class McpEndpointExtensions
return endpoint.SendMessageAsync(new JsonRpcNotification()
{
Method = NotificationMethods.ProgressNotification,
Params = new ProgressNotification()
Params = JsonSerializer.SerializeToNode(new ProgressNotification
{
ProgressToken = progressToken,
Progress = progress,
},
}, McpJsonUtilities.JsonContext.Default.ProgressNotification),
}, 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;
@ -23,5 +24,5 @@ public record JsonRpcNotification : IJsonRpcMessage
/// Optional parameters for the notification.
/// </summary>
[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;
@ -29,5 +30,5 @@ public record JsonRpcRequest : IJsonRpcMessageWithId
/// Optional parameters for the method.
/// </summary>
[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;
namespace ModelContextProtocol.Protocol.Messages;
@ -23,5 +23,5 @@ public record JsonRpcResponse : IJsonRpcMessageWithId
/// The result of the method invocation.
/// </summary>
[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 readonly ILogger _logger;
private readonly McpServerConfig _serverConfig;
private readonly JsonSerializerOptions _jsonOptions;
private readonly TaskCompletionSource<bool> _connectionEstablished;
private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})";
@ -50,7 +49,6 @@ internal sealed class SseClientSessionTransport : TransportBase
_httpClient = httpClient;
_connectionCts = new CancellationTokenSource();
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
_jsonOptions = McpJsonUtilities.DefaultOptions;
_connectionEstablished = new TaskCompletionSource<bool>();
}
@ -94,7 +92,7 @@ internal sealed class SseClientSessionTransport : TransportBase
throw new InvalidOperationException("Transport not connected");
using var content = new StringContent(
JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()),
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage),
Encoding.UTF8,
"application/json"
);
@ -127,7 +125,7 @@ internal sealed class SseClientSessionTransport : TransportBase
}
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");
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
@ -259,7 +257,7 @@ internal sealed class SseClientSessionTransport : TransportBase
try
{
var message = JsonSerializer.Deserialize(data, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage);
if (message == null)
{
_logger.TransportMessageParseUnexpectedType(EndpointName, data);

View file

@ -1,4 +1,6 @@
namespace ModelContextProtocol.Protocol.Types;
using System.Text.Json;
namespace ModelContextProtocol.Protocol.Types;
/// <summary>
/// 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.
/// </summary>
[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; }
/// <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>
[JsonPropertyName("data")]
public JsonElement? Data { get; init; }

View file

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

View file

@ -255,8 +255,8 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
cancellationToken.ThrowIfCancellationRequested();
// 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 ?
new(existingArgs) :
Dictionary<string, object?> arguments = request.Params?.Arguments is { } paramArgs ?
paramArgs.ToDictionary(entry => entry.Key, entry => entry.Value.AsObject()) :
[];
arguments[RequestContextKey] = request;

View file

@ -4,7 +4,7 @@ using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Shared;
using ModelContextProtocol.Utils;
using System.Text.Json.Nodes;
using ModelContextProtocol.Utils.Json;
namespace ModelContextProtocol.Server;
@ -131,13 +131,15 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
private void SetPingHandler()
{
SetRequestHandler<JsonNode, PingResult>(RequestMethods.Ping,
(request, _) => Task.FromResult(new PingResult()));
SetRequestHandler(RequestMethods.Ping,
(request, _) => Task.FromResult(new PingResult()),
McpJsonUtilities.JsonContext.Default.JsonNode,
McpJsonUtilities.JsonContext.Default.PingResult);
}
private void SetInitializeHandler(McpServerOptions options)
{
SetRequestHandler<InitializeRequestParams, InitializeResult>(RequestMethods.Initialize,
SetRequestHandler(RequestMethods.Initialize,
(request, _) =>
{
ClientCapabilities = request?.Capabilities ?? new();
@ -147,23 +149,27 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
_endpointName = $"{_endpointName}, Client ({ClientInfo?.Name} {ClientInfo?.Version})";
GetSessionOrThrow().EndpointName = _endpointName;
return Task.FromResult(new InitializeResult()
return Task.FromResult(new InitializeResult
{
ProtocolVersion = options.ProtocolVersion,
Instructions = options.ServerInstructions,
ServerInfo = options.ServerInfo,
Capabilities = ServerCapabilities ?? new(),
});
});
},
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
McpJsonUtilities.JsonContext.Default.InitializeResult);
}
private void SetCompletionHandler(McpServerOptions options)
{
// 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 ?
(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)
@ -184,11 +190,24 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct));
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>(RequestMethods.ResourcesRead, (request, ct) => readResourceHandler(new(this, request), ct));
SetRequestHandler(
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()));
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)
{
@ -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.");
}
SetRequestHandler<SubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct));
SetRequestHandler<UnsubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesUnsubscribe, (request, ct) => unsubscribeHandler(new(this, request), ct));
SetRequestHandler(
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)
@ -286,8 +314,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
}
}
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct));
SetRequestHandler<GetPromptRequestParams, GetPromptResult>(RequestMethods.PromptsGet, (request, ct) => getPromptHandler(new(this, request), ct));
SetRequestHandler(
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)
@ -370,8 +407,17 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
}
}
SetRequestHandler<ListToolsRequestParams, ListToolsResult>(RequestMethods.ToolsList, (request, ct) => listToolsHandler(new(this, request), ct));
SetRequestHandler<CallToolRequestParams, CallToolResponse>(RequestMethods.ToolsCall, (request, ct) => callToolHandler(new(this, request), ct));
SetRequestHandler(
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)
@ -386,6 +432,10 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
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.Utils;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Utils.Json;
using System.Runtime.CompilerServices;
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));
}
return server.SendRequestAsync<CreateMessageResult>(
new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage, Params = request },
cancellationToken);
return server.SendRequestAsync(
RequestMethods.SamplingCreateMessage,
request,
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult,
cancellationToken: cancellationToken);
}
/// <summary>
@ -164,9 +169,12 @@ public static class McpServerExtensions
throw new ArgumentException("Client connected to the server does not support roots.", nameof(server));
}
return server.SendRequestAsync<ListRootsResult>(
new JsonRpcRequest { Method = RequestMethods.RootsList, Params = request },
cancellationToken);
return server.SendRequestAsync(
RequestMethods.RootsList,
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>

View file

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

View file

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

View file

@ -11,6 +11,7 @@ using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Shared;
@ -43,7 +44,6 @@ internal sealed class McpSession : IDisposable
/// that can be used to request cancellation of the in-flight handler.
/// </summary>
private readonly ConcurrentDictionary<RequestId, CancellationTokenSource> _handlingRequests = new();
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger _logger;
private readonly string _id = Guid.NewGuid().ToString("N");
@ -81,7 +81,6 @@ internal sealed class McpSession : IDisposable
EndpointName = endpointName;
_requestHandlers = requestHandlers;
_notificationHandlers = notificationHandlers;
_jsonOptions = McpJsonUtilities.DefaultOptions;
_logger = logger ?? NullLogger.Instance;
}
@ -155,7 +154,7 @@ internal sealed class McpSession : IDisposable
}
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);
}
}
@ -295,7 +294,7 @@ internal sealed class McpSession : IDisposable
}
_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);
await _transport.SendMessageAsync(new JsonRpcResponse
{
@ -306,15 +305,14 @@ internal sealed class McpSession : IDisposable
}
/// <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.
/// Use this method for custom requests or those not yet covered explicitly by the endpoint implementation.
/// </summary>
/// <typeparam name="TResult">The expected response type.</typeparam>
/// <param name="request">The JSON-RPC request to send.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <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)
{
@ -352,7 +350,7 @@ internal sealed class McpSession : IDisposable
// Expensive logging, use the logging framework to check if the logger is enabled
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
@ -371,22 +369,9 @@ internal sealed class McpSession : IDisposable
if (response is JsonRpcResponse success)
{
// Convert the Result object to JSON and back to get our strongly-typed result
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.RequestResponseReceivedPayload(EndpointName, success.Result?.ToJsonString() ?? "null");
_logger.RequestResponseReceived(EndpointName, request.Method);
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)}");
return success;
}
// Unexpected response type
@ -435,7 +420,7 @@ internal sealed class McpSession : IDisposable
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);
@ -461,26 +446,11 @@ internal sealed class McpSession : IDisposable
}
}
private static CancelledNotification? GetCancelledNotificationParams(object? notificationParams)
private static CancelledNotification? GetCancelledNotificationParams(JsonNode? notificationParams)
{
try
{
switch (notificationParams)
{
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>());
}
return JsonSerializer.Deserialize(notificationParams, McpJsonUtilities.JsonContext.Default.CancelledNotification);
}
catch
{
@ -515,15 +485,15 @@ internal sealed class McpSession : IDisposable
{
tags.Add("rpc.jsonrpc.request_id", request.Id.ToString());
if (request.Params is JsonElement je)
if (request.Params is JsonObject paramsObj)
{
switch (request.Method)
{
case RequestMethods.ToolsCall:
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);
if (activity is not null)
{
@ -533,9 +503,9 @@ internal sealed class McpSession : IDisposable
break;
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);
if (activity is not null)
{

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