diff --git a/src/ModelContextProtocol/Client/McpClient.cs b/src/ModelContextProtocol/Client/McpClient.cs index 2b35e51..d871af1 100644 --- a/src/ModelContextProtocol/Client/McpClient.cs +++ b/src/ModelContextProtocol/Client/McpClient.cs @@ -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( + 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( + 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( - 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, diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs index 4d14117..63af1cf 100644 --- a/src/ModelContextProtocol/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs @@ -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; /// Provides extension methods for interacting with an . public static class McpClientExtensions { - /// - /// Sends a notification to the server with parameters. - /// - /// The client. - /// The notification method name. - /// The parameters to send with the notification. - /// A token to cancel the operation. - 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); - } - /// /// Sends a ping request to verify server connectivity. /// @@ -38,34 +21,46 @@ public static class McpClientExtensions { Throw.IfNull(client); - return client.SendRequestAsync( - CreateRequest(RequestMethods.Ping, null), - cancellationToken); + return client.SendRequestAsync( + RequestMethods.Ping, + parameters: null, + McpJsonUtilities.JsonContext.Default.Object, + McpJsonUtilities.JsonContext.Default.Object, + cancellationToken: cancellationToken); } /// /// Retrieves a list of available tools from the server. /// /// The client. + /// The serializer options governing tool parameter serialization. /// A token to cancel the operation. /// A list of all available tools. public static async Task> 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? tools = null; string? cursor = null; do { - var toolResults = await client.SendRequestAsync( - 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(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. /// /// The client. + /// The serializer options governing tool parameter serialization. /// A token to cancel the operation. /// An asynchronous sequence of all available tools. /// @@ -86,20 +82,28 @@ public static class McpClientExtensions /// will result in requerying the server and yielding the sequence of available tools. /// public static async IAsyncEnumerable 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( - 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( - 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(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( - 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 /// The client. /// The name of the prompt to retrieve /// Optional arguments for the prompt + /// The serialization options governing argument serialization. /// A token to cancel the operation. /// A task containing the prompt's content and messages. public static Task GetPromptAsync( - this IMcpClient client, string name, IReadOnlyDictionary? arguments = null, CancellationToken cancellationToken = default) + this IMcpClient client, + string name, + IReadOnlyDictionary? arguments = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(name); + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); - return client.SendRequestAsync( - CreateRequest(RequestMethods.PromptsGet, CreateParametersDictionary(name, arguments)), - cancellationToken); + var parametersTypeInfo = serializerOptions.GetTypeInfo>(); + + return client.SendRequestAsync( + RequestMethods.PromptsGet, + CreateParametersDictionary(name, arguments), + parametersTypeInfo, + McpJsonUtilities.JsonContext.Default.GetPromptResult, + cancellationToken: cancellationToken); } /// @@ -206,9 +228,12 @@ public static class McpClientExtensions string? cursor = null; do { - var templateResults = await client.SendRequestAsync( - 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( - 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( - 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( - 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( - CreateRequest(RequestMethods.ResourcesRead, new Dictionary() { ["uri"] = uri }), - cancellationToken); + return client.SendRequestAsync( + RequestMethods.ResourcesRead, + new Dictionary { ["uri"] = uri }, + McpJsonUtilities.JsonContext.Default.DictionaryStringObject, + McpJsonUtilities.JsonContext.Default.ReadResourceResult, + cancellationToken: cancellationToken); } /// @@ -362,13 +399,16 @@ public static class McpClientExtensions throw new ArgumentException($"Invalid reference: {validationMessage}", nameof(reference)); } - return client.SendRequestAsync( - CreateRequest(RequestMethods.CompletionComplete, new Dictionary() + return client.SendRequestAsync( + RequestMethods.CompletionComplete, + new Dictionary { ["ref"] = reference, ["argument"] = new Argument { Name = argumentName, Value = argumentValue } - }), - cancellationToken); + }, + McpJsonUtilities.JsonContext.Default.DictionaryStringObject, + McpJsonUtilities.JsonContext.Default.CompleteResult, + cancellationToken: cancellationToken); } /// @@ -382,9 +422,12 @@ public static class McpClientExtensions Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); - return client.SendRequestAsync( - CreateRequest(RequestMethods.ResourcesSubscribe, new Dictionary() { ["uri"] = uri }), - cancellationToken); + return client.SendRequestAsync( + RequestMethods.ResourcesSubscribe, + new Dictionary { ["uri"] = uri }, + McpJsonUtilities.JsonContext.Default.DictionaryStringObject, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken); } /// @@ -398,9 +441,12 @@ public static class McpClientExtensions Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); - return client.SendRequestAsync( - CreateRequest(RequestMethods.ResourcesUnsubscribe, new Dictionary() { ["uri"] = uri }), - cancellationToken); + return client.SendRequestAsync( + RequestMethods.ResourcesUnsubscribe, + new Dictionary { ["uri"] = uri }, + McpJsonUtilities.JsonContext.Default.DictionaryStringObject, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken); } /// @@ -409,17 +455,29 @@ public static class McpClientExtensions /// The client. /// The name of the tool to call. /// Optional arguments for the tool. + /// The serialization options governing argument serialization. /// A token to cancel the operation. /// A task containing the tool's response. public static Task CallToolAsync( - this IMcpClient client, string toolName, IReadOnlyDictionary? arguments = null, CancellationToken cancellationToken = default) + this IMcpClient client, + string toolName, + IReadOnlyDictionary? arguments = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(toolName); + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); - return client.SendRequestAsync( - CreateRequest(RequestMethods.ToolsCall, CreateParametersDictionary(toolName, arguments)), - cancellationToken); + var parametersTypeInfo = serializerOptions.GetTypeInfo>(); + + return client.SendRequestAsync( + RequestMethods.ToolsCall, + CreateParametersDictionary(toolName, arguments), + parametersTypeInfo, + McpJsonUtilities.JsonContext.Default.CallToolResponse, + cancellationToken: cancellationToken); } /// @@ -569,18 +627,14 @@ public static class McpClientExtensions { Throw.IfNull(client); - return client.SendRequestAsync( - CreateRequest(RequestMethods.LoggingSetLevel, new Dictionary() { ["level"] = level }), - cancellationToken); + return client.SendRequestAsync( + RequestMethods.LoggingSetLevel, + new Dictionary { ["level"] = level }, + McpJsonUtilities.JsonContext.Default.DictionaryStringObject, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken); } - private static JsonRpcRequest CreateRequest(string method, IReadOnlyDictionary? parameters) => - new() - { - Method = method, - Params = parameters - }; - private static Dictionary? CreateCursorDictionary(string? cursor) => cursor != null ? new() { ["cursor"] = cursor } : null; @@ -599,36 +653,4 @@ public static class McpClientExtensions return parameters; } - - /// Provides an AI function that calls a tool through . - private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction - { - /// - public override string Name => tool.Name; - - /// - public override string Description => tool.Description ?? string.Empty; - - /// - public override JsonElement JsonSchema => tool.InputSchema; - - /// - public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions; - - /// - protected async override Task InvokeCoreAsync( - IEnumerable> arguments, CancellationToken cancellationToken) - { - IReadOnlyDictionary argDict = - arguments as IReadOnlyDictionary ?? -#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); - } - } } \ No newline at end of file diff --git a/src/ModelContextProtocol/Client/McpClientPrompt.cs b/src/ModelContextProtocol/Client/McpClientPrompt.cs index 8deed8e..71d6a4e 100644 --- a/src/ModelContextProtocol/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol/Client/McpClientPrompt.cs @@ -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. /// /// Optional arguments for the prompt + /// The serialization options governing argument serialization. /// A token to cancel the operation. /// A task containing the prompt's content and messages. public async ValueTask GetAsync( IEnumerable>? arguments = null, + JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) { IReadOnlyDictionary? argDict = arguments as IReadOnlyDictionary ?? arguments?.ToDictionary(); - return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, cancellationToken).ConfigureAwait(false); + return await _client.GetPromptAsync(ProtocolPrompt.Name, argDict, serializerOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } /// Gets the name of the prompt. diff --git a/src/ModelContextProtocol/Client/McpClientTool.cs b/src/ModelContextProtocol/Client/McpClientTool.cs index 10ccd81..1fdc3a5 100644 --- a/src/ModelContextProtocol/Client/McpClientTool.cs +++ b/src/ModelContextProtocol/Client/McpClientTool.cs @@ -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; } /// Gets the protocol type for this instance. @@ -29,7 +30,7 @@ public sealed class McpClientTool : AIFunction public override JsonElement JsonSchema => ProtocolTool.InputSchema; /// - public override JsonSerializerOptions JsonSerializerOptions => McpJsonUtilities.DefaultOptions; + public override JsonSerializerOptions JsonSerializerOptions { get; } /// protected async override Task InvokeCoreAsync( @@ -39,7 +40,7 @@ public sealed class McpClientTool : AIFunction arguments as IReadOnlyDictionary ?? 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); } } \ No newline at end of file diff --git a/src/ModelContextProtocol/IMcpEndpoint.cs b/src/ModelContextProtocol/IMcpEndpoint.cs index d7dbd21..6643e02 100644 --- a/src/ModelContextProtocol/IMcpEndpoint.cs +++ b/src/ModelContextProtocol/IMcpEndpoint.cs @@ -5,12 +5,11 @@ namespace ModelContextProtocol; /// Represents a client or server MCP endpoint. public interface IMcpEndpoint : IAsyncDisposable { - /// Sends a generic JSON-RPC request to the connected endpoint. - /// The expected response type. + /// Sends a JSON-RPC request to the connected endpoint. /// The JSON-RPC request to send. /// A token to cancel the operation. /// A task containing the client's response. - Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class; + Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default); /// Sends a message to the connected endpoint. /// The message. diff --git a/src/ModelContextProtocol/Logging/Log.cs b/src/ModelContextProtocol/Logging/Log.cs index aa9b209..9b5d44b 100644 --- a/src/ModelContextProtocol/Logging/Log.cs +++ b/src/ModelContextProtocol/Logging/Log.cs @@ -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); diff --git a/src/ModelContextProtocol/McpEndpointExtensions.cs b/src/ModelContextProtocol/McpEndpointExtensions.cs index c27ada2..d2a6a95 100644 --- a/src/ModelContextProtocol/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol/McpEndpointExtensions.cs @@ -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; /// Provides extension methods for interacting with an . public static class McpEndpointExtensions { + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The MCP client or server instance. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The request id for the request. + /// The options governing request serialization. + /// A token to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + public static Task SendRequestAsync( + 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 paramsTypeInfo = serializerOptions.GetTypeInfo(); + JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); + return SendRequestAsync(endpoint, method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); + } + + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The MCP client or server instance. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The type information for request parameter serialization. + /// The type information for request parameter deserialization. + /// The request id for the request. + /// A token to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + internal static async Task SendRequestAsync( + this IMcpEndpoint endpoint, + string method, + TParameters parameters, + JsonTypeInfo parametersTypeInfo, + JsonTypeInfo 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."); + } + + /// + /// Sends a notification to the server with parameters. + /// + /// The client. + /// The notification method name. + /// A token to cancel the operation. + 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); + } + + /// + /// Sends a notification to the server with parameters. + /// + /// The MCP client or server instance. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The options governing request serialization. + /// A token to cancel the operation. + public static Task SendNotificationAsync( + this IMcpEndpoint endpoint, + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + JsonTypeInfo parametersTypeInfo = serializerOptions.GetTypeInfo(); + return SendNotificationAsync(endpoint, method, parameters, parametersTypeInfo, cancellationToken); + } + + /// + /// Sends a notification to the server with parameters. + /// + /// The MCP client or server instance. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The type information for request parameter serialization. + /// A token to cancel the operation. + internal static Task SendNotificationAsync( + this IMcpEndpoint endpoint, + string method, + TParameters parameters, + JsonTypeInfo 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); + } + /// Notifies the connected endpoint of progress. - /// The endpoint issueing the notification. + /// The endpoint issuing the notification. /// The identifying the operation. /// The progress update to send. /// A token to cancel the operation. @@ -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); } } diff --git a/src/ModelContextProtocol/Protocol/Messages/JsonRpcNotification.cs b/src/ModelContextProtocol/Protocol/Messages/JsonRpcNotification.cs index 3597737..302a463 100644 --- a/src/ModelContextProtocol/Protocol/Messages/JsonRpcNotification.cs +++ b/src/ModelContextProtocol/Protocol/Messages/JsonRpcNotification.cs @@ -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. /// [JsonPropertyName("params")] - public object? Params { get; init; } + public JsonNode? Params { get; init; } } diff --git a/src/ModelContextProtocol/Protocol/Messages/JsonRpcRequest.cs b/src/ModelContextProtocol/Protocol/Messages/JsonRpcRequest.cs index 9de16b8..8ae4a56 100644 --- a/src/ModelContextProtocol/Protocol/Messages/JsonRpcRequest.cs +++ b/src/ModelContextProtocol/Protocol/Messages/JsonRpcRequest.cs @@ -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. /// [JsonPropertyName("params")] - public object? Params { get; init; } + public JsonNode? Params { get; init; } } diff --git a/src/ModelContextProtocol/Protocol/Messages/JsonRpcResponse.cs b/src/ModelContextProtocol/Protocol/Messages/JsonRpcResponse.cs index a5a1238..9ced163 100644 --- a/src/ModelContextProtocol/Protocol/Messages/JsonRpcResponse.cs +++ b/src/ModelContextProtocol/Protocol/Messages/JsonRpcResponse.cs @@ -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. /// [JsonPropertyName("result")] - public required object? Result { get; init; } + public required JsonNode? Result { get; init; } } diff --git a/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs b/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs index 2b4acac..49b2fe4 100644 --- a/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/SseClientSessionTransport.cs @@ -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 _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() ?? NullLogger.Instance; - _jsonOptions = McpJsonUtilities.DefaultOptions; _connectionEstablished = new TaskCompletionSource(); } @@ -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()), + 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 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()); + var message = JsonSerializer.Deserialize(data, McpJsonUtilities.JsonContext.Default.IJsonRpcMessage); if (message == null) { _logger.TransportMessageParseUnexpectedType(EndpointName, data); diff --git a/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs index b7c8d3f..08c6e57 100644 --- a/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/CallToolRequestParams.cs @@ -1,4 +1,6 @@ -namespace ModelContextProtocol.Protocol.Types; +using System.Text.Json; + +namespace ModelContextProtocol.Protocol.Types; /// /// 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. /// [System.Text.Json.Serialization.JsonPropertyName("arguments")] - public Dictionary? Arguments { get; init; } + public Dictionary? Arguments { get; init; } } diff --git a/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs b/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs index 8c153f2..072992c 100644 --- a/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs +++ b/src/ModelContextProtocol/Protocol/Types/LoggingMessageNotificationParams.cs @@ -24,7 +24,7 @@ public class LoggingMessageNotificationParams public string? Logger { get; init; } /// - /// 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. /// [JsonPropertyName("data")] public JsonElement? Data { get; init; } diff --git a/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs b/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs index 1daece1..0d70f8b 100644 --- a/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs +++ b/src/ModelContextProtocol/Protocol/Types/ResourceContents.cs @@ -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) { diff --git a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs index 03d01ee..0f33c8e 100644 --- a/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs @@ -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 arguments = request.Params?.Arguments is IDictionary existingArgs ? - new(existingArgs) : + Dictionary arguments = request.Params?.Arguments is { } paramArgs ? + paramArgs.ToDictionary(entry => entry.Key, entry => entry.Value.AsObject()) : []; arguments[RequestContextKey] = request; diff --git a/src/ModelContextProtocol/Server/McpServer.cs b/src/ModelContextProtocol/Server/McpServer.cs index 547c223..80a28d3 100644 --- a/src/ModelContextProtocol/Server/McpServer.cs +++ b/src/ModelContextProtocol/Server/McpServer.cs @@ -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(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(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(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(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct)); - SetRequestHandler(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(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(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct)); - SetRequestHandler(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(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct)); - SetRequestHandler(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(RequestMethods.ToolsList, (request, ct) => listToolsHandler(new(this, request), ct)); - SetRequestHandler(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(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); } } \ No newline at end of file diff --git a/src/ModelContextProtocol/Server/McpServerExtensions.cs b/src/ModelContextProtocol/Server/McpServerExtensions.cs index ddaf45f..9b160d4 100644 --- a/src/ModelContextProtocol/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol/Server/McpServerExtensions.cs @@ -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( - new JsonRpcRequest { Method = RequestMethods.SamplingCreateMessage, Params = request }, - cancellationToken); + return server.SendRequestAsync( + RequestMethods.SamplingCreateMessage, + request, + McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, + McpJsonUtilities.JsonContext.Default.CreateMessageResult, + cancellationToken: cancellationToken); } /// @@ -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( - new JsonRpcRequest { Method = RequestMethods.RootsList, Params = request }, - cancellationToken); + return server.SendRequestAsync( + RequestMethods.RootsList, + request, + McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, + McpJsonUtilities.JsonContext.Default.ListRootsResult, + cancellationToken: cancellationToken); } /// Provides an implementation that's implemented via client sampling. diff --git a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs b/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs index c79a09e..9c6744e 100644 --- a/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs +++ b/src/ModelContextProtocol/Server/TemporaryAIFunctionFactory.cs @@ -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(inherit: true)?.Description, hasDefaultValue: parameter.HasDefaultValue, defaultValue: parameter.HasDefaultValue ? parameter.DefaultValue : null, - serializerOptions), AIJsonUtilities.DefaultOptions.GetTypeInfo()); + serializerOptions), McpJsonUtilities.JsonContext.Default.JsonElement); parameterSchemas.Add(parameter.Name, parameterSchema); if (!parameter.IsOptional) diff --git a/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs b/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs index df551c4..052cc16 100644 --- a/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs +++ b/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs @@ -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(string method, Func> handler) - => _requestHandlers.Set(method, handler); + protected void SetRequestHandler( + string method, + Func> handler, + JsonTypeInfo requestTypeInfo, + JsonTypeInfo responseTypeInfo) + + => _requestHandlers.Set(method, handler, requestTypeInfo, responseTypeInfo); public void AddNotificationHandler(string method, Func handler) => _notificationHandlers.Add(method, handler); - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) where TResult : class - => GetSessionOrThrow().SendRequestAsync(request, cancellationToken); + public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) + => GetSessionOrThrow().SendRequestAsync(request, cancellationToken); public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) => GetSessionOrThrow().SendMessageAsync(message, cancellationToken); diff --git a/src/ModelContextProtocol/Shared/McpSession.cs b/src/ModelContextProtocol/Shared/McpSession.cs index dc3d62c..46c3279 100644 --- a/src/ModelContextProtocol/Shared/McpSession.cs +++ b/src/ModelContextProtocol/Shared/McpSession.cs @@ -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. /// private readonly ConcurrentDictionary _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()); + 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 } /// - /// 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. /// - /// The expected response type. /// The JSON-RPC request to send. /// A token to cancel the operation. /// A task containing the server's response. - public async Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where TResult : class + public async Task 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())); + _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()); - var resultObject = JsonSerializer.Deserialize(resultJson, _jsonOptions.GetTypeInfo()); - - // 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())}, 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())); + _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()); - - default: - return JsonSerializer.Deserialize( - JsonSerializer.Serialize(notificationParams, McpJsonUtilities.DefaultOptions.GetTypeInfo()), - McpJsonUtilities.DefaultOptions.GetTypeInfo()); - } + 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(); 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(); tags.Add("mcp.request.params.uri", uri); if (activity is not null) { diff --git a/src/ModelContextProtocol/Shared/RequestHandlers.cs b/src/ModelContextProtocol/Shared/RequestHandlers.cs index be1f80c..83c911c 100644 --- a/src/ModelContextProtocol/Shared/RequestHandlers.cs +++ b/src/ModelContextProtocol/Shared/RequestHandlers.cs @@ -1,11 +1,12 @@ 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.Shared; -internal sealed class RequestHandlers : Dictionary>> +internal sealed class RequestHandlers : Dictionary>> { /// /// Registers a handler for incoming requests of a specific method. @@ -14,18 +15,24 @@ internal sealed class RequestHandlers : DictionaryType of response payload (not full RPC response /// Method identifier to register for /// Handler to be called when a request with specified method identifier is received - public void Set(string method, Func> handler) + /// The JSON contract governing request serialization. + /// The JSON contract governing response serialization. + public void Set( + string method, + Func> handler, + JsonTypeInfo requestTypeInfo, + JsonTypeInfo responseTypeInfo) { Throw.IfNull(method); Throw.IfNull(handler); + Throw.IfNull(requestTypeInfo); + Throw.IfNull(responseTypeInfo); this[method] = async (request, cancellationToken) => { - // Convert the params JsonElement to our type using the same options - var jsonString = JsonSerializer.Serialize(request.Params, McpJsonUtilities.DefaultOptions.GetTypeInfo()); - var typedRequest = JsonSerializer.Deserialize(jsonString, McpJsonUtilities.DefaultOptions.GetTypeInfo()); - - return await handler(typedRequest, cancellationToken).ConfigureAwait(false); + TRequest? typedRequest = JsonSerializer.Deserialize(request.Params, requestTypeInfo); + object? result = await handler(typedRequest, cancellationToken).ConfigureAwait(false); + return JsonSerializer.SerializeToNode(result, responseTypeInfo); }; } } diff --git a/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs b/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs index 337cdbd..2ef17f2 100644 --- a/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs +++ b/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs @@ -75,6 +75,7 @@ public static partial class McpJsonUtilities (JsonTypeInfo)options.GetTypeInfo(typeof(T)); internal static JsonElement DefaultMcpToolSchema { get; } = ParseJsonElement("""{"type":"object"}"""u8); + internal static object? AsObject(this JsonElement element) => element.ValueKind is JsonValueKind.Null ? null : element; internal static bool IsValidMcpToolSchema(JsonElement element) { @@ -151,6 +152,7 @@ public static partial class McpJsonUtilities [JsonSerializable(typeof(SubscribeRequestParams))] [JsonSerializable(typeof(UnsubscribeFromResourceRequestParams))] [JsonSerializable(typeof(UnsubscribeRequestParams))] + [JsonSerializable(typeof(IReadOnlyDictionary))] [ExcludeFromCodeCoverage] internal sealed partial class JsonContext : JsonSerializerContext; diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index 8c70360..6e5655a 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -79,11 +79,11 @@ internal static class Program await server.SendMessageAsync(new JsonRpcNotification() { Method = NotificationMethods.LoggingMessageNotification, - Params = new LoggingMessageNotificationParams + Params = JsonSerializer.SerializeToNode(new LoggingMessageNotificationParams { Level = logLevel, Data = JsonSerializer.Deserialize("\"Random log message\"") - } + }) }, cancellationToken); } @@ -91,10 +91,10 @@ internal static class Program foreach (var resource in _subscribedResources) { ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource.Key }; - await server.SendMessageAsync(new JsonRpcNotification() + await server.SendMessageAsync(new JsonRpcNotification { Method = NotificationMethods.ResourceUpdatedNotification, - Params = notificationParams + Params = JsonSerializer.SerializeToNode(notificationParams), }, cancellationToken); } } @@ -168,7 +168,7 @@ internal static class Program } return new CallToolResponse() { - Content = [new Content() { Text = "Echo: " + message?.ToString(), Type = "text" }] + Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }] }; } else if (request.Params?.Name == "sampleLLM") @@ -179,7 +179,7 @@ internal static class Program { throw new McpServerException("Missing required arguments 'prompt' and 'maxTokens'"); } - var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt?.ToString() ?? "", "sampleLLM", Convert.ToInt32(maxTokens?.ToString())), + var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.GetRawText())), cancellationToken); return new CallToolResponse() diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs index 23afc1d..424b51d 100644 --- a/tests/ModelContextProtocol.TestSseServer/Program.cs +++ b/tests/ModelContextProtocol.TestSseServer/Program.cs @@ -175,7 +175,7 @@ public class Program { throw new McpServerException("Missing required arguments 'prompt' and 'maxTokens'"); } - var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt?.ToString() ?? "", "sampleLLM", Convert.ToInt32(maxTokens?.ToString())), + var sampleResult = await request.Server.RequestSamplingAsync(CreateRequestSamplingParams(prompt.ToString(), "sampleLLM", Convert.ToInt32(maxTokens.ToString())), cancellationToken); return new CallToolResponse() diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index 7daea20..7c3e92f 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -1,9 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; +using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; using System.IO.Pipelines; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol.Tests.Client; @@ -72,7 +75,7 @@ public class McpClientExtensionsTests : LoggedTest { IMcpClient client = await CreateMcpClientForServer(); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(12, tools.Count); var echo = tools.Single(t => t.Name == "Method4"); var result = await echo.InvokeAsync(new Dictionary() { ["i"] = 42 }, TestContext.Current.CancellationToken); @@ -98,7 +101,7 @@ public class McpClientExtensionsTests : LoggedTest { IMcpClient client = await CreateMcpClientForServer(); - await foreach (var tool in client.EnumerateToolsAsync(TestContext.Current.CancellationToken)) + await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)) { if (tool.Name == "Method4") { @@ -110,4 +113,62 @@ public class McpClientExtensionsTests : LoggedTest Assert.Fail("Couldn't find target method"); } + + [Fact] + public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() + { + JsonSerializerOptions options = new(JsonSerializerOptions.Default); + IMcpClient client = await CreateMcpClientForServer(); + bool hasTools = false; + + await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken)) + { + Assert.Same(options, tool.JsonSerializerOptions); + hasTools = true; + } + + foreach (var tool in await client.ListToolsAsync(options, TestContext.Current.CancellationToken)) + { + Assert.Same(options, tool.JsonSerializerOptions); + } + + Assert.True(hasTools); + } + + [Fact] + public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + IMcpClient client = await CreateMcpClientForServer(); + + var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); + await Assert.ThrowsAsync(() => tool.InvokeAsync(new Dictionary { ["i"] = 42 }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task SendRequestAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + IMcpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync(() => client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task SendNotificationAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + IMcpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetPromptsAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + IMcpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync(() => client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + } } \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs index f6b8f2b..d4545dd 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs @@ -1,8 +1,9 @@ -using System.Threading.Channels; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Types; +using System.Text.Json; +using System.Threading.Channels; namespace ModelContextProtocol.Tests.Client; @@ -206,16 +207,16 @@ public class McpClientFactoryTests _channel.Writer.TryWrite(new JsonRpcResponse { Id = ((JsonRpcRequest)message).Id, - Result = new InitializeResult() + Result = JsonSerializer.SerializeToNode(new InitializeResult { Capabilities = new ServerCapabilities(), ProtocolVersion = "2024-11-05", - ServerInfo = new Implementation() + ServerInfo = new Implementation { Name = "NopTransport", Version = "1.0.0" }, - } + }), }); break; } diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index a08bbb5..d131aca 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -68,7 +68,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture t.Name == "echo"); var result = await echo.InvokeAsync([new KeyValuePair("message", "Hello MCP!")], TestContext.Current.CancellationToken); @@ -140,7 +140,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture(() => - client.GetPromptAsync("non_existent_prompt", null, TestContext.Current.CancellationToken)); + client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken)); } [Theory] @@ -259,7 +259,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture { - var notificationParams = JsonSerializer.Deserialize(notification.Params!.ToString() ?? string.Empty); + var notificationParams = JsonSerializer.Deserialize(notification.Params); tcs.TrySetResult(true); return Task.CompletedTask; }); @@ -280,7 +280,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture { - var notificationParams = JsonSerializer.Deserialize(notification.Params!.ToString() ?? string.Empty); + var notificationParams = JsonSerializer.Deserialize(notification.Params); receivedNotification.TrySetResult(true); return Task.CompletedTask; }); @@ -381,7 +381,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture(), - TestContext.Current.CancellationToken); + cancellationToken: TestContext.Current.CancellationToken); // assert Assert.NotNull(result); @@ -485,7 +485,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture() { ["prompt"] = "In just a few words, what is the most famous tower in Paris?", - }, TestContext.Current.CancellationToken); + }, cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotEmpty(result.Content); @@ -556,8 +556,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture { - var loggingMessageNotificationParameters = JsonSerializer.Deserialize(notification.Params!.ToString() ?? string.Empty, - jsonSerializerOptions); + var loggingMessageNotificationParameters = JsonSerializer.Deserialize(notification.Params, jsonSerializerOptions); if (loggingMessageNotificationParameters is not null) { receivedNotification.TrySetResult(true); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index c19469a..26d834b 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -154,7 +154,7 @@ public class McpServerBuilderExtensionsPromptsTests : LoggedTest, IAsyncDisposab var prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsChatMessages)); Assert.Equal("Returns chat messages", prompt.Description); - var result = await prompt.GetAsync(new Dictionary() { ["message"] = "hello" }, TestContext.Current.CancellationToken); + var result = await prompt.GetAsync(new Dictionary() { ["message"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken); var chatMessages = result.ToChatMessages(); Assert.NotNull(chatMessages); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index f86e27a..fc52613 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -172,7 +172,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable { IMcpClient client = await CreateMcpClientForServer(); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); McpClientTool echoTool = tools.First(t => t.Name == "Echo"); @@ -217,7 +217,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)) { - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); McpClientTool echoTool = tools.First(t => t.Name == "Echo"); @@ -244,7 +244,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable { IMcpClient client = await CreateMcpClientForServer(); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); Channel listChanged = Channel.CreateUnbounded(); @@ -265,7 +265,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable serverTools.Add(newTool); await notificationRead; - tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(17, tools.Count); Assert.Contains(tools, t => t.Name == "NewTool"); @@ -274,7 +274,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable serverTools.Remove(newTool); await notificationRead; - tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); Assert.DoesNotContain(tools, t => t.Name == "NewTool"); } @@ -287,7 +287,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable var result = await client.CallToolAsync( "Echo", new Dictionary() { ["message"] = "Peter" }, - TestContext.Current.CancellationToken); + cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.NotNull(result.Content); @@ -305,7 +305,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable var result = await client.CallToolAsync( "EchoArray", new Dictionary() { ["message"] = "Peter" }, - TestContext.Current.CancellationToken); + cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(result.Content); Assert.NotEmpty(result.Content); @@ -536,7 +536,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable { IMcpClient client = await CreateMcpClientForServer(); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); Assert.NotEmpty(tools); @@ -612,26 +612,25 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable IMcpClient client = await CreateMcpClientForServer(); client.AddNotificationHandler(NotificationMethods.ProgressNotification, notification => { - ProgressNotification pn = JsonSerializer.Deserialize((JsonElement)notification.Params!)!; + ProgressNotification pn = JsonSerializer.Deserialize(notification.Params)!; notifications.Enqueue(pn); return Task.CompletedTask; }); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); Assert.NotEmpty(tools); McpClientTool progressTool = tools.First(t => t.Name == nameof(EchoTool.SendsProgressNotifications)); - var result = await client.SendRequestAsync(new JsonRpcRequest() - { - Method = RequestMethods.ToolsCall, - Params = new CallToolRequestParams() + var result = await client.SendRequestAsync( + RequestMethods.ToolsCall, + new CallToolRequestParams { Name = progressTool.ProtocolTool.Name, Meta = new() { ProgressToken = new("abc123") }, }, - }, TestContext.Current.CancellationToken); + cancellationToken: TestContext.Current.CancellationToken); Assert.Contains("done", JsonSerializer.Serialize(result)); SpinWait.SpinUntil(() => notifications.Count == 10, TimeSpan.FromSeconds(10)); @@ -652,18 +651,17 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable { IMcpClient client = await CreateMcpClientForServer(); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); Assert.NotEmpty(tools); McpClientTool cancelableTool = tools.First(t => t.Name == nameof(EchoTool.InfiniteCancelableOperation)); var requestId = new RequestId(Guid.NewGuid().ToString()); - var invokeTask = client.SendRequestAsync(new JsonRpcRequest() - { - Method = RequestMethods.ToolsCall, - Id = requestId, - Params = new CallToolRequestParams() { Name = cancelableTool.ProtocolTool.Name }, - }, TestContext.Current.CancellationToken); + var invokeTask = client.SendRequestAsync( + RequestMethods.ToolsCall, + new CallToolRequestParams { Name = cancelableTool.ProtocolTool.Name }, + requestId: requestId, + cancellationToken: TestContext.Current.CancellationToken); await client.SendNotificationAsync( NotificationMethods.CancelledNotification, diff --git a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs index 4218855..24c5430 100644 --- a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs +++ b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs @@ -23,7 +23,7 @@ public class DiagnosticTests { await RunConnected(async (client, server) => { - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); Assert.NotEmpty(tools); diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index e4cf89a..e0a4a6e 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -5,6 +5,8 @@ using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Server; using ModelContextProtocol.Tests.Utils; using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; namespace ModelContextProtocol.Tests.Server; @@ -171,7 +173,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); + JsonObject jObj = Assert.IsType(response); + Assert.Empty(jObj); }); } @@ -184,9 +187,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (InitializeResult)response; + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result); Assert.Equal("TestServer", result.ServerInfo.Name); Assert.Equal("1.0", result.ServerInfo.Version); Assert.Equal("2024", result.ProtocolVersion); @@ -202,10 +204,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (CompleteResult)response; - Assert.NotNull(result.Completion); + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result?.Completion); Assert.Empty(result.Completion.Values); Assert.Equal(0, result.Completion.Total); Assert.False(result.Completion.HasMore); @@ -233,10 +233,8 @@ public class McpServerTests : LoggedTest }, assertResult: response => { - Assert.IsType(response); - - var result = (CompleteResult)response; - Assert.NotNull(result.Completion); + CompleteResult? result = JsonSerializer.Deserialize(response); + Assert.NotNull(result?.Completion); Assert.NotEmpty(result.Completion.Values); Assert.Equal("test", result.Completion.Values[0]); Assert.Equal(2, result.Completion.Total); @@ -273,10 +271,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (ListResourceTemplatesResult)response; - Assert.NotNull(result.ResourceTemplates); + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result?.ResourceTemplates); Assert.NotEmpty(result.ResourceTemplates); Assert.Equal("test", result.ResourceTemplates[0].UriTemplate); }); @@ -304,10 +300,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (ListResourcesResult)response; - Assert.NotNull(result.Resources); + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result?.Resources); Assert.NotEmpty(result.Resources); Assert.Equal("test", result.Resources[0].Uri); }); @@ -331,7 +325,7 @@ public class McpServerTests : LoggedTest { return Task.FromResult(new ReadResourceResult { - Contents = [new TextResourceContents() { Text = "test" }] + Contents = [new TextResourceContents { Text = "test" }] }); }, ListResourcesHandler = (request, ct) => throw new NotImplementedException(), @@ -341,10 +335,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (ReadResourceResult)response; - Assert.NotNull(result.Contents); + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result?.Contents); Assert.NotEmpty(result.Contents); TextResourceContents textResource = Assert.IsType(result.Contents[0]); @@ -380,10 +372,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (ListPromptsResult)response; - Assert.NotNull(result.Prompts); + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result?.Prompts); Assert.NotEmpty(result.Prompts); Assert.Equal("test", result.Prompts[0].Name); }); @@ -411,9 +401,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (GetPromptResult)response; + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result); Assert.Equal("test", result.Description); }); } @@ -446,9 +435,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (ListToolsResult)response; + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result); Assert.NotEmpty(result.Tools); Assert.Equal("test", result.Tools[0].Name); }); @@ -482,9 +470,8 @@ public class McpServerTests : LoggedTest configureOptions: null, assertResult: response => { - Assert.IsType(response); - - var result = (CallToolResponse)response; + var result = JsonSerializer.Deserialize(response); + Assert.NotNull(result); Assert.NotEmpty(result.Content); Assert.Equal("test", result.Content[0].Text); }); @@ -496,7 +483,7 @@ public class McpServerTests : LoggedTest await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured"); } - private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action? configureOptions, Action assertResult) + private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action? configureOptions, Action assertResult) { await using var transport = new TestServerTransport(); var options = CreateOptions(serverCapabilities); @@ -524,7 +511,6 @@ public class McpServerTests : LoggedTest var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5)); Assert.NotNull(response); - Assert.NotNull(response.Result); assertResult(response.Result); @@ -610,10 +596,11 @@ public class McpServerTests : LoggedTest supportsSampling ? new ClientCapabilities { Sampling = new SamplingCapability() } : null; - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) where T : class + public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) { - CreateMessageRequestParams rp = Assert.IsType(request.Params); + CreateMessageRequestParams? rp = JsonSerializer.Deserialize(request.Params); + Assert.NotNull(rp); Assert.Equal(0.75f, rp.Temperature); Assert.Equal(42, rp.MaxTokens); Assert.Equal(["."], rp.StopSequences); @@ -634,7 +621,12 @@ public class McpServerTests : LoggedTest Role = "assistant", StopReason = "endTurn", }; - return Task.FromResult((T)(object)result); + + return Task.FromResult(new JsonRpcResponse + { + Id = new RequestId("0"), + Result = JsonSerializer.SerializeToNode(result), + }); } public ValueTask DisposeAsync() => default; @@ -670,7 +662,7 @@ public class McpServerTests : LoggedTest await transport.SendMessageAsync(new JsonRpcNotification { Method = NotificationMethods.ProgressNotification, - Params = new ProgressNotification() + Params = JsonSerializer.SerializeToNode(new ProgressNotification { ProgressToken = new("abc"), Progress = new() @@ -679,11 +671,12 @@ public class McpServerTests : LoggedTest Total = 100, Message = "Progress message", }, - }, + }), }, TestContext.Current.CancellationToken); var notification = await notificationReceived.Task; - var progress = (ProgressNotification)notification.Params!; + var progress = JsonSerializer.Deserialize(notification.Params); + Assert.NotNull(progress); Assert.Equal("abc", progress.ProgressToken.ToString()); Assert.Equal(50, progress.Progress.Progress); Assert.Equal(100, progress.Progress.Total); diff --git a/tests/ModelContextProtocol.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.Tests/SseIntegrationTests.cs index 2d9ba13..f6ecee8 100644 --- a/tests/ModelContextProtocol.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/SseIntegrationTests.cs @@ -3,7 +3,6 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Tests.Utils; -using System.Text.Json; namespace ModelContextProtocol.Tests; @@ -41,7 +40,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10)); // Send a test message through POST endpoint - await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, TestContext.Current.CancellationToken); + await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.True(true); @@ -78,7 +77,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou defaultOptions, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); - var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); // assert Assert.NotEmpty(tools); @@ -145,7 +144,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou { ["prompt"] = "Test prompt", ["maxTokens"] = 100 - }, TestContext.Current.CancellationToken); + }, cancellationToken: TestContext.Current.CancellationToken); // assert Assert.NotNull(result); @@ -188,7 +187,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10)); // Send a test message through POST endpoint - await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, TestContext.Current.CancellationToken); + await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken); // Assert Assert.True(true); @@ -229,7 +228,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou var receivedNotification = new TaskCompletionSource(); client.AddNotificationHandler("test/notification", (args) => { - var msg = ((JsonElement?)args.Params)?.GetProperty("message").GetString(); + var msg = args.Params?["message"]?.GetValue(); receivedNotification.SetResult(msg); return Task.CompletedTask; diff --git a/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs b/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs index 4fad0d5..004e54a 100644 --- a/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/SseServerIntegrationTests.cs @@ -63,7 +63,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture(() => - client.GetPromptAsync("non_existent_prompt", null, TestContext.Current.CancellationToken)); + client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] @@ -238,7 +238,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture + Params = new JsonObject { - ["text"] = JsonSerializer.SerializeToElement(chineseText) - } + ["text"] = chineseText + }, }; // Clear output and send message @@ -175,10 +176,10 @@ public class StdioServerTransportTests : LoggedTest { Method = "test", Id = new RequestId(45), - Params = new Dictionary + Params = new JsonObject { - ["text"] = JsonSerializer.SerializeToElement(emojiText) - } + ["text"] = emojiText + }, }; // Clear output and send message diff --git a/tests/ModelContextProtocol.Tests/Utils/InMemoryTestSseServer.cs b/tests/ModelContextProtocol.Tests/Utils/InMemoryTestSseServer.cs index 0bdfde1..651d85e 100644 --- a/tests/ModelContextProtocol.Tests/Utils/InMemoryTestSseServer.cs +++ b/tests/ModelContextProtocol.Tests/Utils/InMemoryTestSseServer.cs @@ -289,7 +289,7 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable var jsonRpcResponse = new JsonRpcResponse() { Id = jsonRpcRequest.Id, - Result = new InitializeResult() + Result = JsonSerializer.SerializeToNode(new InitializeResult { ProtocolVersion = "2024-11-05", Capabilities = new(), @@ -298,7 +298,7 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable Name = "ExampleServer", Version = "1.0.0" } - } + }) }; // Echo back to the HTTP response @@ -369,7 +369,7 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable { JsonRpc = "2.0", Method = "test/notification", - Params = new { message = content } + Params = JsonSerializer.SerializeToNode(new { message = content }), }; var serialized = JsonSerializer.Serialize(notification); diff --git a/tests/ModelContextProtocol.Tests/Utils/TestServerTransport.cs b/tests/ModelContextProtocol.Tests/Utils/TestServerTransport.cs index 33a1336..f216601 100644 --- a/tests/ModelContextProtocol.Tests/Utils/TestServerTransport.cs +++ b/tests/ModelContextProtocol.Tests/Utils/TestServerTransport.cs @@ -1,7 +1,8 @@ -using System.Threading.Channels; -using ModelContextProtocol.Protocol.Messages; +using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Transport; using ModelContextProtocol.Protocol.Types; +using System.Text.Json; +using System.Threading.Channels; namespace ModelContextProtocol.Tests.Utils; @@ -59,10 +60,10 @@ public class TestServerTransport : ITransport await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = new ModelContextProtocol.Protocol.Types.ListRootsResult + Result = JsonSerializer.SerializeToNode(new ListRootsResult { Roots = [] - } + }), }, cancellationToken); } @@ -71,7 +72,7 @@ public class TestServerTransport : ITransport await WriteMessageAsync(new JsonRpcResponse { Id = request.Id, - Result = new CreateMessageResult { Content = new(), Model = "model", Role = "role" } + Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = "role" }), }, cancellationToken); }