Add McpServer/ClientResource{Template} and friends (#391)

* Add McpServer/ClientResource{Template} and friends

This PR copies the same design used by tools and prompts to add McpServerResource, McpServerResourceAttribute, McpServerResourceTypeAttribute, McpClientResource, and then all of those for ResourceTemplate as well.

Unlike the others, [McpServerResource] is permitted not only on methods but also on properties. This convenience is afforded because such resources aren't parameterized, and thus can be easily modeled as properties, likely auto-props, e.g.
```C#
[McpServerResource]
public static BlobResourceContents MyAmazingData { get; } = Create();
```

Details about resources can be encoded into the attributes or provided via McpServerResource{Template}.Create methods. If not URI is explicitly defined, one is derived from the member name and arguments. For templates, this means manufacturing a URI template that includes variables for all non-DI parameters.

This also fixes a few issues discovered along the way:
- McpClientExtensions are updated to use `ValueTask<T>` instead of `Task<T>` where relevant.
- McpServerPrompt was missing handling for progress notifications; that's been rectified. It was also missing configuring the description on the ProtocolPrompt.
- Renamed IMcpServerPrimitive.Name to Id. For resources, names aren't unique.
- After going back and forth, updated McpServer to not fail for missing handlers and to instead register default handlers. It simplifies the implementation and makes it more consistent.
- Added a bunch of primitive types into the McpJsonUtilities JSON source generator context so that such primitives are usable with tools/prompts/resources without folks being required to add them into their own context for Native AOT. I think we should consider pushing this down to AIJsonUtilities, instead, and that will then flow up to McpJsonUtilities automatically.

* Address feedback
This commit is contained in:
Stephen Toub 2025-05-09 18:21:11 -04:00 • committed by GitHub
commit d86b3277d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -297,6 +297,140 @@ public static partial class McpServerBuilderExtensions
}
#endregion
#region WithResources
private const string WithResourcesRequiresUnreferencedCodeMessage =
$"The non-generic {nameof(WithResources)} and {nameof(WithResourcesFromAssembly)} methods require dynamic lookup of member metadata" +
$"and may not work in Native AOT. Use the generic {nameof(WithResources)} method instead.";
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <typeparam name="TResourceType">The resource type.</typeparam>
/// <param name="builder">The builder instance.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This method discovers all instance and static methods (public and non-public) on the specified <typeparamref name="TResourceType"/>
/// type, where the members are attributed as <see cref="McpServerResourceAttribute"/>, and adds an <see cref="McpServerResource"/>
/// instance for each. For instance members, an instance will be constructed for each invocation of the resource.
/// </remarks>
public static IMcpServerBuilder WithResources<[DynamicallyAccessedMembers(
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicConstructors)] TResourceType>(
this IMcpServerBuilder builder)
{
Throw.IfNull(builder);
foreach (var resourceTemplateMethod in typeof(TResourceType).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
{
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
services => McpServerResource.Create(resourceTemplateMethod, typeof(TResourceType), new() { Services = services })));
}
}
return builder;
}
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <param name="builder">The builder instance.</param>
/// <param name="resourcetemplates">The <see cref="McpServerResource"/> instances to add to the server.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="resourcetemplates"/> is <see langword="null"/>.</exception>
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<McpServerResource> resourcetemplates)
{
Throw.IfNull(builder);
Throw.IfNull(resourcetemplates);
foreach (var resourceTemplate in resourcetemplates)
{
if (resourceTemplate is not null)
{
builder.Services.AddSingleton(resourceTemplate);
}
}
return builder;
}
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
/// <param name="builder">The builder instance.</param>
/// <param name="resourceTemplateTypes">Types with marked methods to add as resources to the server.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="resourceTemplateTypes"/> is <see langword="null"/>.</exception>
/// <remarks>
/// This method discovers all instance and static methods (public and non-public) on the specified <paramref name="resourceTemplateTypes"/>
/// types, where the methods are attributed as <see cref="McpServerResourceAttribute"/>, and adds an <see cref="McpServerResource"/>
/// instance for each. For instance methods, an instance will be constructed for each invocation of the resource.
/// </remarks>
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<Type> resourceTemplateTypes)
{
Throw.IfNull(builder);
Throw.IfNull(resourceTemplateTypes);
foreach (var resourceTemplateType in resourceTemplateTypes)
{
if (resourceTemplateType is not null)
{
foreach (var resourceTemplateMethod in resourceTemplateType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance))
{
if (resourceTemplateMethod.GetCustomAttribute<McpServerResourceAttribute>() is not null)
{
builder.Services.AddSingleton((Func<IServiceProvider, McpServerResource>)(resourceTemplateMethod.IsStatic ?
services => McpServerResource.Create(resourceTemplateMethod, options: new() { Services = services }) :
services => McpServerResource.Create(resourceTemplateMethod, resourceTemplateType, new() { Services = services })));
}
}
}
}
return builder;
}
/// <summary>
/// Adds types marked with the <see cref="McpServerResourceTypeAttribute"/> attribute from the given assembly as resources to the server.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="resourceAssembly">The assembly to load the types from. If <see langword="null"/>, the calling assembly will be used.</param>
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This method scans the specified assembly (or the calling assembly if none is provided) for classes
/// marked with the <see cref="McpServerResourceTypeAttribute"/>. It then discovers all members within those
/// classes that are marked with the <see cref="McpServerResourceAttribute"/> and registers them as <see cref="McpServerResource"/>s
/// in the <paramref name="builder"/>'s <see cref="IServiceCollection"/>.
/// </para>
/// <para>
/// The method automatically handles both static and instance members. For instance members, a new instance
/// of the containing class will be constructed for each invocation of the resource.
/// </para>
/// <para>
/// Resource templates registered through this method can be discovered by clients using the <c>list_resourcetemplates</c> request
/// and invoked using the <c>read_resource</c> request.
/// </para>
/// <para>
/// Note that this method performs reflection at runtime and may not work in Native AOT scenarios. For
/// Native AOT compatibility, consider using the generic <see cref="WithResources{TResourceType}"/> method instead.
/// </para>
/// </remarks>
[RequiresUnreferencedCode(WithResourcesRequiresUnreferencedCodeMessage)]
public static IMcpServerBuilder WithResourcesFromAssembly(this IMcpServerBuilder builder, Assembly? resourceAssembly = null)
{
Throw.IfNull(builder);
resourceAssembly ??= Assembly.GetCallingAssembly();
return builder.WithResources(
from t in resourceAssembly.GetTypes()
where t.GetCustomAttribute<McpServerResourceTypeAttribute>() is not null
select t);
}
#endregion
#region Handlers
/// <summary>
/// Configures a handler for listing resource templates available from the Model Context Protocol server.