Turn on the AOT analyzer. (#92)

* Turn on the AOT analyzer.

* Address feedback

* Add missing types.

* Fix build and react to conflicts.

* Fix failing test.

* Address feedback.
This commit is contained in:
Eirik Tsarpalis 2025-03-18 18:48:28 +02:00 • committed by GitHub
commit e7fa6194f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 522 additions and 68 deletions

View file

@ -0,0 +1,164 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Specifies the types of members that are dynamically accessed.
///
/// This enumeration has a <see cref="FlagsAttribute"/> attribute that allows a
/// bitwise combination of its member values.
/// </summary>
[Flags]
internal enum DynamicallyAccessedMemberTypes
{
/// <summary>
/// Specifies no members.
/// </summary>
None = 0,
/// <summary>
/// Specifies the default, parameterless public constructor.
/// </summary>
PublicParameterlessConstructor = 0x0001,
/// <summary>
/// Specifies all public constructors.
/// </summary>
PublicConstructors = 0x0002 | PublicParameterlessConstructor,
/// <summary>
/// Specifies all non-public constructors.
/// </summary>
NonPublicConstructors = 0x0004,
/// <summary>
/// Specifies all public methods.
/// </summary>
PublicMethods = 0x0008,
/// <summary>
/// Specifies all non-public methods.
/// </summary>
NonPublicMethods = 0x0010,
/// <summary>
/// Specifies all public fields.
/// </summary>
PublicFields = 0x0020,
/// <summary>
/// Specifies all non-public fields.
/// </summary>
NonPublicFields = 0x0040,
/// <summary>
/// Specifies all public nested types.
/// </summary>
PublicNestedTypes = 0x0080,
/// <summary>
/// Specifies all non-public nested types.
/// </summary>
NonPublicNestedTypes = 0x0100,
/// <summary>
/// Specifies all public properties.
/// </summary>
PublicProperties = 0x0200,
/// <summary>
/// Specifies all non-public properties.
/// </summary>
NonPublicProperties = 0x0400,
/// <summary>
/// Specifies all public events.
/// </summary>
PublicEvents = 0x0800,
/// <summary>
/// Specifies all non-public events.
/// </summary>
NonPublicEvents = 0x1000,
/// <summary>
/// Specifies all interfaces implemented by the type.
/// </summary>
Interfaces = 0x2000,
/// <summary>
/// Specifies all non-public constructors, including those inherited from base classes.
/// </summary>
NonPublicConstructorsWithInherited = NonPublicConstructors | 0x4000,
/// <summary>
/// Specifies all non-public methods, including those inherited from base classes.
/// </summary>
NonPublicMethodsWithInherited = NonPublicMethods | 0x8000,
/// <summary>
/// Specifies all non-public fields, including those inherited from base classes.
/// </summary>
NonPublicFieldsWithInherited = NonPublicFields | 0x10000,
/// <summary>
/// Specifies all non-public nested types, including those inherited from base classes.
/// </summary>
NonPublicNestedTypesWithInherited = NonPublicNestedTypes | 0x20000,
/// <summary>
/// Specifies all non-public properties, including those inherited from base classes.
/// </summary>
NonPublicPropertiesWithInherited = NonPublicProperties | 0x40000,
/// <summary>
/// Specifies all non-public events, including those inherited from base classes.
/// </summary>
NonPublicEventsWithInherited = NonPublicEvents | 0x80000,
/// <summary>
/// Specifies all public constructors, including those inherited from base classes.
/// </summary>
PublicConstructorsWithInherited = PublicConstructors | 0x100000,
/// <summary>
/// Specifies all public nested types, including those inherited from base classes.
/// </summary>
PublicNestedTypesWithInherited = PublicNestedTypes | 0x200000,
/// <summary>
/// Specifies all constructors, including those inherited from base classes.
/// </summary>
AllConstructors = PublicConstructorsWithInherited | NonPublicConstructorsWithInherited,
/// <summary>
/// Specifies all methods, including those inherited from base classes.
/// </summary>
AllMethods = PublicMethods | NonPublicMethodsWithInherited,
/// <summary>
/// Specifies all fields, including those inherited from base classes.
/// </summary>
AllFields = PublicFields | NonPublicFieldsWithInherited,
/// <summary>
/// Specifies all nested types, including those inherited from base classes.
/// </summary>
AllNestedTypes = PublicNestedTypesWithInherited | NonPublicNestedTypesWithInherited,
/// <summary>
/// Specifies all properties, including those inherited from base classes.
/// </summary>
AllProperties = PublicProperties | NonPublicPropertiesWithInherited,
/// <summary>
/// Specifies all events, including those inherited from base classes.
/// </summary>
AllEvents = PublicEvents | NonPublicEventsWithInherited,
/// <summary>
/// Specifies all members.
/// </summary>
All = ~None
}

View file

@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that certain members on a specified <see cref="Type"/> are accessed dynamically,
/// for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which members are being accessed during the execution
/// of a program.
///
/// This attribute is valid on members whose type is <see cref="Type"/> or <see cref="string"/>.
///
/// When this attribute is applied to a location of type <see cref="string"/>, the assumption is
/// that the string represents a fully qualified type name.
///
/// When this attribute is applied to a class, interface, or struct, the members specified
/// can be accessed dynamically on <see cref="Type"/> instances returned from calling
/// <see cref="object.GetType"/> on instances of that class, interface, or struct.
///
/// If the attribute is applied to a method it's treated as a special case and it implies
/// the attribute should be applied to the "this" parameter of the method. As such the attribute
/// should only be used on instance methods of types assignable to System.Type (or string, but no methods
/// will use it there).
/// </remarks>
[AttributeUsage(
AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter |
AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method |
AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
Inherited = false)]
internal sealed class DynamicallyAccessedMembersAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DynamicallyAccessedMembersAttribute"/> class
/// with the specified member types.
/// </summary>
/// <param name="memberTypes">The types of members dynamically accessed.</param>
public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
{
MemberTypes = memberTypes;
}
/// <summary>
/// Gets the <see cref="DynamicallyAccessedMemberTypes"/> which specifies the type
/// of members dynamically accessed.
/// </summary>
public DynamicallyAccessedMemberTypes MemberTypes { get; }
}

View file

@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that the specified method requires the ability to generate new code at runtime,
/// for example through <see cref="Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when compiling ahead of time.
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
internal sealed class RequiresDynamicCodeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresDynamicCodeAttribute"/> class
/// with the specified message.
/// </summary>
/// <param name="message">
/// A message that contains information about the usage of dynamic code.
/// </param>
public RequiresDynamicCodeAttribute(string message)
{
Message = message;
}
/// <summary>
/// Gets a message that contains information about the usage of dynamic code.
/// </summary>
public string Message { get; }
/// <summary>
/// Gets or sets an optional URL that contains more information about the method,
/// why it requires dynamic code, and what options a consumer has to deal with it.
/// </summary>
public string? Url { get; set; }
}

View file

@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// Indicates that the specified method requires dynamic access to code that is not referenced
/// statically, for example through <see cref="System.Reflection"/>.
/// </summary>
/// <remarks>
/// This allows tools to understand which methods are unsafe to call when removing unreferenced
/// code from an application.
/// </remarks>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
internal sealed class RequiresUnreferencedCodeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresUnreferencedCodeAttribute"/> class
/// with the specified message.
/// </summary>
/// <param name="message">
/// A message that contains information about the usage of unreferenced code.
/// </param>
public RequiresUnreferencedCodeAttribute(string message)
{
Message = message;
}
/// <summary>
/// Gets a message that contains information about the usage of unreferenced code.
/// </summary>
public string Message { get; }
/// <summary>
/// Gets or sets an optional URL that contains more information about the method,
/// why it requires unreferenced code, and what options a consumer has to deal with it.
/// </summary>
public string? Url { get; set; }
}

View file

@ -0,0 +1,84 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Diagnostics.CodeAnalysis;
/// <summary>
/// /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a
/// single code artifact.
/// </summary>
/// <remarks>
/// <see cref="UnconditionalSuppressMessageAttribute"/> is different than
/// <see cref="SuppressMessageAttribute"/> in that it doesn't have a
/// <see cref="ConditionalAttribute"/>. So it is always preserved in the compiled assembly.
/// </remarks>
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
internal sealed class UnconditionalSuppressMessageAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="UnconditionalSuppressMessageAttribute"/>
/// class, specifying the category of the tool and the identifier for an analysis rule.
/// </summary>
/// <param name="category">The category for the attribute.</param>
/// <param name="checkId">The identifier of the analysis rule the attribute applies to.</param>
public UnconditionalSuppressMessageAttribute(string category, string checkId)
{
Category = category;
CheckId = checkId;
}
/// <summary>
/// Gets the category identifying the classification of the attribute.
/// </summary>
/// <remarks>
/// The <see cref="Category"/> property describes the tool or tool analysis category
/// for which a message suppression attribute applies.
/// </remarks>
public string Category { get; }
/// <summary>
/// Gets the identifier of the analysis tool rule to be suppressed.
/// </summary>
/// <remarks>
/// Concatenated together, the <see cref="Category"/> and <see cref="CheckId"/>
/// properties form a unique check identifier.
/// </remarks>
public string CheckId { get; }
/// <summary>
/// Gets or sets the scope of the code that is relevant for the attribute.
/// </summary>
/// <remarks>
/// The Scope property is an optional argument that specifies the metadata scope for which
/// the attribute is relevant.
/// </remarks>
public string? Scope { get; set; }
/// <summary>
/// Gets or sets a fully qualified path that represents the target of the attribute.
/// </summary>
/// <remarks>
/// The <see cref="Target"/> property is an optional argument identifying the analysis target
/// of the attribute. An example value is "System.IO.Stream.ctor():System.Void".
/// Because it is fully qualified, it can be long, particularly for targets such as parameters.
/// The analysis tool user interface should be capable of automatically formatting the parameter.
/// </remarks>
public string? Target { get; set; }
/// <summary>
/// Gets or sets an optional argument expanding on exclusion criteria.
/// </summary>
/// <remarks>
/// The <see cref="MessageId"/> property is an optional argument that specifies additional
/// exclusion where the literal metadata target is not sufficiently precise. For example,
/// the <see cref="UnconditionalSuppressMessageAttribute"/> cannot be applied within a method,
/// and it may be desirable to suppress a violation against a statement in the method that will
/// give a rule violation, but not against all statements in the method.
/// </remarks>
public string? MessageId { get; set; }
/// <summary>
/// Gets or sets the justification for suppressing the code analysis message.
/// </summary>
public string? Justification { get; set; }
}

View file

@ -9,6 +9,10 @@
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup>
<!-- license and package properties -->
<PackageId>McpDotNet.Extensions.AI</PackageId>
@ -37,4 +41,8 @@
<PackageReference Include="System.Text.Json" Version="9.0.3" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<Compile Include="..\Common\Polyfills\**\*.cs" />
</ItemGroup>
</Project>

View file

@ -4,10 +4,9 @@ using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Shared;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text.Json;
namespace McpDotNet.Client;
@ -139,7 +138,10 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
initializationCts.Token).ConfigureAwait(false);
// Store server information
_logger.ServerCapabilitiesReceived(EndpointName, JsonSerializer.Serialize(initializeResponse.Capabilities), JsonSerializer.Serialize(initializeResponse.ServerInfo));
_logger.ServerCapabilitiesReceived(EndpointName,
capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, JsonSerializerOptionsExtensions.JsonContext.Default.ServerCapabilities),
serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, JsonSerializerOptionsExtensions.JsonContext.Default.Implementation));
ServerCapabilities = initializeResponse.Capabilities;
ServerInfo = initializeResponse.ServerInfo;
ServerInstructions = initializeResponse.Instructions;

View file

@ -1,6 +1,7 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
using McpDotNet.Utils;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;
using System.Text.Json;
@ -465,6 +466,8 @@ public static class McpClientExtensions
/// <summary>Provides an AI function that calls a tool through <see cref="IMcpClient"/>.</summary>
private sealed class McpAIFunction(IMcpClient client, Tool tool) : AIFunction
{
private JsonElement? _jsonSchema;
/// <inheritdoc/>
public override string Name => tool.Name;
@ -472,7 +475,7 @@ public static class McpClientExtensions
public override string Description => tool.Description ?? string.Empty;
/// <inheritdoc/>
public override JsonElement JsonSchema =>
public override JsonElement JsonSchema => _jsonSchema ??=
JsonSerializer.SerializeToElement(new Dictionary<string, object>
{
["type"] = "object",
@ -480,7 +483,7 @@ public static class McpClientExtensions
["description"] = tool.Description ?? string.Empty,
["properties"] = tool.InputSchema?.Properties ?? [],
["required"] = tool.InputSchema?.Required ?? []
});
}, JsonSerializerOptionsExtensions.JsonContext.Default.DictionaryStringObject);
/// <inheritdoc/>
protected async override Task<object?> InvokeCoreAsync(
@ -498,8 +501,7 @@ public static class McpClientExtensions
}
CallToolResponse result = await client.CallToolAsync(tool.Name, argDict, cancellationToken).ConfigureAwait(false);
return JsonSerializer.SerializeToElement(result);
return JsonSerializer.SerializeToElement(result, JsonSerializerOptionsExtensions.JsonContext.Default.CallToolResponse);
}
}
}

View file

@ -2,9 +2,9 @@
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using McpDotNet.Utils;
using McpDotNet.Utils.Json;
using Microsoft.Extensions.AI;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
@ -15,21 +15,28 @@ namespace McpDotNet;
/// </summary>
public static partial class McpServerBuilderExtensions
{
private const string RequiresUnreferencedCodeMessage = "This method requires dynamic lookup of method metadata and might not work in Native AOT.";
/// <summary>
/// Adds a tool to the server.
/// </summary>
/// <typeparam name="TTool">The tool type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
public static IMcpServerBuilder WithTool<TTool>(this IMcpServerBuilder builder)
public static IMcpServerBuilder WithTool<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TTool>(this IMcpServerBuilder builder)
{
return WithTools(builder, typeof(TTool));
Throw.IfNull(builder);
List<AIFunction> functions = [];
PopulateFunctions(typeof(TTool), functions);
return WithTools(builder, functions);
}
/// <summary>
/// Adds all tools marked with <see cref="McpToolTypeAttribute"/> from the current assembly to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder)
{
return WithToolsFromAssembly(builder, Assembly.GetCallingAssembly());
@ -42,6 +49,7 @@ public static partial class McpServerBuilderExtensions
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="toolTypes"/> is <see langword="null"/>.</exception>
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<Type> toolTypes)
{
Throw.IfNull(builder);
@ -56,18 +64,7 @@ public static partial class McpServerBuilderExtensions
throw new ArgumentNullException(nameof(toolTypes), $"A tool type provided by the enumerator was null.");
}
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
if (method.GetCustomAttribute<McpToolAttribute>() is not { } attribute)
{
continue;
}
functions.Add(AIFunctionFactory.Create(method, target: null, new()
{
Name = attribute.Name ?? method.Name,
}));
}
PopulateFunctions(toolType, functions);
}
return WithTools(builder, functions);
@ -99,7 +96,7 @@ public static partial class McpServerBuilderExtensions
{
Name = function.Name,
Description = function.Description,
InputSchema = JsonSerializer.Deserialize<JsonSchema>(function.JsonSchema),
InputSchema = JsonSerializer.Deserialize(function.JsonSchema, JsonSerializerOptionsExtensions.JsonContext.Default.JsonSchema),
});
callbacks.Add(function.Name, async (request, cancellationToken) =>
@ -155,6 +152,7 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param>
/// <param name="assembly">The assembly to load the types from. Null to get the current assembly</param>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithToolsFromAssembly(this IMcpServerBuilder builder, Assembly? assembly = null)
{
assembly ??= Assembly.GetCallingAssembly();
@ -182,4 +180,22 @@ public static partial class McpServerBuilderExtensions
WithTools(builder, toolTypes) :
builder;
}
private static void PopulateFunctions(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type toolType,
List<AIFunction> functions)
{
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
if (method.GetCustomAttribute<McpToolAttribute>() is not { } attribute)
{
continue;
}
functions.Add(AIFunctionFactory.Create(method, target: null, new()
{
Name = attribute.Name ?? method.Name,
}));
}
}
}

View file

@ -1,8 +1,12 @@
namespace McpDotNet.Protocol.Messages;
using McpDotNet.Utils.Json;
using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Messages;
/// <summary>
/// Base interface for all JSON-RPC messages in the MCP protocol.
/// </summary>
[JsonConverter(typeof(JsonRpcMessageConverter))]
public interface IJsonRpcMessage
{
/// <summary>

View file

@ -77,7 +77,7 @@ public sealed class HttpListenerSseServerTransport : TransportBase, IServerTrans
try
{
var json = JsonSerializer.Serialize(message, _jsonOptions);
var json = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
_logger.TransportSendingMessage(EndpointName, id, json);
await _httpServerProvider.SendEvent(json, "message").ConfigureAwait(false);
@ -125,7 +125,7 @@ public sealed class HttpListenerSseServerTransport : TransportBase, IServerTrans
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(request, _jsonOptions);
var message = JsonSerializer.Deserialize(request, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
if (message != null)
{
// Fire-and-forget the message to the message channel

View file

@ -110,7 +110,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
throw new InvalidOperationException("Transport not connected");
using var content = new StringContent(
JsonSerializer.Serialize(message, _jsonOptions),
JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()),
Encoding.UTF8,
"application/json"
);
@ -143,7 +143,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
}
else
{
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize<JsonRpcResponse>(responseContent, _jsonOptions) ??
JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, _jsonOptions.GetTypeInfo<JsonRpcResponse>()) ??
throw new McpTransportException("Failed to initialize client");
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
@ -277,7 +277,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(data, _jsonOptions);
var message = JsonSerializer.Deserialize(data, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
if (message == null)
{
_logger.TransportMessageParseUnexpectedType(EndpointName, data);

View file

@ -134,7 +134,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
try
{
var json = JsonSerializer.Serialize(message, _jsonOptions);
var json = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
_logger.TransportSendingMessage(EndpointName, id, json);
// Write the message followed by a newline
@ -206,7 +206,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
try
{
line=line.Trim();//Fixes an error when the service prefixes nonprintable characters
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
var message = JsonSerializer.Deserialize(line, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
if (message != null)
{
string messageId = "(no id)";

View file

@ -117,7 +117,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
try
{
var json = JsonSerializer.Serialize(message, _jsonOptions);
var json = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
_logger.TransportSendingMessage(EndpointName, id, json);
await _stdout.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
@ -166,7 +166,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport
try
{
var message = JsonSerializer.Deserialize<IJsonRpcMessage>(line, _jsonOptions);
var message = JsonSerializer.Deserialize(line, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
if (message != null)
{
string messageId = "(no id)";

View file

@ -95,7 +95,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
}
catch (Exception ex)
{
var payload = JsonSerializer.Serialize(message);
var payload = JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>());
_logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, ex);
}
}
@ -236,7 +236,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
// Expensive logging, use the logging framework to check if the logger is enabled
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request));
_logger.SendingRequestPayload(EndpointName, JsonSerializer.Serialize(request, _jsonOptions.GetTypeInfo<JsonRpcRequest>()));
}
// Less expensive information logging
@ -256,8 +256,8 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
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);
var resultObject = JsonSerializer.Deserialize<TResult>(resultJson, _jsonOptions);
var resultJson = JsonSerializer.Serialize(success.Result, _jsonOptions.GetTypeInfo<object?>());
var resultObject = JsonSerializer.Deserialize(resultJson, _jsonOptions.GetTypeInfo<TResult>());
// Not expensive logging because we're already converting to JSON in order to get the result object
_logger.RequestResponseReceivedPayload(EndpointName, resultJson);
@ -270,7 +270,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
// Result object was null, this is unexpected
_logger.RequestResponseTypeConversionError(EndpointName, request.Method, typeof(TResult));
throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result)}, expected {typeof(TResult)}");
throw new McpClientException($"Unexpected response type {JsonSerializer.Serialize(success.Result, _jsonOptions.GetTypeInfo<TResult>())}, expected {typeof(TResult)}");
}
// Unexpected response type
@ -295,7 +295,7 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message));
_logger.SendingMessage(EndpointName, JsonSerializer.Serialize(message, _jsonOptions.GetTypeInfo<IJsonRpcMessage>()));
}
return _transport.SendMessageAsync(message, cancellationToken);
@ -339,8 +339,8 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
_requestHandlers[method] = async (request, cancellationToken) =>
{
// Convert the params JsonElement to our type using the same options
var jsonString = JsonSerializer.Serialize(request.Params);
var typedRequest = JsonSerializer.Deserialize<TRequest>(jsonString, _jsonOptions);
var jsonString = JsonSerializer.Serialize(request.Params, _jsonOptions.GetTypeInfo<object?>());
var typedRequest = JsonSerializer.Deserialize(jsonString, _jsonOptions.GetTypeInfo<TRequest>());
return await handler(typedRequest, cancellationToken).ConfigureAwait(false);
};

View file

@ -1,13 +1,15 @@
using System.Text.Json;
using McpDotNet.Protocol.Messages;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
using McpDotNet.Protocol.Messages;
namespace McpDotNet.Utils.Json;
/// <summary>
/// JSON converter for IJsonRpcMessage that handles polymorphic deserialization of different message types.
/// </summary>
internal sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
{
/// <inheritdoc/>
public override IJsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@ -40,13 +42,13 @@ internal sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
// Messages with an error property are error responses
if (hasError)
{
return JsonSerializer.Deserialize<JsonRpcError>(rawText, options);
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcError>());
}
// Messages with a result property are success responses
if (root.TryGetProperty("result", out _))
{
return JsonSerializer.Deserialize<JsonRpcResponse>(rawText, options);
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcResponse>());
}
throw new JsonException("Response must have either result or error");
@ -55,13 +57,13 @@ internal sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
// Messages with a method but no id are notifications
if (hasMethod && !hasId)
{
return JsonSerializer.Deserialize<JsonRpcNotification>(rawText, options);
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcNotification>());
}
// Messages with both method and id are requests
if (hasMethod && hasId)
{
return JsonSerializer.Deserialize<JsonRpcRequest>(rawText, options);
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcRequest>());
}
throw new JsonException("Invalid JSON-RPC message format");
@ -73,16 +75,16 @@ internal sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
switch (value)
{
case JsonRpcRequest request:
JsonSerializer.Serialize(writer, request, options);
JsonSerializer.Serialize(writer, request, options.GetTypeInfo<JsonRpcRequest>());
break;
case JsonRpcNotification notification:
JsonSerializer.Serialize(writer, notification, options);
JsonSerializer.Serialize(writer, notification, options.GetTypeInfo<JsonRpcNotification>());
break;
case JsonRpcResponse response:
JsonSerializer.Serialize(writer, response, options);
JsonSerializer.Serialize(writer, response, options.GetTypeInfo<JsonRpcResponse>());
break;
case JsonRpcError error:
JsonSerializer.Serialize(writer, error, options);
JsonSerializer.Serialize(writer, error, options.GetTypeInfo<JsonRpcError>());
break;
default:
throw new JsonException($"Unknown JSON-RPC message type: {value.GetType()}");

View file

@ -1,12 +1,17 @@
using System.Text.Json;
using McpDotNet.Protocol.Messages;
using McpDotNet.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 McpDotNet.Utils.Json;
/// <summary>
/// Extensions for configuring System.Text.Json serialization options for MCP.
/// </summary>
internal static class JsonSerializerOptionsExtensions
internal static partial class JsonSerializerOptionsExtensions
{
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
@ -14,19 +19,54 @@ internal static class JsonSerializerOptionsExtensions
/// 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()
{
JsonSerializerOptions options = new();
// 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.
// Add custom converters
options.Converters.Add(new JsonRpcMessageConverter());
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
if (JsonSerializer.IsReflectionEnabledByDefault)
{
// Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below.
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
Converters = { new JsonStringEnumConverter() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
// Configure general options
options.PropertyNameCaseInsensitive = true;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.NumberHandling = JsonNumberHandling.AllowReadingFromString;
options.MakeReadOnly();
return options;
}
return options;
return JsonContext.Default.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(Implementation))]
[JsonSerializable(typeof(CreateMessageResult))]
[JsonSerializable(typeof(ListRootsResult))]
[JsonSerializable(typeof(InitializeResult))]
[JsonSerializable(typeof(JsonSchema))]
[JsonSerializable(typeof(CallToolResponse))]
internal sealed partial class JsonContext : JsonSerializerContext;
}

View file

@ -9,6 +9,10 @@
<AnalysisMode>All</AnalysisMode>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup>
<!-- license and package properties -->
<PackageId>mcpdotnet</PackageId>

View file

@ -2,7 +2,6 @@
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Serilog;
using System.Text;

View file

@ -287,7 +287,7 @@ public class McpServerBuilderExtensionsToolsTests
var tool = result.Tools.First(t => t.Name == "TestTool");
Assert.Equal("TestTool", tool.Name);
Assert.Empty(tool.Description);
Assert.Empty(tool.Description!);
Assert.NotNull(tool.InputSchema);
Assert.Equal("object", tool.InputSchema.Type);
Assert.NotNull(tool.InputSchema.Properties);

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