Add [McpServerTool] support for instance methods (#100)
* Add [McpServerTool] support for instance methods By popular demand, enables [McpServerTool] on instance methods. The instance is constructed per invocation, using ActivatorUtilities.CreateInstance when possible so that constructor arguments are satisfied from DI. * Add disposal tests
This commit is contained in:
parent
6fa1443a6c
commit
b61cdef994
3 changed files with 391 additions and 38 deletions
|
|
@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
@ -13,7 +14,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
{
|
||||
/// <summary>Key used temporarily for flowing request context into an AIFunction.</summary>
|
||||
/// <remarks>This will be replaced with use of AIFunctionArguments.Context.</remarks>
|
||||
private const string RequestContextKey = "__temporary_RequestContext";
|
||||
internal const string RequestContextKey = "__temporary_RequestContext";
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
|
|
@ -48,7 +49,27 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
// AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by
|
||||
// which the arguments are passed.
|
||||
|
||||
return Create(TemporaryAIFunctionFactory.Create(method, target, new TemporaryAIFunctionFactoryOptions()
|
||||
return Create(TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, name, description, services)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
|
||||
/// </summary>
|
||||
public static new AIFunctionMcpServerTool Create(
|
||||
MethodInfo method,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
IServiceProvider? services = null)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
return Create(TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, name, description, services)));
|
||||
}
|
||||
|
||||
private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
|
||||
MethodInfo method, string? name, string? description, IServiceProvider? services) =>
|
||||
new TemporaryAIFunctionFactoryOptions()
|
||||
{
|
||||
Name = name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
|
||||
Description = description,
|
||||
|
|
@ -115,8 +136,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
|||
return null;
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
|
||||
public static new AIFunctionMcpServerTool Create(AIFunction function)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ModelContextProtocol.Server;
|
||||
|
|
@ -40,7 +41,7 @@ public abstract class McpServerTool
|
|||
/// </param>
|
||||
/// <param name="services">
|
||||
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
|
||||
/// used to determine which parameters should be satisifed from dependency injection, and so what services
|
||||
/// used to determine which parameters should be satisifed from dependency injection; what services
|
||||
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
|
||||
/// </param>
|
||||
/// <returns>The created <see cref="McpServerTool"/> for invoking <paramref name="method"/>.</returns>
|
||||
|
|
@ -68,7 +69,7 @@ public abstract class McpServerTool
|
|||
/// </param>
|
||||
/// <param name="services">
|
||||
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
|
||||
/// used to determine which parameters should be satisifed from dependency injection, and so what services
|
||||
/// used to determine which parameters should be satisifed from dependency injection; what services
|
||||
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
|
||||
/// </param>
|
||||
/// <returns>The created <see cref="McpServerTool"/> for invoking <paramref name="method"/>.</returns>
|
||||
|
|
@ -82,6 +83,43 @@ public abstract class McpServerTool
|
|||
IServiceProvider? services = null) =>
|
||||
AIFunctionMcpServerTool.Create(method, target, name, description, services);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via an <see cref="MethodInfo"/> for
|
||||
/// and instance method, along with a <see cref="Type"/> representing the type of the target object to
|
||||
/// instantiate each time the method is invoked.
|
||||
/// </summary>
|
||||
/// <param name="method">The instance method to be represented via the created <see cref="AIFunction"/>.</param>
|
||||
/// <param name="targetType">
|
||||
/// The <see cref="Type"/> to construct an instance of on which to invoke <paramref name="method"/> when
|
||||
/// the resulting <see cref="AIFunction"/> is invoked. If services are provided,
|
||||
/// ActivatorUtilities.CreateInstance will be used to construct the instance using those services; otherwise,
|
||||
/// <see cref="Activator.CreateInstance(Type)"/> is used, utilizing the type's public parameterless constructor.
|
||||
/// If an instance can't be constructed, an exception is thrown during the function's invocation.
|
||||
/// </param>
|
||||
/// <param name="name">
|
||||
/// The name to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but an <see cref="McpServerToolAttribute"/>
|
||||
/// is applied to <paramref name="method"/>, the name from the attribute will be used. If that's not present, the name based
|
||||
/// on <paramref name="method"/>'s name will be used.
|
||||
/// </param>
|
||||
/// <param name="description">
|
||||
/// The description to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but a <see cref="DescriptionAttribute"/>
|
||||
/// is applied to <paramref name="method"/>, the description from that attribute will be used.
|
||||
/// </param>
|
||||
/// <param name="services">
|
||||
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
|
||||
/// used to determine which parameters should be satisifed from dependency injection; what services
|
||||
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
|
||||
/// </param>
|
||||
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
|
||||
public static McpServerTool Create(
|
||||
MethodInfo method,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
|
||||
string? name = null,
|
||||
string? description = null,
|
||||
IServiceProvider? services = null) =>
|
||||
AIFunctionMcpServerTool.Create(method, targetType, name, description, services);
|
||||
|
||||
/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
|
||||
/// <param name="function">The function to wrap.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Server;
|
||||
using ModelContextProtocol.Utils;
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
#if !NET
|
||||
using System.Linq;
|
||||
#endif
|
||||
|
|
@ -110,6 +115,42 @@ internal static partial class TemporaryAIFunctionFactory
|
|||
return ReflectionAIFunction.Build(method, target, options ?? _defaultOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AIFunction"/> instance for a method, specified via an <see cref="MethodInfo"/> instance
|
||||
/// and an optional target object if the method is an instance method.
|
||||
/// </summary>
|
||||
/// <param name="method">The instance method to be represented via the created <see cref="AIFunction"/>.</param>
|
||||
/// <param name="targetType">
|
||||
/// The <see cref="Type"/> to construct an instance of on which to invoke <paramref name="method"/> when
|
||||
/// the resulting <see cref="AIFunction"/> is invoked. If services are provided,
|
||||
/// ActivatorUtilities.CreateInstance will be used to construct the instance using those services; otherwise,
|
||||
/// <see cref="Activator.CreateInstance(Type)"/> is used, utilizing the type's public parameterless constructor.
|
||||
/// If an instance can't be constructed, an exception is thrown during the function's invocation.
|
||||
/// </param>
|
||||
/// <param name="options">Metadata to use to override defaults inferred from <paramref name="method"/>.</param>
|
||||
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Return values are serialized to <see cref="JsonElement"/> using <paramref name="options"/>'s
|
||||
/// <see cref="AIFunctionFactoryOptions.SerializerOptions"/>. Arguments that are not already of the expected type are
|
||||
/// marshaled to the expected type via JSON and using <paramref name="options"/>'s
|
||||
/// <see cref="AIFunctionFactoryOptions.SerializerOptions"/>. If the argument is a <see cref="JsonElement"/>,
|
||||
/// <see cref="JsonDocument"/>, or <see cref="JsonNode"/>, it is deserialized directly. If the argument is anything else unknown,
|
||||
/// it is round-tripped through JSON, serializing the object as JSON and then deserializing it to the expected type.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
|
||||
public static AIFunction Create(
|
||||
MethodInfo method,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
|
||||
TemporaryAIFunctionFactoryOptions? options = null)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
Throw.IfNull(targetType);
|
||||
|
||||
return ReflectionAIFunction.Build(method, targetType, options ?? _defaultOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="AIFunction"/> instance for a method, specified via an <see cref="MethodInfo"/> instance
|
||||
/// and an optional target object if the method is an instance method.
|
||||
|
|
@ -176,6 +217,32 @@ internal static partial class TemporaryAIFunctionFactory
|
|||
return new(functionDescriptor, target, options);
|
||||
}
|
||||
|
||||
public static ReflectionAIFunction Build(
|
||||
MethodInfo method,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
|
||||
TemporaryAIFunctionFactoryOptions options)
|
||||
{
|
||||
Throw.IfNull(method);
|
||||
|
||||
if (method.ContainsGenericParameters)
|
||||
{
|
||||
throw new ArgumentException("Open generic methods are not supported", nameof(method));
|
||||
}
|
||||
|
||||
if (method.IsStatic)
|
||||
{
|
||||
throw new ArgumentException("The method must be an instance method.", nameof(method));
|
||||
}
|
||||
|
||||
if (method.DeclaringType is { } declaringType &&
|
||||
!declaringType.IsAssignableFrom(targetType))
|
||||
{
|
||||
throw new ArgumentException("The target type must be assignable to the method's declaring type.", nameof(targetType));
|
||||
}
|
||||
|
||||
return new(ReflectionAIFunctionDescriptor.GetOrCreate(method, options), targetType, options);
|
||||
}
|
||||
|
||||
private ReflectionAIFunction(ReflectionAIFunctionDescriptor functionDescriptor, object? target, TemporaryAIFunctionFactoryOptions options)
|
||||
{
|
||||
FunctionDescriptor = functionDescriptor;
|
||||
|
|
@ -183,8 +250,20 @@ internal static partial class TemporaryAIFunctionFactory
|
|||
AdditionalProperties = options.AdditionalProperties ?? new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
private ReflectionAIFunction(
|
||||
ReflectionAIFunctionDescriptor functionDescriptor,
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
|
||||
TemporaryAIFunctionFactoryOptions options)
|
||||
{
|
||||
FunctionDescriptor = functionDescriptor;
|
||||
TargetType = targetType;
|
||||
AdditionalProperties = options.AdditionalProperties ?? new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public ReflectionAIFunctionDescriptor FunctionDescriptor { get; }
|
||||
public object? Target { get; }
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
|
||||
public Type? TargetType { get; }
|
||||
public override IReadOnlyDictionary<string, object?> AdditionalProperties { get; }
|
||||
public override string Name => FunctionDescriptor.Name;
|
||||
public override string Description => FunctionDescriptor.Description;
|
||||
|
|
@ -192,22 +271,59 @@ internal static partial class TemporaryAIFunctionFactory
|
|||
public override JsonElement JsonSchema => FunctionDescriptor.JsonSchema;
|
||||
public override JsonSerializerOptions JsonSerializerOptions => FunctionDescriptor.JsonSerializerOptions;
|
||||
|
||||
protected override Task<object?> InvokeCoreAsync(
|
||||
protected override async Task<object?> InvokeCoreAsync(
|
||||
IEnumerable<KeyValuePair<string, object?>> arguments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var paramMarshallers = FunctionDescriptor.ParameterMarshallers;
|
||||
object?[] args = paramMarshallers.Length != 0 ? new object?[paramMarshallers.Length] : [];
|
||||
|
||||
Dictionary<string, object?> argumentsDictionary = arguments.ToDictionary();
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
bool disposeTarget = false;
|
||||
object? target = Target;
|
||||
try
|
||||
{
|
||||
args[i] = paramMarshallers[i](argumentsDictionary, cancellationToken);
|
||||
}
|
||||
if (TargetType is { } targetType)
|
||||
{
|
||||
Debug.Assert(target is null, "Expected target to be null when we have a non-null target type");
|
||||
Debug.Assert(!FunctionDescriptor.Method.IsStatic, "Expected an instance method");
|
||||
|
||||
return FunctionDescriptor.ReturnParameterMarshaller(
|
||||
ReflectionInvoke(FunctionDescriptor.Method, Target, args), cancellationToken);
|
||||
if (argumentsDictionary.TryGetValue(AIFunctionMcpServerTool.RequestContextKey, out object? value) &&
|
||||
value is RequestContext<CallToolRequestParams> requestContext &&
|
||||
requestContext.Server?.Services is { } services)
|
||||
{
|
||||
target = ActivatorUtilities.CreateInstance(services, targetType!);
|
||||
}
|
||||
else
|
||||
{
|
||||
target = Activator.CreateInstance(targetType);
|
||||
}
|
||||
|
||||
disposeTarget = true;
|
||||
}
|
||||
var paramMarshallers = FunctionDescriptor.ParameterMarshallers;
|
||||
object?[] args = paramMarshallers.Length != 0 ? new object?[paramMarshallers.Length] : [];
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
args[i] = paramMarshallers[i](argumentsDictionary, cancellationToken);
|
||||
}
|
||||
|
||||
return await FunctionDescriptor.ReturnParameterMarshaller(
|
||||
ReflectionInvoke(FunctionDescriptor.Method, target, args), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (disposeTarget)
|
||||
{
|
||||
if (target is IAsyncDisposable ad)
|
||||
{
|
||||
await ad.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (target is IDisposable d)
|
||||
{
|
||||
d.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue