Add initial support for Resource Templates (#18)

* Add initial support for Resource Templates

* Make ListResourceTemplatesHandler not required

The server was throwing if this handler wasn't specified, and it should probably be optional anyway.

* Remove extra line

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Fix spaces

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Fix spaces

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Add and use a default ListResourceTemplatesHandler

If no ListResourceTemplatesHandler is specified, use a default one that returns an empty result.

* Fix spaces

* Spaces

* Add Client extensions for ListResourceTemplates

* Integration Test for Client ListResourceTemplates

* Fix comment

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Make Defaults internal

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Simplify test call

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Make delegate static

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Fix test comment

* Spaces

* Fix xml doc

Co-authored-by: Stephen Toub <stoub@microsoft.com>

* Make default handler inline

* Spaces

Co-authored-by: Stephen Halter <halter73@gmail.com>

---------

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
Jonathan Dick 2025-03-21 14:09:45 -04:00 • committed by GitHub
commit fe6b401014
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 287 additions and 3 deletions

View file

@ -176,6 +176,21 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
client.GetPromptAsync("non_existent_prompt", null, CancellationToken.None));
}
[Theory]
[MemberData(nameof(GetClients))]
public async Task ListResourceTemplates_Stdio(string clientId)
{
// arrange
// act
await using var client = await _fixture.CreateClientAsync(clientId);
List<ResourceTemplate> allResourceTemplates = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken).ToListAsync(TestContext.Current.CancellationToken);
// The server provides a single test resource template
Assert.Single(allResourceTemplates);
}
[Theory]
[MemberData(nameof(GetClients))]
public async Task ListResources_Stdio(string clientId)

View file

@ -71,6 +71,19 @@ public class McpServerBuilderExtensionsHandlerTests
Assert.Equal(handler, options.GetPromptHandler);
}
[Fact]
public void WithListResourceTemplatesHandler_Sets_Handler()
{
Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, Task<ListResourceTemplatesResult>> handler = (context, token) => Task.FromResult(new ListResourceTemplatesResult());
_builder.Object.WithListResourceTemplatesHandler(handler);
var serviceProvider = _services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
Assert.Equal(handler, options.ListResourceTemplatesHandler);
}
[Fact]
public void WithListResourcesHandler_Sets_Handler()
{

View file

@ -14,6 +14,7 @@ public class McpServerHandlerTests
Assert.Null(handlers.CallToolHandler);
Assert.Null(handlers.ListPromptsHandler);
Assert.Null(handlers.GetPromptHandler);
Assert.Null(handlers.ListResourceTemplatesHandler);
Assert.Null(handlers.ListResourcesHandler);
Assert.Null(handlers.ReadResourceHandler);
Assert.Null(handlers.GetCompletionHandler);
@ -24,6 +25,7 @@ public class McpServerHandlerTests
handlers.CallToolHandler = (p, c) => Task.FromResult(new CallToolResponse());
handlers.ListPromptsHandler = (p, c) => Task.FromResult(new ListPromptsResult());
handlers.GetPromptHandler = (p, c) => Task.FromResult(new GetPromptResult());
handlers.ListResourceTemplatesHandler = (p, c) => Task.FromResult(new ListResourceTemplatesResult());
handlers.ListResourcesHandler = (p, c) => Task.FromResult(new ListResourcesResult());
handlers.ReadResourceHandler = (p, c) => Task.FromResult(new ReadResourceResult());
handlers.GetCompletionHandler = (p, c) => Task.FromResult(new CompleteResult());
@ -34,6 +36,7 @@ public class McpServerHandlerTests
Assert.NotNull(handlers.CallToolHandler);
Assert.NotNull(handlers.ListPromptsHandler);
Assert.NotNull(handlers.GetPromptHandler);
Assert.NotNull(handlers.ListResourceTemplatesHandler);
Assert.NotNull(handlers.ListResourcesHandler);
Assert.NotNull(handlers.ReadResourceHandler);
Assert.NotNull(handlers.GetCompletionHandler);

View file

@ -297,6 +297,44 @@ public class McpServerTests
});
}
[Fact]
public async Task Can_Handle_ResourceTemplates_List_Requests()
{
await Can_Handle_Requests(
new ServerCapabilities
{
Resources = new()
{
ListResourceTemplatesHandler = (request, ct) =>
{
return Task.FromResult(new ListResourceTemplatesResult
{
ResourceTemplates = [new() { UriTemplate = "test", Name = "Test Resource" }]
});
},
ListResourcesHandler = (request, ct) =>
{
return Task.FromResult(new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
});
},
ReadResourceHandler = (request, ct) => throw new NotImplementedException(),
}
},
"resources/templates/list",
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListResourceTemplatesResult>(response);
var result = (ListResourceTemplatesResult)response;
Assert.NotNull(result.ResourceTemplates);
Assert.NotEmpty(result.ResourceTemplates);
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
});
}
[Fact]
public async Task Can_Handle_Resources_List_Requests()
{