diff --git a/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs b/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs index 6329d61..ce949d7 100644 --- a/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs +++ b/samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs @@ -45,7 +45,7 @@ public static class McpEndpointRouteBuilderExtensions return; } - var message = await context.Request.ReadFromJsonAsync(JsonSerializerOptionsExtensions.DefaultOptions, context.RequestAborted); + var message = await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions, context.RequestAborted); if (message is null) { await Results.BadRequest("No message in request body.").ExecuteAsync(context); diff --git a/samples/AspNetCoreSseServer/SseServerStreamTransport.cs b/samples/AspNetCoreSseServer/SseServerStreamTransport.cs index a644761..52fc688 100644 --- a/samples/AspNetCoreSseServer/SseServerStreamTransport.cs +++ b/samples/AspNetCoreSseServer/SseServerStreamTransport.cs @@ -28,7 +28,7 @@ public class SseServerStreamTransport(Stream sseResponseStream) : ITransport return; } - JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, JsonSerializerOptionsExtensions.DefaultOptions); + JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.DefaultOptions); } // The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type, diff --git a/src/ModelContextProtocol/Client/McpClient.cs b/src/ModelContextProtocol/Client/McpClient.cs index ebde5bb..244522a 100644 --- a/src/ModelContextProtocol/Client/McpClient.cs +++ b/src/ModelContextProtocol/Client/McpClient.cs @@ -139,8 +139,8 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient // Store server information _logger.ServerCapabilitiesReceived(EndpointName, - capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, JsonSerializerOptionsExtensions.JsonContext.Default.ServerCapabilities), - serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, JsonSerializerOptionsExtensions.JsonContext.Default.Implementation)); + capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities), + serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation)); ServerCapabilities = initializeResponse.Capabilities; ServerInfo = initializeResponse.ServerInfo; diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs index 5fb0fa8..5251f7b 100644 --- a/src/ModelContextProtocol/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs @@ -483,7 +483,7 @@ public static class McpClientExtensions ["description"] = tool.Description ?? string.Empty, ["properties"] = tool.InputSchema?.Properties ?? [], ["required"] = tool.InputSchema?.Required ?? [] - }, JsonSerializerOptionsExtensions.JsonContext.Default.DictionaryStringObject); + }, McpJsonUtilities.JsonContext.Default.DictionaryStringObject); /// protected async override Task InvokeCoreAsync( @@ -501,7 +501,7 @@ public static class McpClientExtensions } CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false); - return JsonSerializer.SerializeToElement(result, JsonSerializerOptionsExtensions.JsonContext.Default.CallToolResponse); + return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResponse); } } } \ No newline at end of file diff --git a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs index e148319..af1de38 100644 --- a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs +++ b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs @@ -96,7 +96,7 @@ public static partial class McpServerBuilderExtensions { Name = function.Name, Description = function.Description, - InputSchema = JsonSerializer.Deserialize(function.JsonSchema, JsonSerializerOptionsExtensions.JsonContext.Default.JsonSchema), + InputSchema = JsonSerializer.Deserialize(function.JsonSchema, McpJsonUtilities.JsonContext.Default.JsonSchema), }); callbacks.Add(function.Name, async (request, cancellationToken) => diff --git a/src/ModelContextProtocol/ModelContextProtocol.csproj b/src/ModelContextProtocol/ModelContextProtocol.csproj index f1b5f6c..9b9a89e 100644 --- a/src/ModelContextProtocol/ModelContextProtocol.csproj +++ b/src/ModelContextProtocol/ModelContextProtocol.csproj @@ -12,12 +12,11 @@ true - + - - + diff --git a/src/ModelContextProtocol/Protocol/Transport/HttpListenerSseServerTransport.cs b/src/ModelContextProtocol/Protocol/Transport/HttpListenerSseServerTransport.cs index 9a756d9..f385476 100644 --- a/src/ModelContextProtocol/Protocol/Transport/HttpListenerSseServerTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/HttpListenerSseServerTransport.cs @@ -42,7 +42,7 @@ public sealed class HttpListenerSseServerTransport : TransportBase, IServerTrans { _serverName = serverName; _logger = loggerFactory.CreateLogger(); - _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; + _jsonOptions = McpJsonUtilities.DefaultOptions; _httpServerProvider = new HttpListenerServerProvider(port); } diff --git a/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs b/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs index 577c563..5d72d0c 100644 --- a/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/SseClientTransport.cs @@ -65,7 +65,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport _httpClient = httpClient; _connectionCts = new CancellationTokenSource(); _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; + _jsonOptions = McpJsonUtilities.DefaultOptions; _connectionEstablished = new TaskCompletionSource(); _ownsHttpClient = ownsHttpClient; } diff --git a/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs index f9e13a6..d8986de 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StdioClientTransport.cs @@ -43,7 +43,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport _options = options; _serverConfig = serverConfig; _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; - _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; + _jsonOptions = McpJsonUtilities.DefaultOptions; } /// diff --git a/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs b/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs index 98252d2..2e81634 100644 --- a/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs +++ b/src/ModelContextProtocol/Protocol/Transport/StdioServerTransport.cs @@ -21,7 +21,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport private readonly string _serverName; private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; + private readonly JsonSerializerOptions _jsonOptions = McpJsonUtilities.DefaultOptions; private readonly TextReader _stdin = Console.In; private readonly TextWriter _stdout = Console.Out; diff --git a/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs b/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs index 5f24d81..c8fc983 100644 --- a/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs +++ b/src/ModelContextProtocol/Shared/McpJsonRpcEndpoint.cs @@ -46,7 +46,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable _pendingRequests = new(); _notificationHandlers = new(); _nextRequestId = 1; - _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; + _jsonOptions = McpJsonUtilities.DefaultOptions; _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; } diff --git a/src/ModelContextProtocol/Utils/Json/JsonSerializerOptionsExtensions.cs b/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs similarity index 60% rename from src/ModelContextProtocol/Utils/Json/JsonSerializerOptionsExtensions.cs rename to src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs index 0559856..3c4837e 100644 --- a/src/ModelContextProtocol/Utils/Json/JsonSerializerOptionsExtensions.cs +++ b/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs @@ -1,6 +1,7 @@ using ModelContextProtocol.Protocol.Messages; using ModelContextProtocol.Protocol.Types; using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -8,11 +9,31 @@ using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol.Utils.Json; -/// -/// Extensions for configuring System.Text.Json serialization options for MCP. -/// -internal static partial class JsonSerializerOptionsExtensions +/// Provides a collection of utility methods for working with JSON data in the context of MCP. +public static partial class McpJsonUtilities { + /// + /// Gets the singleton used as the default in JSON serialization operations. + /// + /// + /// + /// For Native AOT or applications disabling , this instance + /// includes source generated contracts for all common exchange types contained in the ModelContextProtocol library. + /// + /// + /// It additionally turns on the following settings: + /// + /// Enables string-based enum serialization as implemented by . + /// Enables as the default ignore condition for properties. + /// Enables as the default number handling for number types. + /// + /// Enables when escaping JSON strings. + /// Consuming applications must ensure that JSON outputs are adequately escaped before embedding in other document formats, + /// such as HTML and XML. + /// + /// + /// + /// public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); /// @@ -26,23 +47,31 @@ internal static partial class JsonSerializerOptionsExtensions // If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize, // and we want to be flexible in terms of what can be put into the various collections in the object model. // Otherwise, use the source-generated options to enable trimming and Native AOT. + JsonSerializerOptions options; if (JsonSerializer.IsReflectionEnabledByDefault) { // Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below. - JsonSerializerOptions options = new(JsonSerializerDefaults.Web) + options = new(JsonSerializerDefaults.Web) { TypeInfoResolver = new DefaultJsonTypeInfoResolver(), Converters = { new JsonStringEnumConverter() }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - NumberHandling = JsonNumberHandling.AllowReadingFromString + NumberHandling = JsonNumberHandling.AllowReadingFromString, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; + } + else + { + options = new(JsonContext.Default.Options) + { + // Compile-time encoder setting not yet available + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; - - options.MakeReadOnly(); - return options; } - return JsonContext.Default.Options; + options.MakeReadOnly(); + return options; } internal static JsonTypeInfo GetTypeInfo(this JsonSerializerOptions options) => @@ -62,6 +91,7 @@ internal static partial class JsonSerializerOptionsExtensions [JsonSerializable(typeof(JsonRpcResponse))] [JsonSerializable(typeof(JsonRpcError))] [JsonSerializable(typeof(ServerCapabilities))] + [JsonSerializable(typeof(ClientCapabilities))] [JsonSerializable(typeof(Implementation))] [JsonSerializable(typeof(CreateMessageResult))] [JsonSerializable(typeof(ListRootsResult))] diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs index 44f60cd..76efb6d 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioServerTransportTests.cs @@ -78,7 +78,7 @@ public class StdioServerTransportTests await transport.SendMessageAsync(message); var result = output.ToString()?.Trim(); - var expected = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions); + var expected = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); Assert.Equal(expected, result); } @@ -113,7 +113,7 @@ public class StdioServerTransportTests public async Task ReadMessagesAsync_Should_Read_Messages() { var message = new JsonRpcRequest { Method = "test", Id = RequestId.FromNumber(44) }; - var json = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions); + var json = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions); TextReader oldIn = Console.In; TextWriter oldOut = Console.Out;