Fix McpServerResource registration (#411)
- Add missing filling of ResourceCollection in McpServerOptionsSetup - Remove AIFunctionMcpServerXx.ToString overrides; for resources, it was changing behavior undesirably, and for the others it was duplicate code. - Fix notification method used for resource list changes. - Fix type in WithResources parameter name.
This commit is contained in:
parent
ce9f73f43f
commit
8d4f51da50
7 changed files with 330 additions and 34 deletions
|
|
@ -334,16 +334,16 @@ public static partial class McpServerBuilderExtensions
|
||||||
|
|
||||||
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
/// <summary>Adds <see cref="McpServerResource"/> instances to the service collection backing <paramref name="builder"/>.</summary>
|
||||||
/// <param name="builder">The builder instance.</param>
|
/// <param name="builder">The builder instance.</param>
|
||||||
/// <param name="resourcetemplates">The <see cref="McpServerResource"/> instances to add to the server.</param>
|
/// <param name="resourceTemplates">The <see cref="McpServerResource"/> instances to add to the server.</param>
|
||||||
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
|
/// <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="builder"/> is <see langword="null"/>.</exception>
|
||||||
/// <exception cref="ArgumentNullException"><paramref name="resourcetemplates"/> 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)
|
public static IMcpServerBuilder WithResources(this IMcpServerBuilder builder, IEnumerable<McpServerResource> resourceTemplates)
|
||||||
{
|
{
|
||||||
Throw.IfNull(builder);
|
Throw.IfNull(builder);
|
||||||
Throw.IfNull(resourcetemplates);
|
Throw.IfNull(resourceTemplates);
|
||||||
|
|
||||||
foreach (var resourceTemplate in resourcetemplates)
|
foreach (var resourceTemplate in resourceTemplates)
|
||||||
{
|
{
|
||||||
if (resourceTemplate is not null)
|
if (resourceTemplate is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -409,7 +409,7 @@ public static partial class McpServerBuilderExtensions
|
||||||
/// of the containing class will be constructed for each invocation of the resource.
|
/// of the containing class will be constructed for each invocation of the resource.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Resource templates registered through this method can be discovered by clients using the <c>list_resourcetemplates</c> request
|
/// 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.
|
/// and invoked using the <c>read_resource</c> request.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ namespace Microsoft.Extensions.DependencyInjection;
|
||||||
/// <param name="serverHandlers">The server handlers configuration options.</param>
|
/// <param name="serverHandlers">The server handlers configuration options.</param>
|
||||||
/// <param name="serverTools">Tools individually registered.</param>
|
/// <param name="serverTools">Tools individually registered.</param>
|
||||||
/// <param name="serverPrompts">Prompts individually registered.</param>
|
/// <param name="serverPrompts">Prompts individually registered.</param>
|
||||||
|
/// <param name="serverResources">Resources individually registered.</param>
|
||||||
internal sealed class McpServerOptionsSetup(
|
internal sealed class McpServerOptionsSetup(
|
||||||
IOptions<McpServerHandlers> serverHandlers,
|
IOptions<McpServerHandlers> serverHandlers,
|
||||||
IEnumerable<McpServerTool> serverTools,
|
IEnumerable<McpServerTool> serverTools,
|
||||||
IEnumerable<McpServerPrompt> serverPrompts) : IConfigureOptions<McpServerOptions>
|
IEnumerable<McpServerPrompt> serverPrompts,
|
||||||
|
IEnumerable<McpServerResource> serverResources) : IConfigureOptions<McpServerOptions>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configures the given McpServerOptions instance by setting server information
|
/// Configures the given McpServerOptions instance by setting server information
|
||||||
|
|
@ -58,6 +60,23 @@ internal sealed class McpServerOptionsSetup(
|
||||||
options.Capabilities.Prompts.PromptCollection = promptCollection;
|
options.Capabilities.Prompts.PromptCollection = promptCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect all of the provided resources into a resources collection. If the options already has
|
||||||
|
// a collection, add to it, otherwise create a new one. We want to maintain the identity
|
||||||
|
// of an existing collection in case someone has provided their own derived type, wants
|
||||||
|
// change notifications, etc.
|
||||||
|
McpServerPrimitiveCollection<McpServerResource> resourceCollection = options.Capabilities?.Resources?.ResourceCollection ?? [];
|
||||||
|
foreach (var resource in serverResources)
|
||||||
|
{
|
||||||
|
resourceCollection.TryAdd(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resourceCollection.IsEmpty)
|
||||||
|
{
|
||||||
|
options.Capabilities ??= new();
|
||||||
|
options.Capabilities.Resources ??= new();
|
||||||
|
options.Capabilities.Resources.ResourceCollection = resourceCollection;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply custom server handlers.
|
// Apply custom server handlers.
|
||||||
serverHandlers.Value.OverwriteWithSetHandlers(options);
|
serverHandlers.Value.OverwriteWithSetHandlers(options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -216,9 +216,6 @@ internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt
|
||||||
ProtocolPrompt = prompt;
|
ProtocolPrompt = prompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString() => AIFunction.ToString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Prompt ProtocolPrompt { get; }
|
public override Prompt ProtocolPrompt { get; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -331,9 +331,6 @@ internal sealed class AIFunctionMcpServerResource : McpServerResource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString() => AIFunction.ToString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override ResourceTemplate ProtocolResourceTemplate { get; }
|
public override ResourceTemplate ProtocolResourceTemplate { get; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,9 +247,6 @@ internal sealed class AIFunctionMcpServerTool : McpServerTool
|
||||||
ProtocolTool = tool;
|
ProtocolTool = tool;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString() => AIFunction.ToString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Tool ProtocolTool { get; }
|
public override Tool ProtocolTool { get; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,26 +81,19 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
|
||||||
// Now that everything has been configured, subscribe to any necessary notifications.
|
// Now that everything has been configured, subscribe to any necessary notifications.
|
||||||
if (transport is not StreamableHttpServerTransport streamableHttpTransport || streamableHttpTransport.Stateless is false)
|
if (transport is not StreamableHttpServerTransport streamableHttpTransport || streamableHttpTransport.Stateless is false)
|
||||||
{
|
{
|
||||||
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
|
Register(ServerOptions.Capabilities?.Tools?.ToolCollection, NotificationMethods.ToolListChangedNotification);
|
||||||
{
|
Register(ServerOptions.Capabilities?.Prompts?.PromptCollection, NotificationMethods.PromptListChangedNotification);
|
||||||
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(NotificationMethods.ToolListChangedNotification);
|
Register(ServerOptions.Capabilities?.Resources?.ResourceCollection, NotificationMethods.ResourceListChangedNotification);
|
||||||
tools.Changed += changed;
|
|
||||||
_disposables.Add(() => tools.Changed -= changed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ServerOptions.Capabilities?.Prompts?.PromptCollection is { } prompts)
|
void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection, string notificationMethod)
|
||||||
|
where TPrimitive : IMcpServerPrimitive
|
||||||
{
|
{
|
||||||
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(NotificationMethods.PromptListChangedNotification);
|
if (collection is not null)
|
||||||
prompts.Changed += changed;
|
{
|
||||||
_disposables.Add(() => prompts.Changed -= changed);
|
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(notificationMethod);
|
||||||
}
|
collection.Changed += changed;
|
||||||
|
_disposables.Add(() => collection.Changed -= changed);
|
||||||
var resources = ServerOptions.Capabilities?.Resources?.ResourceCollection;
|
}
|
||||||
if (resources is not null)
|
|
||||||
{
|
|
||||||
EventHandler changed = (sender, e) => _ = this.SendNotificationAsync(NotificationMethods.PromptListChangedNotification);
|
|
||||||
resources.Changed += changed;
|
|
||||||
_disposables.Add(() => resources.Changed -= changed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using ModelContextProtocol.Client;
|
||||||
|
using ModelContextProtocol.Protocol.Messages;
|
||||||
|
using ModelContextProtocol.Protocol.Types;
|
||||||
|
using ModelContextProtocol.Server;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Tests.Configuration;
|
||||||
|
|
||||||
|
public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTestBase
|
||||||
|
{
|
||||||
|
public McpServerBuilderExtensionsResourcesTests(ITestOutputHelper testOutputHelper)
|
||||||
|
: base(testOutputHelper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
|
||||||
|
{
|
||||||
|
mcpServerBuilder
|
||||||
|
.WithListResourcesHandler(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var cursor = request.Params?.Cursor;
|
||||||
|
switch (cursor)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
NextCursor = "abc",
|
||||||
|
Resources = [new()
|
||||||
|
{
|
||||||
|
Name = "Resource1",
|
||||||
|
Uri = "test://resource1",
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
case "abc":
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
NextCursor = "def",
|
||||||
|
Resources = [new()
|
||||||
|
{
|
||||||
|
Name = "Resource2",
|
||||||
|
Uri = "test://resource2",
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
case "def":
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
NextCursor = null,
|
||||||
|
Resources = [new()
|
||||||
|
{
|
||||||
|
Name = "Resource3",
|
||||||
|
Uri = "test://resource3",
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new McpException($"Unexpected cursor: '{cursor}'", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.WithListResourceTemplatesHandler(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var cursor = request.Params?.Cursor;
|
||||||
|
switch (cursor)
|
||||||
|
{
|
||||||
|
case null:
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
NextCursor = "abc",
|
||||||
|
ResourceTemplates = [new()
|
||||||
|
{
|
||||||
|
Name = "ResourceTemplate1",
|
||||||
|
UriTemplate = "test://resourceTemplate/{id}",
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
case "abc":
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
NextCursor = null,
|
||||||
|
ResourceTemplates = [new()
|
||||||
|
{
|
||||||
|
Name = "ResourceTemplate2",
|
||||||
|
UriTemplate = "test://resourceTemplate2/{id}",
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new McpException($"Unexpected cursor: '{cursor}'", McpErrorCode.InvalidParams);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.WithReadResourceHandler(async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
switch (request.Params?.Uri)
|
||||||
|
{
|
||||||
|
case "test://Resource1":
|
||||||
|
case "test://Resource2":
|
||||||
|
case "test://Resource3":
|
||||||
|
case "test://ResourceTemplate1":
|
||||||
|
case "test://ResourceTemplate2":
|
||||||
|
return new ReadResourceResult()
|
||||||
|
{
|
||||||
|
Contents = [new TextResourceContents() { Text = request.Params?.Uri ?? "(null)" }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new McpException($"Resource not found: {request.Params?.Uri}");
|
||||||
|
})
|
||||||
|
.WithResources<SimpleResources>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Adds_Resources_To_Server()
|
||||||
|
{
|
||||||
|
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
|
||||||
|
var resources = serverOptions?.Capabilities?.Resources?.ResourceCollection;
|
||||||
|
Assert.NotNull(resources);
|
||||||
|
Assert.NotEmpty(resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Can_List_And_Call_Registered_Resources()
|
||||||
|
{
|
||||||
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
Assert.NotNull(client.ServerCapabilities.Resources);
|
||||||
|
|
||||||
|
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(5, resources.Count);
|
||||||
|
|
||||||
|
var resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatDirectResource));
|
||||||
|
Assert.Equal("Some neat direct resource", resource.Description);
|
||||||
|
|
||||||
|
var result = await resource.ReadAsync(cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Single(result.Contents);
|
||||||
|
Assert.Equal("This is a neat resource", Assert.IsType<TextResourceContents>(result.Contents[0]).Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Can_List_And_Call_Registered_ResourceTemplates()
|
||||||
|
{
|
||||||
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
var resources = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(3, resources.Count);
|
||||||
|
|
||||||
|
var resource = resources.First(t => t.Name == nameof(SimpleResources.SomeNeatTemplatedResource));
|
||||||
|
Assert.Equal("Some neat resource with parameters", resource.Description);
|
||||||
|
|
||||||
|
var result = await resource.ReadAsync(new Dictionary<string, object?>() { ["name"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Single(result.Contents);
|
||||||
|
Assert.Equal("This is a neat resource with parameters: hello", Assert.IsType<TextResourceContents>(result.Contents[0]).Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Can_Be_Notified_Of_Resource_Changes()
|
||||||
|
{
|
||||||
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(5, resources.Count);
|
||||||
|
|
||||||
|
Channel<JsonRpcNotification> listChanged = Channel.CreateUnbounded<JsonRpcNotification>();
|
||||||
|
var notificationRead = listChanged.Reader.ReadAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.False(notificationRead.IsCompleted);
|
||||||
|
|
||||||
|
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
|
||||||
|
var serverResources = serverOptions.Capabilities?.Resources?.ResourceCollection;
|
||||||
|
Assert.NotNull(serverResources);
|
||||||
|
|
||||||
|
var newResource = McpServerResource.Create([McpServerResource(Name = "NewResource")] () => "42");
|
||||||
|
await using (client.RegisterNotificationHandler("notifications/resources/list_changed", (notification, cancellationToken) =>
|
||||||
|
{
|
||||||
|
listChanged.Writer.TryWrite(notification);
|
||||||
|
return default;
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
serverResources.Add(newResource);
|
||||||
|
await notificationRead;
|
||||||
|
|
||||||
|
resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(6, resources.Count);
|
||||||
|
Assert.Contains(resources, t => t.Name == "NewResource");
|
||||||
|
|
||||||
|
notificationRead = listChanged.Reader.ReadAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.False(notificationRead.IsCompleted);
|
||||||
|
serverResources.Remove(newResource);
|
||||||
|
await notificationRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal(5, resources.Count);
|
||||||
|
Assert.DoesNotContain(resources, t => t.Name == "NewResource");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Throws_When_Resource_Fails()
|
||||||
|
{
|
||||||
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<McpException>(async () => await client.ReadResourceAsync(
|
||||||
|
$"resource://{nameof(SimpleResources.ThrowsException)}",
|
||||||
|
cancellationToken: TestContext.Current.CancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Throws_Exception_On_Unknown_Resource()
|
||||||
|
{
|
||||||
|
await using IMcpClient client = await CreateMcpClientForServer();
|
||||||
|
|
||||||
|
var e = await Assert.ThrowsAsync<McpException>(async () => await client.ReadResourceAsync(
|
||||||
|
"test://NotRegisteredResource",
|
||||||
|
cancellationToken: TestContext.Current.CancellationToken));
|
||||||
|
|
||||||
|
Assert.Contains("Resource not found", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithResources_InvalidArgs_Throws()
|
||||||
|
{
|
||||||
|
IMcpServerBuilder builder = new ServiceCollection().AddMcpServer();
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("resourceTemplates", () => builder.WithResources((IEnumerable<McpServerResource>)null!));
|
||||||
|
Assert.Throws<ArgumentNullException>("resourceTemplateTypes", () => builder.WithResources((IEnumerable<Type>)null!));
|
||||||
|
|
||||||
|
IMcpServerBuilder nullBuilder = null!;
|
||||||
|
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources<object>());
|
||||||
|
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResources(Array.Empty<Type>()));
|
||||||
|
Assert.Throws<ArgumentNullException>("builder", () => nullBuilder.WithResourcesFromAssembly());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Empty_Enumerables_Is_Allowed()
|
||||||
|
{
|
||||||
|
IMcpServerBuilder builder = new ServiceCollection().AddMcpServer();
|
||||||
|
|
||||||
|
builder.WithResources(resourceTemplates: Array.Empty<McpServerResource>()); // no exception
|
||||||
|
builder.WithResources(resourceTemplateTypes: Array.Empty<Type>()); // no exception
|
||||||
|
builder.WithResources<object>(); // no exception even though no resources exposed
|
||||||
|
builder.WithResourcesFromAssembly(typeof(AIFunction).Assembly); // no exception even though no resources exposed
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Register_Resources_From_Current_Assembly()
|
||||||
|
{
|
||||||
|
ServiceCollection sc = new();
|
||||||
|
sc.AddMcpServer().WithResourcesFromAssembly();
|
||||||
|
IServiceProvider services = sc.BuildServiceProvider();
|
||||||
|
|
||||||
|
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://{nameof(SimpleResources.SomeNeatDirectResource)}");
|
||||||
|
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Register_Resources_From_Multiple_Sources()
|
||||||
|
{
|
||||||
|
ServiceCollection sc = new();
|
||||||
|
sc.AddMcpServer()
|
||||||
|
.WithResources<SimpleResources>()
|
||||||
|
.WithResources<MoreResources>()
|
||||||
|
.WithResources([McpServerResource.Create(() => "42", new() { UriTemplate = "myResources://Returns42/{something}" })]);
|
||||||
|
IServiceProvider services = sc.BuildServiceProvider();
|
||||||
|
|
||||||
|
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResource?.Uri == $"resource://{nameof(SimpleResources.SomeNeatDirectResource)}");
|
||||||
|
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://{nameof(SimpleResources.SomeNeatTemplatedResource)}{{?name}}");
|
||||||
|
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate?.UriTemplate == $"resource://{nameof(MoreResources.AnotherNeatDirectResource)}");
|
||||||
|
Assert.Contains(services.GetServices<McpServerResource>(), t => t.ProtocolResourceTemplate.UriTemplate == "myResources://Returns42/{something}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerResourceType]
|
||||||
|
public sealed class SimpleResources
|
||||||
|
{
|
||||||
|
[McpServerResource, Description("Some neat direct resource")]
|
||||||
|
public static string SomeNeatDirectResource() => "This is a neat resource";
|
||||||
|
|
||||||
|
[McpServerResource, Description("Some neat resource with parameters")]
|
||||||
|
public static string SomeNeatTemplatedResource(string name) => $"This is a neat resource with parameters: {name}";
|
||||||
|
|
||||||
|
[McpServerResource]
|
||||||
|
public static string ThrowsException() => throw new InvalidOperationException("uh oh");
|
||||||
|
}
|
||||||
|
|
||||||
|
[McpServerResourceType]
|
||||||
|
public sealed class MoreResources
|
||||||
|
{
|
||||||
|
[McpServerResource, Description("Another neat direct resource")]
|
||||||
|
public static string AnotherNeatDirectResource() => "This is a neat resource";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue