csharp-sdk/src/ModelContextProtocol/Utils/Json/McpJsonUtilities.cs
Stephen Toub 2c1672cc1a
Fix some issues with Native AOT (#130)
* Fix some issues with Native AOT

- An anonymous type was being used in McpClient for the initialize result message. Switched to use the type for that purpose that already existed.
- A bunch of the request params / result types were not in the source context or referenced from types that were or from custom converters. Added all the request/result types to the context.
- Improved the warning message for WithTools.
2025-03-28 09:50:56 -04:00

161 lines
7 KiB
C#

using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Types;
using System.Diagnostics.CodeAnalysis;
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>
/// </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,
};
}
else
{
// Keep in sync with any additional settings above beyond what's in JsonContext below.
options = new(JsonContext.Default.Options)
{
};
}
// Include all types from AIJsonUtilities, so that anything default usable as part of an AIFunction
// is also usable as part of an McpServerTool.
options.TypeInfoResolverChain.Add(AIJsonUtilities.DefaultOptions.TypeInfoResolver!);
options.MakeReadOnly();
return options;
}
internal static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options) =>
(JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
internal static JsonElement DefaultMcpToolSchema { get; } = ParseJsonElement("""{"type":"object"}"""u8);
internal static bool IsValidMcpToolSchema(JsonElement element)
{
if (element.ValueKind is not JsonValueKind.Object)
{
return false;
}
foreach (JsonProperty property in element.EnumerateObject())
{
if (property.NameEquals("type"))
{
if (property.Value.ValueKind is not JsonValueKind.String ||
!property.Value.ValueEquals("object"))
{
return false;
}
return true; // No need to check other properties
}
}
return false; // No type keyword found.
}
// Keep in sync with CreateDefaultOptions above.
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
UseStringEnumConverter = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
// JSON
[JsonSerializable(typeof(JsonDocument))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(JsonNode))]
// JSON-RPC
[JsonSerializable(typeof(IJsonRpcMessage))]
[JsonSerializable(typeof(JsonRpcRequest))]
[JsonSerializable(typeof(JsonRpcNotification))]
[JsonSerializable(typeof(JsonRpcResponse))]
[JsonSerializable(typeof(JsonRpcError))]
// MCP Request Params / Results
[JsonSerializable(typeof(CallToolRequestParams))]
[JsonSerializable(typeof(CallToolResponse))]
[JsonSerializable(typeof(CompleteRequestParams))]
[JsonSerializable(typeof(CompleteResult))]
[JsonSerializable(typeof(CreateMessageRequestParams))]
[JsonSerializable(typeof(CreateMessageResult))]
[JsonSerializable(typeof(EmptyResult))]
[JsonSerializable(typeof(GetPromptRequestParams))]
[JsonSerializable(typeof(GetPromptResult))]
[JsonSerializable(typeof(InitializeRequestParams))]
[JsonSerializable(typeof(InitializeResult))]
[JsonSerializable(typeof(ListPromptsRequestParams))]
[JsonSerializable(typeof(ListPromptsResult))]
[JsonSerializable(typeof(ListResourcesRequestParams))]
[JsonSerializable(typeof(ListResourcesResult))]
[JsonSerializable(typeof(ListResourceTemplatesRequestParams))]
[JsonSerializable(typeof(ListResourceTemplatesResult))]
[JsonSerializable(typeof(ListRootsRequestParams))]
[JsonSerializable(typeof(ListRootsResult))]
[JsonSerializable(typeof(ListToolsRequestParams))]
[JsonSerializable(typeof(ListToolsResult))]
[JsonSerializable(typeof(LoggingMessageNotificationParams))]
[JsonSerializable(typeof(PingResult))]
[JsonSerializable(typeof(ReadResourceRequestParams))]
[JsonSerializable(typeof(ReadResourceResult))]
[JsonSerializable(typeof(ResourceUpdatedNotificationParams))]
[JsonSerializable(typeof(SetLevelRequestParams))]
[JsonSerializable(typeof(SubscribeRequestParams))]
[JsonSerializable(typeof(UnsubscribeFromResourceRequestParams))]
[JsonSerializable(typeof(UnsubscribeRequestParams))]
[ExcludeFromCodeCoverage]
internal sealed partial class JsonContext : JsonSerializerContext;
private static JsonElement ParseJsonElement(ReadOnlySpan<byte> utf8Json)
{
Utf8JsonReader reader = new(utf8Json);
return JsonElement.ParseValue(ref reader);
}
}