Add WithXx overloads that take target instance (#706)

* Add WithXx overloads that take target instance
* Special-case enumerables
This commit is contained in:
Stephen Toub 2025-08-26 12:38:53 -04:00 • committed by GitHub
commit 2fa658e439
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -4,8 +4,12 @@ using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using Moq;
using System.Collections;
using System.ComponentModel;
using System.Text.Json;
using System.Threading.Channels;
using static ModelContextProtocol.Tests.Configuration.McpServerBuilderExtensionsPromptsTests;
namespace ModelContextProtocol.Tests.Configuration;
@ -243,13 +247,59 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
Assert.Throws<ArgumentNullException>("resourceTemplates", () => builder.WithResources((IEnumerable<McpServerResource>)null!));
Assert.Throws<ArgumentNullException>("resourceTemplateTypes", () => builder.WithResources((IEnumerable<Type>)null!));
Assert.Throws<ArgumentNullException>("target", () => builder.WithResources<object>(target: null!));
IMcpServerBuilder nullBuilder = null!;
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources<object>());
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources(new object()));
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources(Array.Empty<Type>()));
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResourcesFromAssembly());
}
[Fact]
public async Task WithResources_TargetInstance_UsesTarget()
{
ServiceCollection sc = new();
var target = new ResourceWithId(new ObjectWithId() { Id = "42" });
sc.AddMcpServer().WithResources(target);
McpServerResource resource = sc.BuildServiceProvider().GetServices<McpServerResource>().First(t => t.ProtocolResource?.Name == "returns_string");
var result = await resource.ReadAsync(new RequestContext<ReadResourceRequestParams>(new Mock<IMcpServer>().Object)
{
Params = new()
{
Uri = "returns://string"
}
}, TestContext.Current.CancellationToken);
Assert.Equal(target.ReturnsString(), (result?.Contents[0] as TextResourceContents)?.Text);
}
[Fact]
public async Task WithResources_TargetInstance_UsesEnumerableImplementation()
{
ServiceCollection sc = new();
sc.AddMcpServer().WithResources(new MyResourceProvider());
var resources = sc.BuildServiceProvider().GetServices<McpServerResource>().ToArray();
Assert.Equal(2, resources.Length);
Assert.Contains(resources, t => t.ProtocolResource?.Name == "Returns42");
Assert.Contains(resources, t => t.ProtocolResource?.Name == "Returns43");
}
private sealed class MyResourceProvider : IEnumerable<McpServerResource>
{
public IEnumerator<McpServerResource> GetEnumerator()
{
yield return McpServerResource.Create(() => "42", new() { Name = "Returns42" });
yield return McpServerResource.Create(() => "43", new() { Name = "Returns43" });
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
[Fact]
public void Empty_Enumerables_Is_Allowed()
{
@ -307,4 +357,11 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
[McpServerResource, Description("Another neat direct resource")]
public static string AnotherNeatDirectResource() => "This is a neat resource";
}
[McpServerResourceType]
public sealed class ResourceWithId(ObjectWithId id)
{
[McpServerResource(UriTemplate = "returns://string")]
public string ReturnsString() => $"Id: {id.Id}";
}
}