blob: 536d88cc75e00d4deeb9920c92e9a127785a503d [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:async/async.dart';
import 'package:dart_mcp/server.dart';
import 'package:test/test.dart';
import '../test_utils.dart';
void main() {
test('client can read resources from the server', () async {
final environment = TestEnvironment(
TestMCPClient(),
TestMCPServerWithResources.new,
);
final initializeResult = await environment.initializeServer();
expect(
initializeResult.capabilities.resources,
equals(Resources(listChanged: true, subscribe: true)),
);
final serverConnection = environment.serverConnection;
final resourcesResult = await serverConnection.listResources(
ListResourcesRequest(),
);
expect(resourcesResult.resources.length, 1);
final resource = resourcesResult.resources.single;
final result = await serverConnection.readResource(
ReadResourceRequest(uri: resource.uri),
);
expect(
result.contents.single,
isA<ResourceContents>()
.having((c) => c.isText, 'isText', true)
.having(
(c) => (c as TextResourceContents).text,
'text',
'hello world!',
),
);
});
test('client can subscribe to resource updates from the server', () async {
final environment = TestEnvironment(
TestMCPClient(),
TestMCPServerWithResources.new,
);
await environment.initializeServer();
final serverConnection = environment.serverConnection;
final server = environment.server;
final resourceListChangedQueue = StreamQueue(
serverConnection.resourceListChanged,
);
final fooResource = Resource(name: 'foo', uri: 'foo://bar');
var fooContents = 'bar';
server.addResource(
fooResource,
(_) => ReadResourceResult(
contents: [
TextResourceContents(uri: fooResource.uri, text: fooContents),
],
),
);
await resourceListChangedQueue.next;
final resources = await serverConnection.listResources(
ListResourcesRequest(),
);
expect(
resources.resources,
unorderedEquals([fooResource, TestMCPServerWithResources.helloWorld]),
);
final resourceChangedQueue = StreamQueue(serverConnection.resourceUpdated);
await serverConnection.subscribeResource(
SubscribeRequest(uri: fooResource.uri),
);
fooContents = 'baz';
server.updateResource(fooResource);
expect(
await resourceChangedQueue.next,
isA<ResourceUpdatedNotification>().having(
(n) => n.uri,
'uri',
fooResource.uri,
),
);
expect(
await serverConnection.readResource(
ReadResourceRequest(uri: fooResource.uri),
),
isA<ReadResourceResult>().having(
(r) => r.contents.single,
'contents',
isA<TextResourceContents>()
.having((c) => c.text, 'text', 'baz')
.having((c) => c.uri, 'uri', fooResource.uri),
),
);
await serverConnection.unsubscribeResource(
UnsubscribeRequest(uri: fooResource.uri),
);
fooContents = 'zap';
server.updateResource(fooResource);
expect(resourceChangedQueue.hasNext, completion(false));
server.removeResource(fooResource.uri);
expect(
await resourceListChangedQueue.next,
ResourceListChangedNotification(),
);
expect(resourceListChangedQueue.hasNext, completion(false));
/// We need to manually shut down to so that the `hasNext` futures can
/// complete.
await environment.shutdown();
});
test('resource change notifications are throttled', () async {
final environment = TestEnvironment(
TestMCPClient(),
TestMCPServerWithResources.new,
);
await environment.initializeServer();
final serverConnection = environment.serverConnection;
final server = environment.server;
final resourceListChangedQueue = StreamQueue(
serverConnection.resourceListChanged,
);
final resources = [
for (var i = 0; i < 5; i++) Resource(name: '$i', uri: 'foo://$i'),
];
for (var resource in resources) {
server.addResource(
resource,
(_) => ReadResourceResult(
contents: [
TextResourceContents(uri: resource.uri, text: resource.name),
],
),
);
}
// Should get exactly two notifications even though we have more resources,
// one initial notification and one after the throttle delay.
await resourceListChangedQueue.take(2);
expect(resourceListChangedQueue.hasNext, completion(false));
await pumpEventQueue();
final resourceChangedQueue = StreamQueue(serverConnection.resourceUpdated);
final resource = resources.first;
await serverConnection.subscribeResource(
SubscribeRequest(uri: resource.uri),
);
// Allow the subscription to propagate.
await pumpEventQueue();
// Send 5 notifications back to back.
for (var i = 0; i < 5; i++) {
server.updateResource(resource);
}
// Only two should make it through, one at the start and one after the
// timeout.
for (var i = 0; i < 2; i++) {
expect(
await resourceChangedQueue.next,
isA<ResourceUpdatedNotification>().having(
(n) => n.uri,
'uri',
resource.uri,
),
);
}
expect(resourceChangedQueue.hasNext, completion(false));
await pumpEventQueue();
await environment.shutdown();
});
test('Resource templates can be listed and queried', () async {
final environment = TestEnvironment(
TestMCPClient(),
(channel) => TestMCPServerWithResources(
channel,
fileContents: {'package:foo/foo.dart': 'hello world!'},
),
);
await environment.initializeServer();
final serverConnection = environment.serverConnection;
final templatesResponse = await serverConnection.listResourceTemplates(
ListResourceTemplatesRequest(),
);
expect(
templatesResponse.resourceTemplates.single,
TestMCPServerWithResources.packageUriTemplate,
);
final readResourceResponse = await serverConnection.readResource(
ReadResourceRequest(uri: 'package:foo/foo.dart'),
);
expect(
(readResourceResponse.contents.single as TextResourceContents).text,
'hello world!',
);
});
}
final class TestMCPServerWithResources extends TestMCPServer
with ResourcesSupport {
final Map<String, String> fileContents;
@override
/// Shorten this delay for the test so they run quickly.
Duration get resourceUpdateThrottleDelay => Duration.zero;
TestMCPServerWithResources(super.channel, {this.fileContents = const {}});
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
addResource(
helloWorld,
(_) => ReadResourceResult(
contents: [
TextResourceContents(text: 'hello world!', uri: helloWorld.uri),
],
),
);
addResourceTemplate(packageUriTemplate, _readPackageResource);
return super.initialize(request);
}
Future<ReadResourceResult?> _readPackageResource(
ReadResourceRequest request,
) async {
if (!request.uri.startsWith('package:')) return null;
if (!request.uri.endsWith('.dart')) {
throw UnsupportedError('Only dart files can be read');
}
return ReadResourceResult(
contents: [
TextResourceContents(
uri: request.uri,
text: fileContents[request.uri]!,
),
],
);
}
static final helloWorld = Resource(name: 'hello world', uri: 'hello://world');
static final packageUriTemplate = ResourceTemplate(
uriTemplate: 'package:{package}/{library}',
name: 'Dart package resource',
);
}