Skip to content

Commit 4ab0796

Browse files
authored
Add resource template support (#68)
See https://modelcontextprotocol.io/specification/2024-11-05/server/resources#resource-templates The support is pretty low level right now, and we do not provide any assistance for matching the provided templates or extracting out the values. We likely should add that support at some point later on, but we should be able to do so in a non-breaking manner. We will simply stop invoking the `handlers` for URIs that can't possibly match the template.
1 parent 4c3fa22 commit 4ab0796

File tree

4 files changed

+138
-11
lines changed

4 files changed

+138
-11
lines changed

pkgs/dart_mcp/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Save the `ServerCapabilities` object on the `ServerConnection` class to make
44
it easier to check the capabilities of the server.
5+
- Added support for adding and listing `ResourceTemplate`s.
56

67
## 0.1.0
78

pkgs/dart_mcp/lib/src/client/client.dart

+8-2
Original file line numberDiff line numberDiff line change
@@ -257,14 +257,20 @@ base class ServerConnection extends MCPBase {
257257
Future<CallToolResult> callTool(CallToolRequest request) =>
258258
sendRequest(CallToolRequest.methodName, request);
259259

260-
/// Lists all the resources from this server.
260+
/// Lists all the [Resource]s from this server.
261261
Future<ListResourcesResult> listResources(ListResourcesRequest request) =>
262262
sendRequest(ListResourcesRequest.methodName, request);
263263

264-
/// Reads a [Resource] returned from the [ListResourcesResult].
264+
/// Reads a [Resource] returned from the [ListResourcesResult] or matching
265+
/// a [ResourceTemplate] from a [ListResourceTemplatesResult].
265266
Future<ReadResourceResult> readResource(ReadResourceRequest request) =>
266267
sendRequest(ReadResourceRequest.methodName, request);
267268

269+
/// Lists all the [ResourceTemplate]s from this server.
270+
Future<ListResourceTemplatesResult> listResourceTemplates(
271+
ListResourceTemplatesRequest request,
272+
) => sendRequest(ListResourceTemplatesRequest.methodName, request);
273+
268274
/// Lists all the prompts from this server.
269275
Future<ListPromptsResult> listPrompts(ListPromptsRequest request) =>
270276
sendRequest(ListPromptsRequest.methodName, request);

pkgs/dart_mcp/lib/src/server/resources_support.dart

+72-9
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,38 @@
44

55
part of 'server.dart';
66

7+
typedef ReadResourceHandler =
8+
FutureOr<ReadResourceResult?> Function(ReadResourceRequest);
9+
710
/// A mixin for MCP servers which support the `resources` capability.
811
///
9-
/// Servers should add resources using the [addResource] method, typically
10-
/// inside the [initialize] method, but they may also be added after
11-
/// initialization if needed.
12+
/// Servers should add [Resource]s using the [addResource] method, typically
13+
/// inside the [initialize] method or constructor, but they may also be added
14+
/// after initialization if needed.
1215
///
1316
/// Resources can later be removed using [removeResource], or the client can be
1417
/// notified of updates using [updateResource].
1518
///
1619
/// Implements the `subscribe` and `listChanges` capabilities for clients, so
1720
/// they can be notified of changes to resources.
1821
///
22+
/// Any [ResourceTemplate]s, should typically be added in [initialize] method or
23+
/// the constructor using [addResourceTemplate]. There is no notification
24+
/// protocol for templates which are added after a client requests them once, so
25+
/// they should be added eagerly.
26+
///
1927
/// See https://modelcontextprotocol.io/docs/concepts/resources.
2028
base mixin ResourcesSupport on MCPServer {
2129
/// The current resources by URI.
2230
final Map<String, Resource> _resources = {};
2331

2432
/// The current resource implementations by URI.
25-
final Map<String, FutureOr<ReadResourceResult> Function(ReadResourceRequest)>
26-
_resourceImpls = {};
33+
final Map<String, ReadResourceHandler> _resourceImpls = {};
34+
35+
/// All the resource templates supported by this server, see
36+
/// [addResourceTemplate].
37+
final List<({ResourceTemplate template, ReadResourceHandler handler})>
38+
_resourceTemplates = [];
2739

2840
/// The list of currently subscribed resources by URI.
2941
final Set<String> _subscribedResources = {};
@@ -39,6 +51,10 @@ base mixin ResourcesSupport on MCPServer {
3951
@override
4052
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
4153
registerRequestHandler(ListResourcesRequest.methodName, _listResources);
54+
registerRequestHandler(
55+
ListResourceTemplatesRequest.methodName,
56+
_listResourceTemplates,
57+
);
4258

4359
registerRequestHandler(ReadResourceRequest.methodName, _readResource);
4460

@@ -57,7 +73,7 @@ base mixin ResourcesSupport on MCPServer {
5773
/// If this server is already initialized and still connected to a client,
5874
/// then the client will be notified that the resources list has changed.
5975
///
60-
/// Throws a [StateError] if there is already a [Tool] registered with the
76+
/// Throws a [StateError] if there is already a [Resource] registered with the
6177
/// same name.
6278
void addResource(
6379
Resource resource,
@@ -77,6 +93,43 @@ base mixin ResourcesSupport on MCPServer {
7793
}
7894
}
7995

96+
/// Adds the [ResourceTemplate] [template] with [handler].
97+
///
98+
/// When reading resources, first regular resources added by [addResource]
99+
/// are prioritized. Then, we call the [handler] for each [template], in the
100+
/// order they were added (using this method), and the first one to return a
101+
/// non-null response wins. This package does not automatically handle
102+
/// matching of templates and handlers must accept URIs in any form.
103+
///
104+
/// Throws a [StateError] if there is already a template registered with the
105+
/// same uri template.
106+
void addResourceTemplate(
107+
ResourceTemplate template,
108+
ReadResourceHandler handler,
109+
) {
110+
if (_resourceTemplates.any(
111+
(t) => t.template.uriTemplate == template.uriTemplate,
112+
)) {
113+
throw StateError(
114+
'Failed to add resource template ${template.name}, there is '
115+
'already a resource template with the same uri pattern '
116+
'${template.uriTemplate}.',
117+
);
118+
}
119+
_resourceTemplates.add((template: template, handler: handler));
120+
}
121+
122+
/// Lists all the [ResourceTemplate]s currently available.
123+
ListResourceTemplatesResult _listResourceTemplates(
124+
ListResourceTemplatesRequest request,
125+
) {
126+
return ListResourceTemplatesResult(
127+
resourceTemplates: [
128+
for (var descriptor in _resourceTemplates) descriptor.template,
129+
],
130+
);
131+
}
132+
80133
/// Notifies the client that [resource] has been updated.
81134
///
82135
/// The implementation of that resource can optionally be updated, otherwise
@@ -121,13 +174,23 @@ base mixin ResourcesSupport on MCPServer {
121174
///
122175
/// Throws an [ArgumentError] if it does not exist (this gets translated into
123176
/// a generic JSON RPC2 error response).
124-
FutureOr<ReadResourceResult> _readResource(ReadResourceRequest request) {
177+
FutureOr<ReadResourceResult> _readResource(
178+
ReadResourceRequest request,
179+
) async {
125180
final impl = _resourceImpls[request.uri];
126181
if (impl == null) {
127-
throw ArgumentError.value(request.uri, 'uri', 'Resource not found');
182+
// Check if it matches any resource template.
183+
for (var descriptor in _resourceTemplates) {
184+
final response = await descriptor.handler(request);
185+
if (response != null) return response;
186+
}
128187
}
129188

130-
return impl(request);
189+
final response = await impl?.call(request);
190+
if (response == null) {
191+
throw ArgumentError.value(request.uri, 'uri', 'Resource not found');
192+
}
193+
return response;
131194
}
132195

133196
/// Subscribes the client to the resource at `request.uri`.

pkgs/dart_mcp/test/resources_support_test.dart

+57
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:io';
7+
import 'dart:isolate';
68

79
import 'package:async/async.dart';
810
import 'package:dart_mcp/server.dart';
@@ -133,6 +135,35 @@ void main() {
133135
/// complete.
134136
await environment.shutdown();
135137
});
138+
139+
test('Resource templates can be listed and queried', () async {
140+
final environment = TestEnvironment(
141+
TestMCPClient(),
142+
(c) => TestMCPServerWithResources(channel: c),
143+
);
144+
await environment.initializeServer();
145+
146+
final serverConnection = environment.serverConnection;
147+
148+
final templatesResponse = await serverConnection.listResourceTemplates(
149+
ListResourceTemplatesRequest(),
150+
);
151+
152+
expect(
153+
templatesResponse.resourceTemplates.single,
154+
TestMCPServerWithResources.packageUriTemplate,
155+
);
156+
157+
final readResourceResponse = await serverConnection.readResource(
158+
ReadResourceRequest(uri: 'package:test/test.dart'),
159+
);
160+
expect(
161+
(readResourceResponse.contents.single as TextResourceContents).text,
162+
await File.fromUri(
163+
(await Isolate.resolvePackageUri(Uri.parse('package:test/test.dart')))!,
164+
).readAsString(),
165+
);
166+
});
136167
}
137168

138169
final class TestMCPServerWithResources extends TestMCPServer
@@ -149,8 +180,34 @@ final class TestMCPServerWithResources extends TestMCPServer
149180
],
150181
),
151182
);
183+
addResourceTemplate(packageUriTemplate, _readPackageResource);
152184
return super.initialize(request);
153185
}
154186

187+
Future<ReadResourceResult?> _readPackageResource(
188+
ReadResourceRequest request,
189+
) async {
190+
if (!request.uri.startsWith('package:')) return null;
191+
if (!request.uri.endsWith('.dart')) {
192+
throw UnsupportedError('Only dart files can be read');
193+
}
194+
final resolvedUri =
195+
(await Isolate.resolvePackageUri(Uri.parse(request.uri)))!;
196+
197+
return ReadResourceResult(
198+
contents: [
199+
TextResourceContents(
200+
uri: request.uri,
201+
text: await File.fromUri(resolvedUri).readAsString(),
202+
),
203+
],
204+
);
205+
}
206+
155207
static final helloWorld = Resource(name: 'hello world', uri: 'hello://world');
208+
209+
static final packageUriTemplate = ResourceTemplate(
210+
uriTemplate: 'package:{package}/{library}',
211+
name: 'Dart package resource',
212+
);
156213
}

0 commit comments

Comments
 (0)