re-implement most of #440 for the current stable release (#441)
diff --git a/pkgs/dart_mcp_server/CHANGELOG.md b/pkgs/dart_mcp_server/CHANGELOG.md
index d5b5cb4..57d534d 100644
--- a/pkgs/dart_mcp_server/CHANGELOG.md
+++ b/pkgs/dart_mcp_server/CHANGELOG.md
@@ -1,4 +1,9 @@
-# 0.1.2 (Dart SDK 3.11.0) - WIP
+# 0.1.2+1 (Dart SDK 3.11.5)
+
+- Always enable roots fallback, and merge them with client roots that are set
+ (if any).
+
+# 0.1.2 (Dart SDK 3.11.0)
- Add `--tools=dart|all` argument to allow enabling only vanilla Dart tools for
non-flutter projects.
diff --git a/pkgs/dart_mcp_server/README.md b/pkgs/dart_mcp_server/README.md
index 6ea2dc9..6ff58d1 100644
--- a/pkgs/dart_mcp_server/README.md
+++ b/pkgs/dart_mcp_server/README.md
@@ -18,10 +18,6 @@
experience with the Dart MCP server, an MCP client should also support
[Roots](https://modelcontextprotocol.io/docs/concepts/roots).
-If you are using a client that claims it supports roots but does not actually
-set them, pass `--force-roots-fallback` which will instead enable tools for
-managing the roots.
-
Here are specific instructions for some popular tools:
### Gemini CLI
@@ -91,7 +87,6 @@
"args": [
"mcp-server",
"--experimental-mcp-server", // Can be removed for Dart 3.9.0 or later
- "--force-roots-fallback" // Workaround for a Cursor issue with Roots support
]
}
}
diff --git a/pkgs/dart_mcp_server/lib/src/arg_parser.dart b/pkgs/dart_mcp_server/lib/src/arg_parser.dart
index c01bdb6..6667432 100644
--- a/pkgs/dart_mcp_server/lib/src/arg_parser.dart
+++ b/pkgs/dart_mcp_server/lib/src/arg_parser.dart
@@ -35,12 +35,13 @@
..addFlag(
forceRootsFallbackFlag,
negatable: true,
- defaultsTo: false,
+ defaultsTo: true,
help:
'Forces a behavior for project roots which uses MCP tools '
'instead of the native MCP roots. This can be helpful for '
'clients like Cursor which claim to have roots support but do '
'not actually support it.',
+ hide: true,
)
..addOption(
logFileOption,
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart
index 0430ea0..43aca5c 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart
@@ -106,6 +106,7 @@
'--device-id',
device,
if (target != null) '--target',
+ // ignore: use_null_aware_elements
if (target != null) target,
],
workingDirectory: root,
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
index a064253..77ac94c 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
@@ -69,6 +69,7 @@
return runCommandInRoots(
request,
+ // ignore: use_null_aware_elements
arguments: ['pub', command, if (packageNames != null) ...packageNames],
commandDescription: 'dart|flutter pub $command',
processManager: processManager,
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart
index 45c31d7..f5d8dc5 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart
@@ -108,6 +108,7 @@
'latest',
'version',
]),
+ // ignore: use_null_aware_elements
if (dig<String?>(versionListing, [
'latest',
'pubspec',
@@ -115,6 +116,7 @@
])
case final description?)
'description': description,
+ // ignore: use_null_aware_elements
if (dig<String?>(versionListing, [
'latest',
'pubspec',
@@ -122,6 +124,7 @@
])
case final homepage?)
'homepage': homepage,
+ // ignore: use_null_aware_elements
if (dig<String?>(versionListing, [
'latest',
'pubspec',
@@ -129,6 +132,7 @@
])
case final repository?)
'repository': repository,
+ // ignore: use_null_aware_elements
if (dig<String?>(versionListing, [
'latest',
'pubspec',
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart b/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart
index 0eb1a8c..845956b 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart
@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:collection';
+import 'package:async/async.dart';
import 'package:dart_mcp/server.dart';
import 'package:meta/meta.dart';
@@ -45,14 +46,24 @@
// if they do. If we are implementing the support, we always support it.
_fallbackEnabled ? true : super.supportsRootsChanged;
+ /// Combines the client stream and the fallback controller stream.
@override
- Stream<RootsListChangedNotification?>? get rootsListChanged =>
- // If the client supports roots, just use their stream (or lack thereof).
- // If they don't, use our own stream.
- _fallbackEnabled
- ? _rootsListChangedFallbackController?.stream
- : super.rootsListChanged;
+ Stream<RootsListChangedNotification?>? get rootsListChanged {
+ final clientStream = super.rootsListChanged;
+ if (clientStream == null) {
+ return _rootsListChangedFallbackController?.stream;
+ } else if (_rootsListChangedFallbackController == null) {
+ return clientStream;
+ } else {
+ return StreamGroup.merge([
+ clientStream,
+ _rootsListChangedFallbackController!.stream,
+ ]);
+ }
+ }
+ /// Broadcast controller for roots list changed events from usage of the
+ /// roots tool.
StreamController<RootsListChangedNotification?>?
_rootsListChangedFallbackController;
@@ -71,13 +82,30 @@
}
}
- /// Delegates to the inherited implementation if fallback mode is not enabled,
- /// otherwise returns our own custom roots.
+ /// Returns the custom roots combined with the client's roots.
@override
- Future<ListRootsResult> listRoots([ListRootsRequest? request]) async =>
- _fallbackEnabled
- ? ListRootsResult(roots: _customRoots.toList())
- : super.listRoots(request);
+ Future<ListRootsResult> listRoots([ListRootsRequest? request]) async {
+ final clientRoots = <Root>[];
+ if (super.supportsRoots) {
+ try {
+ final result = await super.listRoots(request);
+ clientRoots.addAll(result.roots);
+ } catch (e, s) {
+ log(LoggingLevel.error, 'Failed to list roots from client: $e\n$s');
+ }
+ }
+
+ final seenUris = <String>{};
+ final allRoots = <Root>[];
+
+ for (final root in clientRoots.followedBy(_customRoots)) {
+ if (seenUris.add(root.uri)) {
+ allRoots.add(root);
+ }
+ }
+
+ return ListRootsResult(roots: allRoots);
+ }
/// Adds the roots in [request] the custom roots and calls [updateRoots].
///
diff --git a/pkgs/dart_mcp_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart
index d9e0220..f560638 100644
--- a/pkgs/dart_mcp_server/lib/src/server.dart
+++ b/pkgs/dart_mcp_server/lib/src/server.dart
@@ -77,7 +77,7 @@
}) : super.fromStreamChannel(
implementation: Implementation(
name: 'dart and flutter tooling',
- version: '0.1.2',
+ version: '0.1.2+1',
),
instructions:
'This server helps to connect Dart and Flutter developers to '
diff --git a/pkgs/dart_mcp_server/test/tools/roots_fallback_support_test.dart b/pkgs/dart_mcp_server/test/tools/roots_fallback_support_test.dart
index a93865a..3b4848e 100644
--- a/pkgs/dart_mcp_server/test/tools/roots_fallback_support_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/roots_fallback_support_test.dart
@@ -13,7 +13,7 @@
import 'package:test/test.dart';
void main() {
- late RootsTrackingSupport server;
+ late TestServer server;
late ServerConnection serverConnection;
final rootA = Root(uri: 'file:///a/');
final rootB = Root(uri: 'file:///b/');
@@ -149,38 +149,97 @@
await server.roots; // wait for the first listRoots request to complete
});
- test('registers no tools', () async {
+ test('registers tools to add and remove roots', () async {
final tools = await serverConnection.listTools(ListToolsRequest());
- expect(tools.tools, isEmpty);
+ expect(
+ tools.tools,
+ unorderedEquals([
+ RootsFallbackSupport.addRootsTool,
+ RootsFallbackSupport.removeRootsTool,
+ ]),
+ );
});
- test('Gives roots changed notifications when roots are added', () async {
- final notifications = StreamQueue(server.rootsListChanged!);
- client.addRoot(rootA);
- expect(await notifications.hasNext, true);
- await notifications.next;
+ test(
+ 'Gives roots changed notifications when roots are added by the client',
+ () async {
+ final notifications = StreamQueue(server.rootsListChanged!);
+ client.addRoot(rootA);
+ expect(await notifications.hasNext, true);
+ await notifications.next;
- client.removeRoot(rootA);
- expect(await notifications.hasNext, true);
- await notifications.next;
- });
+ client.removeRoot(rootA);
+ expect(await notifications.hasNext, true);
+ await notifications.next;
+ },
+ );
- test('can add, remove, and list roots', () async {
+ test(
+ 'Gives roots changed notifications when roots are added by the tool',
+ () async {
+ var next = server.rootsListChanged!.first;
+ unawaited(
+ serverConnection.callTool(
+ CallToolRequest(
+ name: RootsFallbackSupport.addRootsTool.name,
+ arguments: {
+ ParameterNames.roots: [rootA],
+ },
+ ),
+ ),
+ );
+ await next;
+
+ next = server.rootsListChanged!.first;
+ unawaited(
+ serverConnection.callTool(
+ CallToolRequest(
+ name: RootsFallbackSupport.removeRootsTool.name,
+ arguments: {
+ ParameterNames.uris: [rootA.uri],
+ },
+ ),
+ ),
+ );
+ await next;
+ },
+ );
+
+ test('can add, remove, and list roots using client or tools', () async {
expect((await server.listRoots(ListRootsRequest())).roots, isEmpty);
- client
- ..addRoot(rootA)
- ..addRoot(rootB);
+ client.addRoot(rootA);
+ await serverConnection.callTool(
+ CallToolRequest(
+ name: RootsFallbackSupport.addRootsTool.name,
+ arguments: {
+ ParameterNames.roots: [rootB],
+ },
+ ),
+ );
+ await pumpEventQueue();
expect(
(await server.listRoots(ListRootsRequest())).roots,
unorderedEquals([rootA, rootB]),
);
- client.removeRoot(rootB);
+ client.removeRoot(rootA);
+ await pumpEventQueue();
expect(
(await server.listRoots(ListRootsRequest())).roots,
- unorderedEquals([rootA]),
+ unorderedEquals([rootB]),
);
+
+ await serverConnection.callTool(
+ CallToolRequest(
+ name: RootsFallbackSupport.removeRootsTool.name,
+ arguments: {
+ ParameterNames.uris: [rootB.uri],
+ },
+ ),
+ );
+ await pumpEventQueue();
+ expect((await server.listRoots(ListRootsRequest())).roots, isEmpty);
});
});
});
@@ -221,7 +280,7 @@
TestServer(
super.channel, {
super.protocolLogSink,
- this.forceRootsFallback = false,
+ this.forceRootsFallback = true,
}) : super.fromStreamChannel(
implementation: Implementation(name: 'test server', version: '0.1.0'),
instructions: 'A test server with roots fallback support',