Make McpJsonUtilities public and remove IVT to sample (#5)

Also adds in the same encoder used by AIJsonUtilities.
This commit is contained in:
Stephen Toub 2025-03-20 10:00:23 -04:00 • committed by GitHub
commit 6b8c288029
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 56 additions and 27 deletions

View file

@ -45,7 +45,7 @@ public static class McpEndpointRouteBuilderExtensions
return; return;
} }
var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(JsonSerializerOptionsExtensions.DefaultOptions, context.RequestAborted); var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(McpJsonUtilities.DefaultOptions, context.RequestAborted);
if (message is null) if (message is null)
{ {
await Results.BadRequest("No message in request body.").ExecuteAsync(context); await Results.BadRequest("No message in request body.").ExecuteAsync(context);

View file

@ -28,7 +28,7 @@ public class SseServerStreamTransport(Stream sseResponseStream) : ITransport
return; 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, // The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type,

View file

@ -139,8 +139,8 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
// Store server information // Store server information
_logger.ServerCapabilitiesReceived(EndpointName, _logger.ServerCapabilitiesReceived(EndpointName,
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, JsonSerializerOptionsExtensions.JsonContext.Default.ServerCapabilities), capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, JsonSerializerOptionsExtensions.JsonContext.Default.Implementation)); serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
ServerCapabilities = initializeResponse.Capabilities; ServerCapabilities = initializeResponse.Capabilities;
ServerInfo = initializeResponse.ServerInfo; ServerInfo = initializeResponse.ServerInfo;

View file

@ -483,7 +483,7 @@ public static class McpClientExtensions
["description"] = tool.Description ?? string.Empty, ["description"] = tool.Description ?? string.Empty,
["properties"] = tool.InputSchema?.Properties ?? [], ["properties"] = tool.InputSchema?.Properties ?? [],
["required"] = tool.InputSchema?.Required ?? [] ["required"] = tool.InputSchema?.Required ?? []
}, JsonSerializerOptionsExtensions.JsonContext.Default.DictionaryStringObject); }, McpJsonUtilities.JsonContext.Default.DictionaryStringObject);
/// <inheritdoc/> /// <inheritdoc/>
protected async override Task<object?> InvokeCoreAsync( protected async override Task<object?> InvokeCoreAsync(
@ -501,7 +501,7 @@ public static class McpClientExtensions
} }
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false); 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);
} }
} }
} }

View file

@ -96,7 +96,7 @@ public static partial class McpServerBuilderExtensions
{ {
Name = function.Name, Name = function.Name,
Description = function.Description, 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) => callbacks.Add(function.Name, async (request, cancellationToken) =>

View file

@ -12,12 +12,11 @@
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'"> <PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<IsAotCompatible>true</IsAotCompatible> <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="ModelContextProtocol.Tests" /> <InternalsVisibleTo Include="ModelContextProtocol.Tests" />
<InternalsVisibleTo Include="AspNetCoreSseServer" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions"/> <PackageReference Include="Microsoft.Extensions.AI.Abstractions"/>
<PackageReference Include="Microsoft.Extensions.AI" /> <PackageReference Include="Microsoft.Extensions.AI" />

View file

@ -42,7 +42,7 @@ public sealed class HttpListenerSseServerTransport : TransportBase, IServerTrans
{ {
_serverName = serverName; _serverName = serverName;
_logger = loggerFactory.CreateLogger<HttpListenerSseServerTransport>(); _logger = loggerFactory.CreateLogger<HttpListenerSseServerTransport>();
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; _jsonOptions = McpJsonUtilities.DefaultOptions;
_httpServerProvider = new HttpListenerServerProvider(port); _httpServerProvider = new HttpListenerServerProvider(port);
} }

View file

@ -65,7 +65,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
_httpClient = httpClient; _httpClient = httpClient;
_connectionCts = new CancellationTokenSource(); _connectionCts = new CancellationTokenSource();
_logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance; _logger = (ILogger?)loggerFactory?.CreateLogger<SseClientTransport>() ?? NullLogger.Instance;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; _jsonOptions = McpJsonUtilities.DefaultOptions;
_connectionEstablished = new TaskCompletionSource<bool>(); _connectionEstablished = new TaskCompletionSource<bool>();
_ownsHttpClient = ownsHttpClient; _ownsHttpClient = ownsHttpClient;
} }

View file

@ -43,7 +43,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
_options = options; _options = options;
_serverConfig = serverConfig; _serverConfig = serverConfig;
_logger = (ILogger?)loggerFactory?.CreateLogger<StdioClientTransport>() ?? NullLogger.Instance; _logger = (ILogger?)loggerFactory?.CreateLogger<StdioClientTransport>() ?? NullLogger.Instance;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; _jsonOptions = McpJsonUtilities.DefaultOptions;
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -21,7 +21,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
private readonly string _serverName; private readonly string _serverName;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly JsonSerializerOptions _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; private readonly JsonSerializerOptions _jsonOptions = McpJsonUtilities.DefaultOptions;
private readonly TextReader _stdin = Console.In; private readonly TextReader _stdin = Console.In;
private readonly TextWriter _stdout = Console.Out; private readonly TextWriter _stdout = Console.Out;

View file

@ -46,7 +46,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
_pendingRequests = new(); _pendingRequests = new();
_notificationHandlers = new(); _notificationHandlers = new();
_nextRequestId = 1; _nextRequestId = 1;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; _jsonOptions = McpJsonUtilities.DefaultOptions;
_logger = (ILogger?)loggerFactory?.CreateLogger<McpClient>() ?? NullLogger.Instance; _logger = (ILogger?)loggerFactory?.CreateLogger<McpClient>() ?? NullLogger.Instance;
} }

View file

@ -0,0 +1,102 @@
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;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Utils.Json;
/// <summary>Provides a collection of utility methods for working with JSON data in the context of MCP.</summary>
public static partial class McpJsonUtilities
{
/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/> singleton used as the default in JSON serialization operations.
/// </summary>
/// <remarks>
/// <para>
/// For Native AOT or applications disabling <see cref="JsonSerializer.IsReflectionEnabledByDefault"/>, this instance
/// includes source generated contracts for all common exchange types contained in the ModelContextProtocol library.
/// </para>
/// <para>
/// It additionally turns on the following settings:
/// <list type="number">
/// <item>Enables string-based enum serialization as implemented by <see cref="JsonStringEnumConverter"/>.</item>
/// <item>Enables <see cref="JsonIgnoreCondition.WhenWritingNull"/> as the default ignore condition for properties.</item>
/// <item>Enables <see cref="JsonNumberHandling.AllowReadingFromString"/> as the default number handling for number types.</item>
/// <item>
/// Enables <see cref="JavaScriptEncoder.UnsafeRelaxedJsonEscaping"/> 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.
/// </item>
/// </list>
/// </para>
/// </remarks>
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
/// <summary>
/// Creates default options to use for MCP-related serialization.
/// </summary>
/// <returns>The configured options.</returns>
[UnconditionalSuppressMessage("AotAnalysis", "IL3050", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
private static JsonSerializerOptions CreateDefaultOptions()
{
// 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.
options = new(JsonSerializerDefaults.Web)
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
Converters = { new JsonStringEnumConverter() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
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;
}
internal static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options) =>
(JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
// Keep in sync with CreateDefaultOptions above.
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
[JsonSerializable(typeof(JsonDocument))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(JsonNode))]
[JsonSerializable(typeof(IJsonRpcMessage))]
[JsonSerializable(typeof(JsonRpcRequest))]
[JsonSerializable(typeof(JsonRpcNotification))]
[JsonSerializable(typeof(JsonRpcResponse))]
[JsonSerializable(typeof(JsonRpcError))]
[JsonSerializable(typeof(ServerCapabilities))]
[JsonSerializable(typeof(ClientCapabilities))]
[JsonSerializable(typeof(Implementation))]
[JsonSerializable(typeof(CreateMessageResult))]
[JsonSerializable(typeof(ListRootsResult))]
[JsonSerializable(typeof(InitializeResult))]
[JsonSerializable(typeof(JsonSchema))]
[JsonSerializable(typeof(CallToolResponse))]
internal sealed partial class JsonContext : JsonSerializerContext;
}

View file

@ -78,7 +78,7 @@ public class StdioServerTransportTests
await transport.SendMessageAsync(message); await transport.SendMessageAsync(message);
var result = output.ToString()?.Trim(); var result = output.ToString()?.Trim();
var expected = JsonSerializer.Serialize(message, JsonSerializerOptionsExtensions.DefaultOptions); var expected = JsonSerializer.Serialize(message, McpJsonUtilities.DefaultOptions);
Assert.Equal(expected, result); Assert.Equal(expected, result);
} }
@ -113,7 +113,7 @@ public class StdioServerTransportTests
public async Task ReadMessagesAsync_Should_Read_Messages() public async Task ReadMessagesAsync_Should_Read_Messages()
{ {
var message = new JsonRpcRequest { Method = "test", Id = RequestId.FromNumber(44) }; 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; TextReader oldIn = Console.In;
TextWriter oldOut = Console.Out; TextWriter oldOut = Console.Out;