Skip to content

Commit c36924b

Browse files
committed
Add initial support for Resource Templates
1 parent c8c974a commit c36924b

File tree

12 files changed

+225
-4
lines changed

12 files changed

+225
-4
lines changed

src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.Handler.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ namespace ModelContextProtocol;
1111
/// </summary>
1212
public static partial class McpServerBuilderExtensions
1313
{
14+
/// <summary>
15+
/// Sets the handler for list resource templates requests.
16+
/// </summary>
17+
/// <param name="builder">The builder instance.</param>
18+
/// <param name="handler">The handler.</param>
19+
public static IMcpServerBuilder WithListResourceTemplatesHandler(this IMcpServerBuilder builder, Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, Task<ListResourceTemplatesResult>> handler)
20+
{
21+
Throw.IfNull(builder);
22+
23+
builder.Services.Configure<McpServerHandlers>(s => s.ListResourceTemplatesHandler = handler);
24+
return builder;
25+
}
26+
1427
/// <summary>
1528
/// Sets the handler for list tools requests.
1629
/// </summary>

src/ModelContextProtocol/Protocol/Types/Capabilities.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ public record ResourcesCapability
110110
[JsonPropertyName("listChanged")]
111111
public bool? ListChanged { get; init; }
112112

113+
/// <summary>
114+
/// Gets or sets the handler for list resource templates requests.
115+
/// </summary>
116+
[JsonIgnore]
117+
public Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, Task<ListResourceTemplatesResult>>? ListResourceTemplatesHandler { get; init; }
118+
113119
/// <summary>
114120
/// Gets or sets the handler for list resources requests.
115121
/// </summary>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace ModelContextProtocol.Protocol.Types;
2+
3+
/// <summary>
4+
/// Sent from the client to request a list of resource templates the server has.
5+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
6+
/// </summary>
7+
public class ListResourceTemplatesRequestParams
8+
{
9+
/// <summary>
10+
/// An opaque token representing the current pagination position.
11+
/// If provided, the server should return results starting after this cursor.
12+
/// </summary>
13+
[System.Text.Json.Serialization.JsonPropertyName("cursor")]
14+
public string? Cursor { get; init; }
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using ModelContextProtocol.Protocol.Messages;
2+
3+
namespace ModelContextProtocol.Protocol.Types;
4+
5+
/// <summary>
6+
/// The server's response to a resources/templates/list request from the client.
7+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
8+
/// </summary>
9+
public class ListResourceTemplatesResult : PaginatedResult
10+
{
11+
/// <summary>
12+
/// A list of resource templates that the server offers.
13+
/// </summary>
14+
[System.Text.Json.Serialization.JsonPropertyName("resourceTemplates")]
15+
public List<ResourceTemplate> ResourceTemplates { get; set; } = [];
16+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using ModelContextProtocol.Protocol.Types;
2+
3+
using System.Text.Json.Serialization;
4+
5+
namespace ModelContextProtocol.Protocol.Types;
6+
7+
/// <summary>
8+
/// Represents a known resource template that the server is capable of reading.
9+
/// <see href="https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json">See the schema for details</see>
10+
/// </summary>
11+
public record ResourceTemplate
12+
{
13+
/// <summary>
14+
/// The URI template (according to RFC 6570) that can be used to construct resource URIs.
15+
/// </summary>
16+
[JsonPropertyName("uriTemplate")]
17+
public required string UriTemplate { get; init; }
18+
19+
/// <summary>
20+
/// A human-readable name for this resource template.
21+
/// </summary>
22+
[JsonPropertyName("name")]
23+
public required string Name { get; init; }
24+
25+
/// <summary>
26+
/// A description of what this resource template represents.
27+
/// </summary>
28+
[JsonPropertyName("description")]
29+
public string? Description { get; init; }
30+
31+
/// <summary>
32+
/// The MIME type of this resource template, if known.
33+
/// </summary>
34+
[JsonPropertyName("mimeType")]
35+
public string? MimeType { get; init; }
36+
37+
/// <summary>
38+
/// Optional annotations for the resource template.
39+
/// </summary>
40+
[JsonPropertyName("annotations")]
41+
public Annotations? Annotations { get; init; }
42+
}

src/ModelContextProtocol/Server/McpServer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,13 @@ private void SetResourcesHandler(McpServerOptions options)
147147
}
148148

149149
if (resourcesCapability.ListResourcesHandler is not { } listResourcesHandler ||
150-
resourcesCapability.ReadResourceHandler is not { } readResourceHandler)
150+
resourcesCapability.ReadResourceHandler is not { } readResourceHandler ||
151+
resourcesCapability.ListResourceTemplatesHandler is not { } listResourceTemplatesHandler)
151152
{
152153
throw new McpServerException("Resources capability was enabled, but ListResources and/or ReadResource handlers were not specified.");
153154
}
154155

156+
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>("resources/templates/list", (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
155157
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>("resources/list", (request, ct) => listResourcesHandler(new(this, request), ct));
156158
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>("resources/read", (request, ct) => readResourceHandler(new(this, request), ct));
157159

src/ModelContextProtocol/Server/McpServerHandlers.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public sealed class McpServerHandlers
2727
/// </summary>
2828
public Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>>? GetPromptHandler { get; set; }
2929

30+
/// <summary>
31+
/// Gets or sets the handler for list resource templates requests.
32+
/// </summary>
33+
public Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, Task<ListResourceTemplatesResult>>? ListResourceTemplatesHandler { get; set; }
34+
3035
/// <summary>
3136
/// Gets or sets the handler for list resources requests.
3237
/// </summary>
@@ -82,11 +87,13 @@ promptsCapability with
8287
resourcesCapability = resourcesCapability is null ?
8388
new()
8489
{
90+
ListResourceTemplatesHandler = ListResourceTemplatesHandler,
8591
ListResourcesHandler = ListResourcesHandler,
8692
ReadResourceHandler = ReadResourceHandler
8793
} :
8894
resourcesCapability with
8995
{
96+
ListResourceTemplatesHandler = ListResourceTemplatesHandler ?? resourcesCapability.ListResourceTemplatesHandler,
9097
ListResourcesHandler = ListResourcesHandler ?? resourcesCapability.ListResourcesHandler,
9198
ReadResourceHandler = ReadResourceHandler ?? resourcesCapability.ReadResourceHandler
9299
};

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,21 @@ private static ResourcesCapability ConfigureResources()
313313

314314
return new()
315315
{
316+
ListResourceTemplatesHandler = (request, cancellationToken) =>
317+
{
318+
319+
return Task.FromResult(new ListResourceTemplatesResult()
320+
{
321+
ResourceTemplates = [
322+
new ResourceTemplate()
323+
{
324+
UriTemplate = "test://dynamic/resource/{id}",
325+
Name = "Dynamic Resource",
326+
}
327+
]
328+
});
329+
},
330+
316331
ListResourcesHandler = (request, cancellationToken) =>
317332
{
318333
int startIndex = 0;
@@ -349,6 +364,27 @@ private static ResourcesCapability ConfigureResources()
349364
{
350365
throw new McpServerException("Missing required argument 'uri'");
351366
}
367+
368+
if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
369+
{
370+
var id = request.Params.Uri.Split('/').LastOrDefault();
371+
if (string.IsNullOrEmpty(id))
372+
{
373+
throw new McpServerException("Invalid resource URI");
374+
}
375+
return Task.FromResult(new ReadResourceResult()
376+
{
377+
Contents = [
378+
new ResourceContents()
379+
{
380+
Uri = request.Params.Uri,
381+
MimeType = "text/plain",
382+
Text = $"Dynamic resource {id}: This is a plaintext resource"
383+
}
384+
]
385+
});
386+
}
387+
352388
ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri)
353389
?? throw new McpServerException("Resource not found");
354390

@@ -364,7 +400,8 @@ private static ResourcesCapability ConfigureResources()
364400
{
365401
throw new McpServerException("Missing required argument 'uri'");
366402
}
367-
if (!request.Params.Uri.StartsWith("test://static/resource/"))
403+
if (!request.Params.Uri.StartsWith("test://static/resource/")
404+
&& !request.Params.Uri.StartsWith("test://dynamic/resource/"))
368405
{
369406
throw new McpServerException("Invalid resource URI");
370407
}
@@ -383,7 +420,8 @@ private static ResourcesCapability ConfigureResources()
383420
{
384421
throw new McpServerException("Missing required argument 'uri'");
385422
}
386-
if (!request.Params.Uri.StartsWith("test://static/resource/"))
423+
if (!request.Params.Uri.StartsWith("test://static/resource/")
424+
&& !request.Params.Uri.StartsWith("test://dynamic/resource/"))
387425
{
388426
throw new McpServerException("Invalid resource URI");
389427
}

tests/ModelContextProtocol.TestSseServer/Program.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
200200
},
201201
Resources = new()
202202
{
203+
ListResourceTemplatesHandler = (request, cancellationToken) =>
204+
{
205+
206+
return Task.FromResult(new ListResourceTemplatesResult()
207+
{
208+
ResourceTemplates = [
209+
new ResourceTemplate()
210+
{
211+
UriTemplate = "test://dynamic/resource/{id}",
212+
Name = "Dynamic Resource",
213+
}
214+
]
215+
});
216+
},
217+
203218
ListResourcesHandler = (request, cancellationToken) =>
204219
{
205220
int startIndex = 0;
@@ -236,7 +251,27 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
236251
{
237252
throw new McpServerException("Missing required argument 'uri'");
238253
}
239-
254+
255+
if (request.Params.Uri.StartsWith("test://dynamic/resource/"))
256+
{
257+
var id = request.Params.Uri.Split('/').LastOrDefault();
258+
if (string.IsNullOrEmpty(id))
259+
{
260+
throw new McpServerException("Invalid resource URI");
261+
}
262+
return Task.FromResult(new ReadResourceResult()
263+
{
264+
Contents = [
265+
new ResourceContents()
266+
{
267+
Uri = request.Params.Uri,
268+
MimeType = "text/plain",
269+
Text = $"Dynamic resource {id}: This is a plaintext resource"
270+
}
271+
]
272+
});
273+
}
274+
240275
ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Params.Uri) ??
241276
throw new McpServerException("Resource not found");
242277

tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsHandlerTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ public void WithGetPromptHandler_Sets_Handler()
7171
Assert.Equal(handler, options.GetPromptHandler);
7272
}
7373

74+
[Fact]
75+
public void WithListResourceTemplatesHandler_Sets_Handler()
76+
{
77+
Func<RequestContext<ListResourceTemplatesRequestParams>, CancellationToken, Task<ListResourceTemplatesResult>> handler = (context, token) => Task.FromResult(new ListResourceTemplatesResult());
78+
79+
_builder.Object.WithListResourceTemplatesHandler(handler);
80+
81+
var serviceProvider = _services.BuildServiceProvider();
82+
var options = serviceProvider.GetRequiredService<IOptions<McpServerHandlers>>().Value;
83+
84+
Assert.Equal(handler, options.ListResourceTemplatesHandler);
85+
}
86+
7487
[Fact]
7588
public void WithListResourcesHandler_Sets_Handler()
7689
{

0 commit comments

Comments
 (0)