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:
parent
c0e1d0dd9a
commit
e7fa6194f6
22 changed files with 522 additions and 68 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)";
|
||||
|
|
|
|||
|
|
@ -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)";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue