Skip to content

Commit cb8861a

Browse files
committed
Add WithXx overloads that take target instance
1 parent 70960e1 commit cb8861a

File tree

4 files changed

+179
-4
lines changed

4 files changed

+179
-4
lines changed

src/ModelContextProtocol/McpServerBuilderExtensions.cs

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,39 @@ public static partial class McpServerBuilderExtensions
5353
return builder;
5454
}
5555

56+
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
57+
/// <typeparam name="TToolType">The tool type.</typeparam>
58+
/// <param name="builder">The builder instance.</param>
59+
/// <param name="target">The target instance from which the tools should be sourced.</param>
60+
/// <param name="serializerOptions">The serializer options governing tool parameter marshalling.</param>
61+
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
62+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
63+
/// <remarks>
64+
/// This method discovers all instance methods (public and non-public) on the specified <typeparamref name="TToolType"/>
65+
/// type, where the methods are attributed as <see cref="McpServerToolAttribute"/>, and adds an <see cref="McpServerTool"/>
66+
/// instance for each, using <paramref name="target"/> as the associated instance.
67+
/// </remarks>
68+
public static IMcpServerBuilder WithTools<[DynamicallyAccessedMembers(
69+
DynamicallyAccessedMemberTypes.PublicMethods |
70+
DynamicallyAccessedMemberTypes.NonPublicMethods)] TToolType>(
71+
this IMcpServerBuilder builder,
72+
TToolType target,
73+
JsonSerializerOptions? serializerOptions = null)
74+
{
75+
Throw.IfNull(builder);
76+
Throw.IfNull(target);
77+
78+
foreach (var toolMethod in typeof(TToolType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
79+
{
80+
if (toolMethod.GetCustomAttribute<McpServerToolAttribute>() is not null)
81+
{
82+
builder.Services.AddSingleton(services => McpServerTool.Create(toolMethod, target, new() { Services = services, SerializerOptions = serializerOptions }));
83+
}
84+
}
85+
86+
return builder;
87+
}
88+
5689
/// <summary>Adds <see cref="McpServerTool"/> instances to the service collection backing <paramref name="builder"/>.</summary>
5790
/// <param name="builder">The builder instance.</param>
5891
/// <param name="tools">The <see cref="McpServerTool"/> instances to add to the server.</param>
@@ -137,7 +170,7 @@ public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, IEnume
137170
/// </para>
138171
/// <para>
139172
/// Note that this method performs reflection at runtime and may not work in Native AOT scenarios. For
140-
/// Native AOT compatibility, consider using the generic <see cref="WithTools{TToolType}"/> method instead.
173+
/// Native AOT compatibility, consider using the generic <see cref="M:WithTools"/> method instead.
141174
/// </para>
142175
/// </remarks>
143176
[RequiresUnreferencedCode(WithToolsRequiresUnreferencedCodeMessage)]
@@ -193,6 +226,39 @@ where t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null
193226
return builder;
194227
}
195228

229+
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
230+
/// <typeparam name="TPromptType">The prompt type.</typeparam>
231+
/// <param name="builder">The builder instance.</param>
232+
/// <param name="target">The target instance from which the prompts should be sourced.</param>
233+
/// <param name="serializerOptions">The serializer options governing prompt parameter marshalling.</param>
234+
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
235+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
236+
/// <remarks>
237+
/// This method discovers all instance methods (public and non-public) on the specified <typeparamref name="TPromptType"/>
238+
/// type, where the methods are attributed as <see cref="McpServerPromptAttribute"/>, and adds an <see cref="McpServerPrompt"/>
239+
/// instance for each, using <paramref name="target"/> as the associated instance.
240+
/// </remarks>
241+
public static IMcpServerBuilder WithPrompts<[DynamicallyAccessedMembers(
242+
DynamicallyAccessedMemberTypes.PublicMethods |
243+
DynamicallyAccessedMemberTypes.NonPublicMethods)] TPromptType>(
244+
this IMcpServerBuilder builder,
245+
TPromptType target,
246+
JsonSerializerOptions? serializerOptions = null)
247+
{
248+
Throw.IfNull(builder);
249+
Throw.IfNull(target);
250+
251+
foreach (var promptMethod in typeof(TPromptType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
252+
{
253+
if (promptMethod.GetCustomAttribute<McpServerPromptAttribute>() is not null)
254+
{
255+
builder.Services.AddSingleton(services => McpServerPrompt.Create(promptMethod, target, new() { Services = services, SerializerOptions = serializerOptions }));
256+
}
257+
}
258+
259+
return builder;
260+
}
261+
196262
/// <summary>Adds <see cref="McpServerPrompt"/> instances to the service collection backing <paramref name="builder"/>.</summary>
197263
/// <param name="builder">The builder instance.</param>
198264
/// <param name="prompts">The <see cref="McpServerPrompt"/> instances to add to the server.</param>
@@ -277,7 +343,7 @@ public static IMcpServerBuilder WithPrompts(this IMcpServerBuilder builder, IEnu
277343
/// </para>
278344
/// <para>
279345
/// Note that this method performs reflection at runtime and may not work in Native AOT scenarios. For
280-
/// Native AOT compatibility, consider using the generic <see cref="WithPrompts{TPromptType}"/> method instead.
346+
/// Native AOT compatibility, consider using the generic <see cref="M:WithPrompts"/> method instead.
281347
/// </para>
282348
/// </remarks>
283349
[RequiresUnreferencedCode(WithPromptsRequiresUnreferencedCodeMessage)]
@@ -311,7 +377,8 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
311377
/// instance for each. For instance members, an instance will be constructed for each invocation of the resource.
312378
/// </remarks>
313379
public static IMcpServerBuilder WithResources<[DynamicallyAccessedMembers(
314-
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
380+
DynamicallyAccessedMemberTypes.PublicMethods |
381+
DynamicallyAccessedMemberTypes.NonPublicMethods |
315382
DynamicallyAccessedMemberTypes.PublicConstructors)] TResourceType>(
316383
this IMcpServerBuilder builder)
317384
{
@@ -330,6 +397,37 @@ where t.GetCustomAttribute<McpServerPromptTypeAttribute>() is not null
330397
return builder;
331398
}
332399

400+
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
401+
/// <typeparam name="TResourceType">The resource type.</typeparam>
402+
/// <param name="builder">The builder instance.</param>
403+
/// <param name="target">The target instance from which the prompts should be sourced.</param>
404+
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
405+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
406+
/// <remarks>
407+
/// This method discovers all instance methods (public and non-public) on the specified <typeparamref name="TResourceType"/>
408+
/// type, where the methods are attributed as <see cref="McpServerResource"/>, and adds an <see cref="McpServerResource"/>
409+
/// instance for each, using <paramref name="target"/> as the associated instance.
410+
/// </remarks>
411+
public static IMcpServerBuilder WithResources<[DynamicallyAccessedMembers(
412+
DynamicallyAccessedMemberTypes.PublicMethods |
413+
DynamicallyAccessedMemberTypes.NonPublicMethods)] TResourceType>(
414+
this IMcpServerBuilder builder,
415+
TResourceType target)
416+
{
417+
Throw.IfNull(builder);
418+
Throw.IfNull(target);
419+
420+
foreach (var resourceTemplateMethod in typeof(TResourceType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
421+
{
422+
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
423+
{
424+
builder.Services.AddSingleton(services => McpServerResource.Create(resourceTemplateMethod, target, new() { Services = services }));
425+
}
426+
}
427+
428+
return builder;
429+
}
430+
333431
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
334432
/// <param name="builder">The builder instance.</param>
335433
/// <param name="resourceTemplates">The <see cref="McpServerResource"/> instances to add to the server.</param>
@@ -412,7 +510,7 @@ public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IE
412510
/// </para>
413511
/// <para>
414512
/// Note that this method performs reflection at runtime and may not work in Native AOT scenarios. For
415-
/// Native AOT compatibility, consider using the generic <see cref="WithResources{TResourceType}"/> method instead.
513+
/// Native AOT compatibility, consider using the generic <see cref="M:WithResources"/> method instead.
416514
/// </para>
417515
/// </remarks>
418516
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
using ModelContextProtocol.Client;
55
using ModelContextProtocol.Protocol;
66
using ModelContextProtocol.Server;
7+
using Moq;
78
using System.ComponentModel;
9+
using System.Text.Json;
810
using System.Text.Json.Serialization;
911
using System.Threading.Channels;
1012

@@ -217,13 +219,39 @@ public void WithPrompts_InvalidArgs_Throws()
217219

218220
Assert.Throws<ArgumentNullException>("prompts", () => builder.WithPrompts((IEnumerable<McpServerPrompt>)null!));
219221
Assert.Throws<ArgumentNullException>("promptTypes", () => builder.WithPrompts((IEnumerable<Type>)null!));
222+
Assert.Throws<ArgumentNullException>("target", () => builder.WithPrompts<object>(target: null!));
220223

221224
IMcpServerBuilder nullBuilder = null!;
222225
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithPrompts<object>());
226+
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithPrompts(new object()));
223227
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithPrompts(Array.Empty<Type>()));
224228
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithPromptsFromAssembly());
225229
}
226230

231+
[Fact]
232+
public async Task WithPrompts_TargetInstance_UsesTarget()
233+
{
234+
ServiceCollection sc = new();
235+
236+
var target = new SimplePrompts(new ObjectWithId() { Id = "42" });
237+
sc.AddMcpServer().WithPrompts(target);
238+
239+
McpServerPrompt prompt = sc.BuildServiceProvider().GetServices<McpServerPrompt>().First(t => t.ProtocolPrompt.Name == "returns_string");
240+
var result = await prompt.GetAsync(new RequestContext<GetPromptRequestParams>(new Mock<IMcpServer>().Object)
241+
{
242+
Params = new GetPromptRequestParams
243+
{
244+
Name = "returns_string",
245+
Arguments = new Dictionary<string, JsonElement>
246+
{
247+
["message"] = JsonSerializer.SerializeToElement("hello", AIJsonUtilities.DefaultOptions),
248+
}
249+
}
250+
}, TestContext.Current.CancellationToken);
251+
252+
Assert.Equal(target.ReturnsString("hello"), (result.Messages[0].Content as TextContentBlock)?.Text);
253+
}
254+
227255
[Fact]
228256
public void Empty_Enumerables_Is_Allowed()
229257
{

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
using ModelContextProtocol.Client;
55
using ModelContextProtocol.Protocol;
66
using ModelContextProtocol.Server;
7+
using Moq;
78
using System.ComponentModel;
9+
using System.Text.Json;
810
using System.Threading.Channels;
11+
using static ModelContextProtocol.Tests.Configuration.McpServerBuilderExtensionsPromptsTests;
912

1013
namespace ModelContextProtocol.Tests.Configuration;
1114

@@ -243,13 +246,35 @@ public void WithResources_InvalidArgs_Throws()
243246

244247
Assert.Throws<ArgumentNullException>("resourceTemplates", () => builder.WithResources((IEnumerable<McpServerResource>)null!));
245248
Assert.Throws<ArgumentNullException>("resourceTemplateTypes", () => builder.WithResources((IEnumerable<Type>)null!));
249+
Assert.Throws<ArgumentNullException>("target", () => builder.WithResources<object>(target: null!));
246250

247251
IMcpServerBuilder nullBuilder = null!;
248252
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources<object>());
253+
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources(new object()));
249254
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources(Array.Empty<Type>()));
250255
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResourcesFromAssembly());
251256
}
252257

258+
[Fact]
259+
public async Task WithResources_TargetInstance_UsesTarget()
260+
{
261+
ServiceCollection sc = new();
262+
263+
var target = new ResourceWithId(new ObjectWithId() { Id = "42" });
264+
sc.AddMcpServer().WithResources(target);
265+
266+
McpServerResource resource = sc.BuildServiceProvider().GetServices<McpServerResource>().First(t => t.ProtocolResource?.Name == "returns_string");
267+
var result = await resource.ReadAsync(new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object)
268+
{
269+
Params = new()
270+
{
271+
Uri = "returns://string"
272+
}
273+
}, TestContext.Current.CancellationToken);
274+
275+
Assert.Equal(target.ReturnsString(), (result?.Contents[0] as TextResourceContents)?.Text);
276+
}
277+
253278
[Fact]
254279
public void Empty_Enumerables_Is_Allowed()
255280
{
@@ -307,4 +332,11 @@ public sealed class MoreResources
307332
[McpServerResource, Description("Another neat direct resource")]
308333
public static string AnotherNeatDirectResource() => "This is a neat resource";
309334
}
335+
336+
[McpServerResourceType]
337+
public sealed class ResourceWithId(ObjectWithId id)
338+
{
339+
[McpServerResource(UriTemplate = "returns://string")]
340+
public string ReturnsString() => $"Id: {id.Id}";
341+
}
310342
}

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using ModelContextProtocol.Client;
66
using ModelContextProtocol.Protocol;
77
using ModelContextProtocol.Server;
8+
using Moq;
89
using System.Collections.Concurrent;
910
using System.ComponentModel;
1011
using System.IO.Pipelines;
@@ -403,9 +404,11 @@ public void WithTools_InvalidArgs_Throws()
403404

404405
Assert.Throws<ArgumentNullException>("tools", () => builder.WithTools((IEnumerable<McpServerTool>)null!));
405406
Assert.Throws<ArgumentNullException>("toolTypes", () => builder.WithTools((IEnumerable<Type>)null!));
407+
Assert.Throws<ArgumentNullException>("target", () => builder.WithTools<object>(target: null!));
406408

407409
IMcpServerBuilder nullBuilder = null!;
408410
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithTools<object>());
411+
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithTools(new object()));
409412
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithTools(Array.Empty<Type>()));
410413
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithToolsFromAssembly());
411414
}
@@ -503,6 +506,20 @@ public void WithToolsFromAssembly_Parameters_Satisfiable_From_DI(ServiceLifetime
503506
}
504507
}
505508

509+
[Fact]
510+
public async Task WithTools_TargetInstance_UsesTarget()
511+
{
512+
ServiceCollection sc = new();
513+
514+
var target = new EchoTool(new ObjectWithId());
515+
sc.AddMcpServer().WithTools(target, BuilderToolsJsonContext.Default.Options);
516+
517+
McpServerTool tool = sc.BuildServiceProvider().GetServices<McpServerTool>().First(t => t.ProtocolTool.Name == "get_ctor_parameter");
518+
var result = await tool.InvokeAsync(new RequestContext<CallToolRequestParams>(new Mock<IMcpServer>().Object), TestContext.Current.CancellationToken);
519+
520+
Assert.Equal(target.GetCtorParameter(), (result.Content[0] as TextContentBlock)?.Text);
521+
}
522+
506523
[Fact]
507524
public async Task Recognizes_Parameter_Types()
508525
{

0 commit comments

Comments
 (0)