Skip to content

Commit b61cdef

Browse files
authored
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
1 parent 6fa1443 commit b61cdef

File tree

6 files changed

+391
-38
lines changed

6 files changed

+391
-38
lines changed

src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Tools.cs

+41-18
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,51 @@ public static partial class McpServerBuilderExtensions
1414
{
1515
private const string RequiresUnreferencedCodeMessage = "This method requires dynamic lookup of method metadata and might not work in Native AOT.";
1616

17-
/// <summary>
18-
/// Adds a tool to the server.
19-
/// </summary>
17+
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
2018
/// <typeparam name="TTool">The tool type.</typeparam>
2119
/// <param name="builder">The builder instance.</param>
2220
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
23-
public static IMcpServerBuilder WithTools<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] TTool>(
21+
/// <remarks>
22+
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TTool"/>
23+
/// type, where the methods are attributed as <see cref="McpServerToolAttribute"/>, and adds an <see cref="McpServerTool"/>
24+
/// instance for each. For instance methods, an instance will be constructed for each invocation of the tool.
25+
/// </remarks>
26+
public static IMcpServerBuilder WithTools<[DynamicallyAccessedMembers(
27+
DynamicallyAccessedMemberTypes.PublicMethods |
28+
DynamicallyAccessedMemberTypes.NonPublicMethods |
29+
DynamicallyAccessedMemberTypes.PublicConstructors)] TTool>(
2430
this IMcpServerBuilder builder)
2531
{
2632
Throw.IfNull(builder);
2733

28-
foreach (var toolMethod in GetToolMethods(typeof(TTool)))
34+
foreach (var toolMethod in typeof(TTool).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
2935
{
30-
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services));
36+
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
37+
{
38+
if (toolMethod.IsStatic)
39+
{
40+
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services));
41+
}
42+
else
43+
{
44+
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, typeof(TTool), services: services));
45+
}
46+
}
3147
}
3248

3349
return builder;
3450
}
3551

36-
/// <summary>
37-
/// Adds tools to the server.
38-
/// </summary>
52+
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
3953
/// <param name="builder">The builder instance.</param>
4054
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
4155
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
4256
/// <exception cref="ArgumentNullException"><paramref name="toolTypes"/> is <see langword="null"/>.</exception>
57+
/// <remarks>
58+
/// This method discovers all instance and static methods (public and non-public) on the specified <paramref name="toolTypes"/>
59+
/// types, where the methods are attributed as <see cref="McpServerToolAttribute"/>, and adds an <see cref="McpServerTool"/>
60+
/// instance for each. For instance methods, an instance will be constructed for each invocation of the tool.
61+
/// </remarks>
4362
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
4463
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params IEnumerable<Type> toolTypes)
4564
{
@@ -50,13 +69,23 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params
5069
{
5170
if (toolType is not null)
5271
{
53-
foreach (var toolMethod in GetToolMethods(toolType))
72+
foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
5473
{
55-
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, services: services));
74+
if (method.GetCustomAttribute<McpServerToolAttribute>() is not null)
75+
{
76+
if (method.IsStatic)
77+
{
78+
builder.Services.AddSingleton(services => McpServerTool.Create(method, services: services));
79+
}
80+
else
81+
{
82+
builder.Services.AddSingleton(services => McpServerTool.Create(method, toolType, services: services));
83+
}
84+
}
5685
}
5786
}
5887
}
59-
88+
6089
return builder;
6190
}
6291

@@ -78,10 +107,4 @@ from t in toolAssembly.GetTypes()
78107
where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
79108
select t);
80109
}
81-
82-
private static IEnumerable<MethodInfo> GetToolMethods(
83-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type toolType) =>
84-
from method in toolType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
85-
where method.GetCustomAttribute<McpServerToolAttribute>() is not null
86-
select method;
87110
}

src/ModelContextProtocol/Server/AIFunctionMcpServerTool.cs

+24-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using ModelContextProtocol.Protocol.Types;
44
using ModelContextProtocol.Utils;
55
using ModelContextProtocol.Utils.Json;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Reflection;
78
using System.Text.Json;
89

@@ -13,7 +14,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
1314
{
1415
/// <summary>Key used temporarily for flowing request context into an AIFunction.</summary>
1516
/// <remarks>This will be replaced with use of AIFunctionArguments.Context.</remarks>
16-
private const string RequestContextKey = "__temporary_RequestContext";
17+
internal const string RequestContextKey = "__temporary_RequestContext";
1718

1819
/// <summary>
1920
/// 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
4849
// AIFunctionFactory, delete the TemporaryXx types, and fix-up the mechanism by
4950
// which the arguments are passed.
5051

51-
return Create(TemporaryAIFunctionFactory.Create(method, target, new TemporaryAIFunctionFactoryOptions()
52+
return Create(TemporaryAIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, name, description, services)));
53+
}
54+
55+
/// <summary>
56+
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via a <see cref="Delegate"/> instance.
57+
/// </summary>
58+
public static new AIFunctionMcpServerTool Create(
59+
MethodInfo method,
60+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
61+
string? name = null,
62+
string? description = null,
63+
IServiceProvider? services = null)
64+
{
65+
Throw.IfNull(method);
66+
67+
return Create(TemporaryAIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, name, description, services)));
68+
}
69+
70+
private static TemporaryAIFunctionFactoryOptions CreateAIFunctionFactoryOptions(
71+
MethodInfo method, string? name, string? description, IServiceProvider? services) =>
72+
new TemporaryAIFunctionFactoryOptions()
5273
{
5374
Name = name ?? method.GetCustomAttribute<McpServerToolAttribute>()?.Name,
5475
Description = description,
@@ -115,8 +136,7 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
115136
return null;
116137
}
117138
},
118-
}));
119-
}
139+
};
120140

121141
/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
122142
public static new AIFunctionMcpServerTool Create(AIFunction function)

src/ModelContextProtocol/Server/McpServerTool.cs

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.AI;
22
using ModelContextProtocol.Protocol.Types;
33
using System.ComponentModel;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Reflection;
56

67
namespace ModelContextProtocol.Server;
@@ -40,7 +41,7 @@ public abstract Task<CallToolResponse> InvokeAsync(
4041
/// </param>
4142
/// <param name="services">
4243
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
43-
/// used to determine which parameters should be satisifed from dependency injection, and so what services
44+
/// used to determine which parameters should be satisifed from dependency injection; what services
4445
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
4546
/// </param>
4647
/// <returns>The created <see cref="McpServerTool"/> for invoking <paramref name="method"/>.</returns>
@@ -68,7 +69,7 @@ public static McpServerTool Create(
6869
/// </param>
6970
/// <param name="services">
7071
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
71-
/// used to determine which parameters should be satisifed from dependency injection, and so what services
72+
/// used to determine which parameters should be satisifed from dependency injection; what services
7273
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
7374
/// </param>
7475
/// <returns>The created <see cref="McpServerTool"/> for invoking <paramref name="method"/>.</returns>
@@ -82,6 +83,43 @@ public static McpServerTool Create(
8283
IServiceProvider? services = null) =>
8384
AIFunctionMcpServerTool.Create(method, target, name, description, services);
8485

86+
/// <summary>
87+
/// Creates an <see cref="McpServerTool"/> instance for a method, specified via an <see cref="MethodInfo"/> for
88+
/// and instance method, along with a <see cref="Type"/> representing the type of the target object to
89+
/// instantiate each time the method is invoked.
90+
/// </summary>
91+
/// <param name="method">The instance method to be represented via the created <see cref="AIFunction"/>.</param>
92+
/// <param name="targetType">
93+
/// The <see cref="Type"/> to construct an instance of on which to invoke <paramref name="method"/> when
94+
/// the resulting <see cref="AIFunction"/> is invoked. If services are provided,
95+
/// ActivatorUtilities.CreateInstance will be used to construct the instance using those services; otherwise,
96+
/// <see cref="Activator.CreateInstance(Type)"/> is used, utilizing the type's public parameterless constructor.
97+
/// If an instance can't be constructed, an exception is thrown during the function's invocation.
98+
/// </param>
99+
/// <param name="name">
100+
/// The name to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but an <see cref="McpServerToolAttribute"/>
101+
/// is applied to <paramref name="method"/>, the name from the attribute will be used. If that's not present, the name based
102+
/// on <paramref name="method"/>'s name will be used.
103+
/// </param>
104+
/// <param name="description">
105+
/// The description to use for the <see cref="McpServerTool"/>. If <see langword="null"/>, but a <see cref="DescriptionAttribute"/>
106+
/// is applied to <paramref name="method"/>, the description from that attribute will be used.
107+
/// </param>
108+
/// <param name="services">
109+
/// Optional services used in the construction of the <see cref="McpServerTool"/>. These services will be
110+
/// used to determine which parameters should be satisifed from dependency injection; what services
111+
/// are satisfied via this provider should match what's satisfied via the provider passed in at invocation time.
112+
/// </param>
113+
/// <returns>The created <see cref="AIFunction"/> for invoking <paramref name="method"/>.</returns>
114+
/// <exception cref="ArgumentNullException"><paramref name="method"/> is <see langword="null"/>.</exception>
115+
public static McpServerTool Create(
116+
MethodInfo method,
117+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType,
118+
string? name = null,
119+
string? description = null,
120+
IServiceProvider? services = null) =>
121+
AIFunctionMcpServerTool.Create(method, targetType, name, description, services);
122+
85123
/// <summary>Creates an <see cref="McpServerTool"/> that wraps the specified <see cref="AIFunction"/>.</summary>
86124
/// <param name="function">The function to wrap.</param>
87125
/// <exception cref="ArgumentNullException"><paramref name="function"/> is <see langword="null"/>.</exception>

0 commit comments

Comments
 (0)