diff --git a/src/ModelContextProtocol/Client/McpClientExtensions.cs b/src/ModelContextProtocol/Client/McpClientExtensions.cs
index 9c2ca38..56b77f6 100644
--- a/src/ModelContextProtocol/Client/McpClientExtensions.cs
+++ b/src/ModelContextProtocol/Client/McpClientExtensions.cs
@@ -139,6 +139,44 @@ public static class McpClientExtensions
cancellationToken);
}
+ ///
+ /// Retrieves a sequence of available resource templates from the server.
+ ///
+ /// The client.
+ /// A token to cancel the operation.
+ /// An asynchronous sequence of resource template information.
+ public static async IAsyncEnumerable ListResourceTemplatesAsync(
+ this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ string? cursor = null;
+ do
+ {
+ var resources = await ListResourceTemplatesAsync(client, cursor, cancellationToken).ConfigureAwait(false);
+ foreach (var resource in resources.ResourceTemplates)
+ {
+ yield return resource;
+ }
+
+ cursor = resources.NextCursor;
+ }
+ while (cursor is not null);
+ }
+
+ ///
+ /// Retrieves a list of available resources from the server.
+ ///
+ /// The client.
+ /// A cursor to paginate the results.
+ /// A token to cancel the operation.
+ public static Task ListResourceTemplatesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
+ {
+ Throw.IfNull(client);
+
+ return client.SendRequestAsync(
+ CreateRequest("resources/templates/list", CreateCursorDictionary(cursor)),
+ cancellationToken);
+ }
+
///
/// Retrieves a sequence of available resources from the server.
///
diff --git a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Handler.cs b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Handler.cs
index 6e88992..3612b92 100644
--- a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Handler.cs
+++ b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Handler.cs
@@ -11,6 +11,19 @@ namespace ModelContextProtocol;
///
public static partial class McpServerBuilderExtensions
{
+ ///
+ /// Sets the handler for list resource templates requests.
+ ///
+ /// The builder instance.
+ /// The handler.
+ public static IMcpServerBuilder WithListResourceTemplatesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler)
+ {
+ Throw.IfNull(builder);
+
+ builder.Services.Configure(s => s.ListResourceTemplatesHandler = handler);
+ return builder;
+ }
+
///
/// Sets the handler for list tools requests.
///
diff --git a/src/ModelContextProtocol/Protocol/Types/Capabilities.cs b/src/ModelContextProtocol/Protocol/Types/Capabilities.cs
index 600e20f..3392f57 100644
--- a/src/ModelContextProtocol/Protocol/Types/Capabilities.cs
+++ b/src/ModelContextProtocol/Protocol/Types/Capabilities.cs
@@ -110,6 +110,12 @@ public record ResourcesCapability
[JsonPropertyName("listChanged")]
public bool? ListChanged { get; init; }
+ ///
+ /// Gets or sets the handler for list resource templates requests.
+ ///
+ [JsonIgnore]
+ public Func, CancellationToken, Task>? ListResourceTemplatesHandler { get; init; }
+
///
/// Gets or sets the handler for list resources requests.
///
diff --git a/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs
new file mode 100644
index 0000000..4b4eecf
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesRequestParams.cs
@@ -0,0 +1,15 @@
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// Sent from the client to request a list of resource templates the server has.
+/// See the schema for details
+///
+public class ListResourceTemplatesRequestParams
+{
+ ///
+ /// An opaque token representing the current pagination position.
+ /// If provided, the server should return results starting after this cursor.
+ ///
+ [System.Text.Json.Serialization.JsonPropertyName("cursor")]
+ public string? Cursor { get; init; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs
new file mode 100644
index 0000000..d233792
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/ListResourceTemplatesResult.cs
@@ -0,0 +1,16 @@
+using ModelContextProtocol.Protocol.Messages;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// The server's response to a resources/templates/list request from the client.
+/// See the schema for details
+///
+public class ListResourceTemplatesResult : PaginatedResult
+{
+ ///
+ /// A list of resource templates that the server offers.
+ ///
+ [System.Text.Json.Serialization.JsonPropertyName("resourceTemplates")]
+ public List ResourceTemplates { get; set; } = [];
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs b/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs
new file mode 100644
index 0000000..b32822a
--- /dev/null
+++ b/src/ModelContextProtocol/Protocol/Types/ResourceTemplate.cs
@@ -0,0 +1,42 @@
+using ModelContextProtocol.Protocol.Types;
+
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Protocol.Types;
+
+///
+/// Represents a known resource template that the server is capable of reading.
+/// See the schema for details
+///
+public record ResourceTemplate
+{
+ ///
+ /// The URI template (according to RFC 6570) that can be used to construct resource URIs.
+ ///
+ [JsonPropertyName("uriTemplate")]
+ public required string UriTemplate { get; init; }
+
+ ///
+ /// A human-readable name for this resource template.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; init; }
+
+ ///
+ /// A description of what this resource template represents.
+ ///
+ [JsonPropertyName("description")]
+ public string? Description { get; init; }
+
+ ///
+ /// The MIME type of this resource template, if known.
+ ///
+ [JsonPropertyName("mimeType")]
+ public string? MimeType { get; init; }
+
+ ///
+ /// Optional annotations for the resource template.
+ ///
+ [JsonPropertyName("annotations")]
+ public Annotations? Annotations { get; init; }
+}
\ No newline at end of file
diff --git a/src/ModelContextProtocol/Server/McpServer.cs b/src/ModelContextProtocol/Server/McpServer.cs
index 5e813fe..6ad1def 100644
--- a/src/ModelContextProtocol/Server/McpServer.cs
+++ b/src/ModelContextProtocol/Server/McpServer.cs
@@ -155,6 +155,12 @@ internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
SetRequestHandler("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
SetRequestHandler("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
+ // Set the list resource templates handler, or use the default if not specified
+ var listResourceTemplatesHandler = resourcesCapability.ListResourceTemplatesHandler
+ ?? (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
+
+ SetRequestHandler("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
+
if (resourcesCapability.Subscribe is not true)
{
return;
diff --git a/src/ModelContextProtocol/Server/McpServerHandlers.cs b/src/ModelContextProtocol/Server/McpServerHandlers.cs
index 5632733..c41c601 100644
--- a/src/ModelContextProtocol/Server/McpServerHandlers.cs
+++ b/src/ModelContextProtocol/Server/McpServerHandlers.cs
@@ -27,6 +27,11 @@ public sealed class McpServerHandlers
///
public Func, CancellationToken, Task>? GetPromptHandler { get; set; }
+ ///
+ /// Gets or sets the handler for list resource templates requests.
+ ///
+ public Func, CancellationToken, Task>? ListResourceTemplatesHandler { get; set; }
+
///
/// Gets or sets the handler for list resources requests.
///
@@ -82,11 +87,13 @@ public sealed class McpServerHandlers
resourcesCapability = resourcesCapability is null ?
new()
{
+ ListResourceTemplatesHandler = ListResourceTemplatesHandler,
ListResourcesHandler = ListResourcesHandler,
ReadResourceHandler = ReadResourceHandler
} :
resourcesCapability with
{
+ ListResourceTemplatesHandler = ListResourceTemplatesHandler ?? resourcesCapability.ListResourceTemplatesHandler,
ListResourcesHandler = ListResourcesHandler ?? resourcesCapability.ListResourcesHandler,
ReadResourceHandler = ReadResourceHandler ?? resourcesCapability.ReadResourceHandler
};
diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs
index a59e1ec..8574a73 100644
--- a/tests/ModelContextProtocol.TestServer/Program.cs
+++ b/tests/ModelContextProtocol.TestServer/Program.cs
@@ -313,6 +313,20 @@ internal static class Program
return new()
{
+ ListResourceTemplatesHandler = (request, cancellationToken) =>
+ {
+ return Task.FromResult(new ListResourceTemplatesResult()
+ {
+ ResourceTemplates = [
+ new ResourceTemplate()
+ {
+ UriTemplate = "test://dynamic/resource/{id}",
+ Name = "Dynamic Resource",
+ }
+ ]
+ });
+ },
+
ListResourcesHandler = (request, cancellationToken) =>
{
int startIndex = 0;
@@ -349,6 +363,27 @@ internal static class Program
{
throw new McpServerException("Missing required argument 'uri'");
}
+
+ if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
+ {
+ var id = request.Params.Uri.Split('/').LastOrDefault();
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new McpServerException("Invalid resource URI");
+ }
+ return Task.FromResult(new ReadResourceResult()
+ {
+ Contents = [
+ new ResourceContents()
+ {
+ Uri = request.Params.Uri,
+ MimeType = "text/plain",
+ Text = $"Dynamic resource {id}: This is a plaintext resource"
+ }
+ ]
+ });
+ }
+
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
?? throw new McpServerException("Resource not found");
@@ -364,7 +399,8 @@ internal static class Program
{
throw new McpServerException("Missing required argument 'uri'");
}
- if (!request.Params.Uri.StartsWith("test://static/resource/"))
+ if (!request.Params.Uri.StartsWith("test://static/resource/")
+ && !request.Params.Uri.StartsWith("test://dynamic/resource/"))
{
throw new McpServerException("Invalid resource URI");
}
@@ -383,7 +419,8 @@ internal static class Program
{
throw new McpServerException("Missing required argument 'uri'");
}
- if (!request.Params.Uri.StartsWith("test://static/resource/"))
+ if (!request.Params.Uri.StartsWith("test://static/resource/")
+ && !request.Params.Uri.StartsWith("test://dynamic/resource/"))
{
throw new McpServerException("Invalid resource URI");
}
diff --git a/tests/ModelContextProtocol.TestSseServer/Program.cs b/tests/ModelContextProtocol.TestSseServer/Program.cs
index 7e9f5e4..712f845 100644
--- a/tests/ModelContextProtocol.TestSseServer/Program.cs
+++ b/tests/ModelContextProtocol.TestSseServer/Program.cs
@@ -200,6 +200,21 @@ internal class Program
},
Resources = new()
{
+ ListResourceTemplatesHandler = (request, cancellationToken) =>
+ {
+
+ return Task.FromResult(new ListResourceTemplatesResult()
+ {
+ ResourceTemplates = [
+ new ResourceTemplate()
+ {
+ UriTemplate = "test://dynamic/resource/{id}",
+ Name = "Dynamic Resource",
+ }
+ ]
+ });
+ },
+
ListResourcesHandler = (request, cancellationToken) =>
{
int startIndex = 0;
@@ -236,7 +251,27 @@ internal class Program
{
throw new McpServerException("Missing required argument 'uri'");
}
-
+
+ if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
+ {
+ var id = request.Params.Uri.Split('/').LastOrDefault();
+ if (string.IsNullOrEmpty(id))
+ {
+ throw new McpServerException("Invalid resource URI");
+ }
+ return Task.FromResult(new ReadResourceResult()
+ {
+ Contents = [
+ new ResourceContents()
+ {
+ Uri = request.Params.Uri,
+ MimeType = "text/plain",
+ Text = $"Dynamic resource {id}: This is a plaintext resource"
+ }
+ ]
+ });
+ }
+
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
throw new McpServerException("Resource not found");
diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
index af13cc7..121c4ea 100644
--- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
+++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs
@@ -176,6 +176,21 @@ public class ClientIntegrationTests : IClassFixture 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)
diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs
index 7cd5c8e..9a0fe72 100644
--- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs
+++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs
@@ -71,6 +71,19 @@ public class McpServerBuilderExtensionsHandlerTests
Assert.Equal(handler, options.GetPromptHandler);
}
+ [Fact]
+ public void WithListResourceTemplatesHandler_Sets_Handler()
+ {
+ Func, CancellationToken, Task> handler = (context, token) => Task.FromResult(new ListResourceTemplatesResult());
+
+ _builder.Object.WithListResourceTemplatesHandler(handler);
+
+ var serviceProvider = _services.BuildServiceProvider();
+ var options = serviceProvider.GetRequiredService>().Value;
+
+ Assert.Equal(handler, options.ListResourceTemplatesHandler);
+ }
+
[Fact]
public void WithListResourcesHandler_Sets_Handler()
{
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs
index 9ed7576..3f7d5f7 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerDelegatesTests.cs
@@ -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);
diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
index 849ad2f..15dc5c9 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs
@@ -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(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()
{