Enable roots tools always, collapse under one tool (#440)

Fixes https://github.com/dart-lang/ai/issues/439 - and hopefully all future roots issues.

This enables adding/removing roots at will and will merge those with any roots set by the client.

## Security implications

The Dart MCP server in general doesn't perform destructive operations (outside of dart format), so security implications here are light. It does mean that an agent attached to one workspace, can actually interact with other dart projects through the MCP server, but I think that is generally a plus.

Everything is still guarded behind a tool call, so users can choose to not auto-approve based on their agent settings, and any adding/removing of roots will be explicit in that mode.
diff --git a/pkgs/dart_mcp_server/CHANGELOG.md b/pkgs/dart_mcp_server/CHANGELOG.md
index 6d10bd4..60abf58 100644
--- a/pkgs/dart_mcp_server/CHANGELOG.md
+++ b/pkgs/dart_mcp_server/CHANGELOG.md
@@ -1,3 +1,8 @@
+# 0.1.4 (Dart SDK 3.13.0-WIP)
+
+- Always enable roots fallback, and merge them with client roots that are set
+  (if any).
+
 # 0.1.3 (Dart SDK 3.12.0)
 
 - Add additional analytics for initialization events, and various list\* method
diff --git a/pkgs/dart_mcp_server/README.md b/pkgs/dart_mcp_server/README.md
index fa2a4cf..d03a9f2 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
@@ -90,8 +86,7 @@
       "command": "dart",
       "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
+        "--experimental-mcp-server" // Can be removed for Dart 3.9.0 or later
       ]
     }
   }
@@ -183,7 +178,6 @@
 
 | Tool Name | Title | Description | Categories | Enabled |
 | --- | --- | --- | --- | --- |
-| `add_roots` | Add roots | Adds one or more project roots. Tools are only allowed to run under these roots, so you must call this function before passing any roots to any other tools. | None | Yes |
 | `analyze_files` | Analyze projects | Analyzes specific paths, or the entire project, for errors. | analysis | Yes |
 | `create_project` | Create project | Creates a new Dart or Flutter project. | cli | No |
 | `dart_fix` | Dart fix | Runs `dart fix --apply` for the given project roots. | cli | No |
@@ -202,8 +196,8 @@
 | `pub` | pub | Runs a pub command for the given project roots, like `dart pub get` or `flutter pub add`. | cli, package_deps | Yes |
 | `pub_dev_search` | pub.dev search | Searches pub.dev for packages relevant to a given search query. The response will describe each result with its download count, package description, topics, license, and publisher. | package_deps | Yes |
 | `read_package_uris` |  | Reads "package" and "package-root" scheme URIs which represent paths under Dart package dependencies. "package" URIs are always relative to the "lib" directory and "package-root" URIs are relative to the true root directory of the package. For example, the URI "package:test/test.dart" represents the path "lib/test.dart" under the "test" package. "package-root:test/example/test.dart" represents the path "example/test.dart". This API supports both reading files and listing directories. | package_deps | Yes |
-| `remove_roots` | Remove roots | Removes one or more project roots previously added via the add_roots tool. | None | Yes |
 | `rip_grep_packages` |  | Uses ripgrep to find patterns in package dependencies. Note that ripgrep must be installed already, see https://github.com/BurntSushi/ripgrep for instructions. | package_deps | Yes |
+| `roots` |  | Manage project roots. | None | Yes |
 | `run_tests` | Run tests | Run Dart or Flutter tests with an agent centric UX. ALWAYS use instead of `dart test` or `flutter test` shell commands. | cli | No |
 | `stop_app` |  | Kills a running Flutter process started by the launch_app tool. | flutter, flutter_app_lifecycle | No |
 | `widget_inspector` | Widget Inspector | Interact with the Flutter widget inspector in the active Flutter application. Requires an active DTD connection. | flutter | Yes |
diff --git a/pkgs/dart_mcp_server/lib/src/arg_parser.dart b/pkgs/dart_mcp_server/lib/src/arg_parser.dart
index 4ab478b..dbe2ed5 100644
--- a/pkgs/dart_mcp_server/lib/src/arg_parser.dart
+++ b/pkgs/dart_mcp_server/lib/src/arg_parser.dart
@@ -48,10 +48,10 @@
           negatable: true,
           defaultsTo: false,
           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.',
+              'Deprecated: tools to manage roots are always available to '
+              'improve compatibility. Use `--$disabledFeaturesOption roots` '
+              'to disable these if desired.',
+          hide: true,
         )
         ..addOption(
           logFileOption,
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 6c036ac..b0c22d7 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,8 @@
 import 'dart:async';
 import 'dart:collection';
 
+import 'package:async/async.dart';
+
 import 'package:dart_mcp/server.dart';
 import 'package:meta/meta.dart';
 
@@ -26,151 +28,127 @@
     hashCode: (root) => root.uri.hashCode,
   );
 
-  /// Whether or not to force the fallback mode for roots, regardless of the
-  /// client's reported support.
-  ///
-  /// Override this to enable it.
-  bool get forceRootsFallback => false;
-
-  /// Whether fallback mode is enabled.
-  ///
-  /// Unsafe to call until after the server is initialized.
-  bool get _fallbackEnabled => forceRootsFallback || !super.supportsRoots;
-
   /// Always supported, either by the client or this mixin.
   @override
   bool get supportsRoots => true;
 
   @override
-  bool get supportsRootsChanged =>
-      // If the client supports roots, then we only support root change events
-      // if they do. If we are implementing the support, we always support it.
-      _fallbackEnabled ? true : super.supportsRootsChanged;
+  bool get supportsRootsChanged => true;
 
+  /// 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;
+    }
+    return StreamGroup.merge([
+      clientStream,
+      _rootsListChangedFallbackController.stream,
+    ]);
+  }
 
-  StreamController<RootsListChangedNotification?>?
-  _rootsListChangedFallbackController;
+  /// Broadcast controller for roots list changed events from usage of the
+  /// roots tool.
+  final _rootsListChangedFallbackController =
+      StreamController<RootsListChangedNotification?>.broadcast();
 
   @override
   FutureOr<InitializeResult> initialize(InitializeRequest request) async {
     try {
       return super.initialize(request);
     } finally {
-      // Can't call `super.supportsRoots` until after `super.initialize`.
-      if (_fallbackEnabled) {
-        registerTool(removeRootsTool, _removeRoots);
-        registerTool(addRootsTool, _addRoots);
-        _rootsListChangedFallbackController =
-            StreamController<RootsListChangedNotification?>.broadcast();
-      }
+      registerTool(rootsTool, _roots);
     }
   }
 
   @visibleForTesting
-  static final List<Tool> allTools = [removeRootsTool, addRootsTool];
+  static final List<Tool> allTools = [rootsTool];
 
-  /// Delegates to the inherited implementation if fallback mode is not enabled,
-  /// otherwise returns our own custom roots.
   @override
-  Future<ListRootsResult> listRoots([ListRootsRequest? request]) async =>
-      _fallbackEnabled
-      ? ListRootsResult(roots: _customRoots.toList())
-      : super.listRoots(request);
-
-  /// Adds the roots in [request] the custom roots and calls [updateRoots].
-  ///
-  /// Should only be called if [_fallbackEnabled] is `true`.
-  Future<CallToolResult> _addRoots(CallToolRequest request) async {
-    if (!_fallbackEnabled) {
-      throw StateError(
-        'This tool should not be invoked if the client supports roots',
-      );
+  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');
+      }
     }
 
-    (request.arguments![ParameterNames.roots] as List).cast<Root>().forEach(
-      _customRoots.add,
-    );
-    _rootsListChangedFallbackController?.add(RootsListChangedNotification());
+    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);
+  }
+
+  /// Handles requests to the roots tool, delegating to the subcommand.
+  Future<CallToolResult> _roots(CallToolRequest request) async {
+    final command = request.arguments![ParameterNames.command] as String;
+    switch (command) {
+      case RootsCommands.add:
+        return _addRoots(request);
+      case RootsCommands.remove:
+        return _removeRoots(request);
+      default:
+        return CallToolResult(
+          isError: true,
+          content: [TextContent(text: 'Unknown command: $command')],
+        );
+    }
+  }
+
+  Future<CallToolResult> _addRoots(CallToolRequest request) async {
+    final uris = (request.arguments?[ParameterNames.uris] as List)
+        .cast<String>();
+    _customRoots.addAll(uris.map((u) => Root(uri: u)));
+    _rootsListChangedFallbackController.add(RootsListChangedNotification());
     return success;
   }
 
-  /// Removes the roots in [request] from the custom roots and calls
-  /// [updateRoots].
-  ///
-  /// Should only be called if [_fallbackEnabled] is true.
   Future<CallToolResult> _removeRoots(CallToolRequest request) async {
-    if (!_fallbackEnabled) {
-      throw StateError(
-        'This tool should not be invoked if the client supports roots',
-      );
-    }
-
-    final roots = (request.arguments![ParameterNames.uris] as List)
-        .cast<String>()
-        .map((uri) => Root(uri: uri));
-    _customRoots.removeAll(roots);
-    _rootsListChangedFallbackController?.add(RootsListChangedNotification());
-
+    final uris = (request.arguments?[ParameterNames.uris] as List)
+        .cast<String>();
+    _customRoots.removeAll(uris.map((u) => Root(uri: u)));
+    _rootsListChangedFallbackController.add(RootsListChangedNotification());
     return success;
   }
 
   @override
   Future<void> shutdown() async {
     await super.shutdown();
-    await _rootsListChangedFallbackController?.close();
+    await _rootsListChangedFallbackController.close();
   }
 
   @visibleForTesting
-  static final addRootsTool = Tool(
-    name: ToolNames.addRoots.name,
-    description:
-        'Adds one or more project roots. Tools are only allowed to run under '
-        'these roots, so you must call this function before passing any roots '
-        'to any other tools.',
-    annotations: ToolAnnotations(title: 'Add roots', readOnlyHint: false),
+  static final rootsTool = Tool(
+    name: ToolNames.roots.name,
+    description: 'Manage project roots.',
     inputSchema: Schema.object(
       properties: {
-        ParameterNames.roots: Schema.list(
-          description: 'All the project roots to add to this server.',
-          items: Schema.object(
-            properties: {
-              ParameterNames.uri: Schema.string(
-                description: 'The URI of the root.',
-              ),
-              ParameterNames.name: Schema.string(
-                description: 'An optional name of the root.',
-              ),
-            },
-            required: [ParameterNames.uri],
-          ),
+        ParameterNames.command: EnumSchema.untitledSingleSelect(
+          description: 'The command to execute.',
+          values: [RootsCommands.add, RootsCommands.remove],
         ),
-      },
-      additionalProperties: false,
-    ),
-  )..categories = [FeatureCategory.all];
-
-  @visibleForTesting
-  static final removeRootsTool = Tool(
-    name: ToolNames.removeRoots.name,
-    description:
-        'Removes one or more project roots previously added via '
-        'the add_roots tool.',
-    annotations: ToolAnnotations(title: 'Remove roots', readOnlyHint: false),
-    inputSchema: Schema.object(
-      properties: {
         ParameterNames.uris: Schema.list(
-          description: 'All the project roots to remove from this server.',
-          items: Schema.string(description: 'The URIs of the roots to remove.'),
+          description: 'The URIs to add or remove as roots.',
+          items: Schema.string(),
         ),
       },
+      required: [ParameterNames.command, ParameterNames.uris],
       additionalProperties: false,
     ),
   )..categories = [FeatureCategory.all];
 }
+
+extension RootsCommands on Never {
+  static const add = 'add';
+  static const remove = 'remove';
+}
diff --git a/pkgs/dart_mcp_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart
index 8825ee7..b20dede 100644
--- a/pkgs/dart_mcp_server/lib/src/server.dart
+++ b/pkgs/dart_mcp_server/lib/src/server.dart
@@ -72,9 +72,6 @@
   final FileSystem fileSystem;
 
   @override
-  final bool forceRootsFallback;
-
-  @override
   final Sdk sdk;
 
   @override
@@ -87,7 +84,6 @@
     this.analytics,
     @visibleForTesting this.processManager = const LocalProcessManager(),
     @visibleForTesting this.fileSystem = const LocalFileSystem(),
-    this.forceRootsFallback = false,
     super.protocolLogSink,
   }) : super.fromStreamChannel(
          implementation: Implementation(
@@ -114,7 +110,7 @@
   /// The version of the MCP server.
   ///
   /// Should match the version in the CHANGELOG.md.
-  static final version = '0.1.3';
+  static final version = '0.1.4';
 
   /// Runs the MCP server given command line arguments and an optional
   /// [Analytics] instance.
@@ -155,7 +151,6 @@
         server = DartMCPServer(
           channel ?? stdioChannel(input: io.stdin, output: io.stdout),
           featuresConfig: FeaturesConfiguration.fromArgs(parsedArgs),
-          forceRootsFallback: parsedArgs.flag(forceRootsFallbackFlag),
           sdk: Sdk.find(
             dartSdkPath: dartSdkPath,
             flutterSdkPath: flutterSdkPath,
diff --git a/pkgs/dart_mcp_server/lib/src/utils/names.dart b/pkgs/dart_mcp_server/lib/src/utils/names.dart
index 94b1b38..db1a2fa 100644
--- a/pkgs/dart_mcp_server/lib/src/utils/names.dart
+++ b/pkgs/dart_mcp_server/lib/src/utils/names.dart
@@ -36,7 +36,6 @@
 
 /// The names of all the tools provided by the server.
 enum ToolNames {
-  addRoots('add_roots'),
   analyzeFiles('analyze_files'),
   createProject('create_project'),
   dartFix('dart_fix'),
@@ -55,8 +54,8 @@
   pub('pub'),
   pubDevSearch('pub_dev_search'),
   readPackageUris('read_package_uris'),
-  removeRoots('remove_roots'),
   ripGrepPackages('rip_grep_packages'),
+  roots('roots'),
   runTests('run_tests'),
   stopApp('stop_app'),
   widgetInspector('widget_inspector');
diff --git a/pkgs/dart_mcp_server/test/features_configuration_test.dart b/pkgs/dart_mcp_server/test/features_configuration_test.dart
index 5ca1517..f66bfd9 100644
--- a/pkgs/dart_mcp_server/test/features_configuration_test.dart
+++ b/pkgs/dart_mcp_server/test/features_configuration_test.dart
@@ -104,10 +104,7 @@
     });
 
     test('runtime validation with TestHarness', () async {
-      final harness = await TestHarness.start(
-        inProcess: true,
-        forceRootsFallback: true,
-      );
+      final harness = await TestHarness.start(inProcess: true);
 
       final toolsResult = await harness.mcpServerConnection.listTools();
       final promptsResult = await harness.mcpServerConnection.listPrompts();
diff --git a/pkgs/dart_mcp_server/test/test_harness.dart b/pkgs/dart_mcp_server/test/test_harness.dart
index 6a5daa3..0ec03ca 100644
--- a/pkgs/dart_mcp_server/test/test_harness.dart
+++ b/pkgs/dart_mcp_server/test/test_harness.dart
@@ -100,7 +100,6 @@
     FileSystem? fileSystem,
     ProcessManager? processManager,
     List<String> cliArgs = const [],
-    bool forceRootsFallback = false,
     Sdk? sdk,
     bool startFakeEditorExtension = true,
     FeaturesConfiguration featuresConfig = const FeaturesConfiguration(),
@@ -122,7 +121,6 @@
       processManager,
       sdk,
       cliArgs,
-      forceRootsFallback,
       featuresConfig,
     );
     final connection = serverConnectionPair.serverConnection;
@@ -484,7 +482,6 @@
   ProcessManager processManager,
   Sdk sdk,
   List<String> cliArgs,
-  bool forceRootsFallback,
   FeaturesConfiguration featuresConfig,
 ) async {
   ServerConnection connection;
@@ -535,16 +532,10 @@
       fileSystem: fileSystem,
       sdk: sdk,
       analytics: analytics,
-      forceRootsFallback: forceRootsFallback,
     );
     addTearDown(server.shutdown);
     connection = client.connectServer(clientChannel);
   } else {
-    assert(
-      !forceRootsFallback,
-      'forceRootsFallback is not supported when running in process, pass the '
-      '--force-roots-fallback clie arg instead',
-    );
     final process = await Process.start(sdk.dartExecutablePath, [
       'pub', // Using `pub` gives us incremental compilation
       'run',
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 a9cd906..29ecbac 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
@@ -9,221 +9,96 @@
 import 'package:dart_mcp/server.dart';
 import 'package:dart_mcp_server/src/mixins/roots_fallback_support.dart';
 import 'package:dart_mcp_server/src/utils/names.dart';
-import 'package:stream_channel/stream_channel.dart';
 import 'package:test/test.dart';
 
+import '../test_harness.dart';
+
 void main() {
-  late RootsTrackingSupport server;
-  late ServerConnection serverConnection;
+  late TestHarness harness;
   final rootA = Root(uri: 'file:///a/');
   final rootB = Root(uri: 'file:///b/');
 
-  late StreamController<String> clientController;
-  late StreamController<String> serverController;
-
   setUp(() async {
-    clientController = StreamController<String>();
-    serverController = StreamController<String>();
-    server = TestServer(
-      StreamChannel.withCloseGuarantee(
-        serverController.stream,
-        clientController.sink,
-      ),
-    );
-    addTearDown(() async {
-      await clientController.close();
-      await serverController.close();
-    });
+    harness = await TestHarness.start(inProcess: true);
   });
 
   group('RootsFallbackSupport', () {
-    group('when the client doesn\'t support roots', () {
-      late TestClientWithoutRoots client;
+    Future<void> addRoots(List<String> roots) async {
+      await harness.mcpServerConnection.callTool(
+        CallToolRequest(
+          name: ToolNames.roots.name,
+          arguments: {
+            ParameterNames.command: RootsCommands.add,
+            ParameterNames.uris: roots,
+          },
+        ),
+      );
+    }
 
-      Future<void> addRoots(List<Root> roots) async {
-        await serverConnection.callTool(
-          CallToolRequest(
-            name: RootsFallbackSupport.addRootsTool.name,
-            arguments: {ParameterNames.roots: roots},
-          ),
-        );
-      }
+    Future<void> removeRoots(List<String> roots) async {
+      await harness.mcpServerConnection.callTool(
+        CallToolRequest(
+          name: ToolNames.roots.name,
+          arguments: {
+            ParameterNames.command: RootsCommands.remove,
+            ParameterNames.uris: roots,
+          },
+        ),
+      );
+    }
 
-      Future<void> removeRoots(List<Root> roots) async {
-        await serverConnection.callTool(
-          CallToolRequest(
-            name: RootsFallbackSupport.removeRootsTool.name,
-            arguments: {
-              ParameterNames.uris: [for (final root in roots) root.uri],
-            },
-          ),
-        );
-      }
-
-      setUp(() async {
-        client = TestClientWithoutRoots();
-        addTearDown(client.shutdown);
-        serverConnection = client.connectServer(
-          StreamChannel.withCloseGuarantee(
-            clientController.stream,
-            serverController.sink,
-          ),
-        );
-        await serverConnection.initialize(
-          InitializeRequest(
-            protocolVersion: ProtocolVersion.latestSupported,
-            capabilities: client.capabilities,
-            clientInfo: client.implementation,
-          ),
-        );
-        serverConnection.notifyInitialized();
-      });
-
-      test('supportsRoots is true', () async {
-        expect(server.supportsRoots, isTrue);
-      });
-
-      test('registers tools to add and remove roots', () async {
-        final tools = await serverConnection.listTools(ListToolsRequest());
-        expect(
-          tools.tools,
-          unorderedEquals([
-            RootsFallbackSupport.addRootsTool,
-            RootsFallbackSupport.removeRootsTool,
-          ]),
-        );
-      });
-
-      test('Gives roots changed notifications when tools are called', () async {
-        final notifications = StreamQueue(server.rootsListChanged!);
-        await addRoots([rootA]);
-        expect(await notifications.hasNext, true);
-        await notifications.next;
-
-        await removeRoots([rootA]);
-        expect(await notifications.hasNext, true);
-        await notifications.next;
-      });
-
-      test('can add, remove, and list roots', () async {
-        expect((await server.listRoots(ListRootsRequest())).roots, isEmpty);
-
-        await addRoots([rootA, rootB]);
-        expect(
-          (await server.listRoots(ListRootsRequest())).roots,
-          unorderedEquals([rootA, rootB]),
-        );
-
-        await removeRoots([rootB]);
-        expect(
-          (await server.listRoots(ListRootsRequest())).roots,
-          unorderedEquals([rootA]),
-        );
-      });
+    test('registers roots tool', () async {
+      final tools = await harness.mcpServerConnection.listTools(
+        ListToolsRequest(),
+      );
+      expect(tools.tools.map((t) => t.name), contains(ToolNames.roots.name));
     });
 
-    group('when the client does support roots', () {
-      late TestClientWithRoots client;
+    test('can add and remove roots', () async {
+      final server = harness.serverConnectionPair.server!;
 
-      setUp(() async {
-        client = TestClientWithRoots();
-        addTearDown(client.shutdown);
-        serverConnection = client.connectServer(
-          StreamChannel.withCloseGuarantee(
-            clientController.stream,
-            serverController.sink,
-          ),
-        );
-        await serverConnection.initialize(
-          InitializeRequest(
-            protocolVersion: ProtocolVersion.latestSupported,
-            capabilities: client.capabilities,
-            clientInfo: client.implementation,
-          ),
-        );
-        serverConnection.notifyInitialized();
-      });
+      expect(await server.roots, isEmpty);
 
-      test('supportsRoots is true', () async {
-        expect(server.supportsRoots, isTrue);
-        await server.roots; // wait for the first listRoots request to complete
-      });
+      await addRoots([rootA.uri, rootB.uri]);
+      expect(await server.roots, unorderedEquals([rootA, rootB]));
 
-      test('registers no tools', () async {
-        final tools = await serverConnection.listTools(ListToolsRequest());
-        expect(tools.tools, isEmpty);
-      });
+      await removeRoots([rootB.uri]);
+      expect(await server.roots, unorderedEquals([rootA]));
+    });
 
-      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('can combine client and custom roots', () async {
+      final server = harness.serverConnectionPair.server!;
+      final notifications = StreamQueue(server.rootsListChanged);
+      addTearDown(notifications.cancel);
+      final clientRoot = Root(uri: 'file:///client-root/');
+      final next = notifications.next;
+      harness.mcpClient.addRoot(clientRoot);
+      await next;
+      expect(
+        (await server.roots).map((r) => r.uri),
+        unorderedEquals([clientRoot.uri]),
+      );
 
-        client.removeRoot(rootA);
-        expect(await notifications.hasNext, true);
-        await notifications.next;
-      });
+      await addRoots([rootA.uri]);
 
-      test('can add, remove, and list roots', () async {
-        expect((await server.listRoots(ListRootsRequest())).roots, isEmpty);
+      expect(
+        (await server.roots).map((r) => r.uri),
+        unorderedEquals([clientRoot.uri, rootA.uri]),
+      );
+    });
 
-        client
-          ..addRoot(rootA)
-          ..addRoot(rootB);
-        expect(
-          (await server.listRoots(ListRootsRequest())).roots,
-          unorderedEquals([rootA, rootB]),
-        );
+    test('Gives roots changed notifications when tools are called', () async {
+      final server = harness.serverConnectionPair.server!;
+      final notifications = StreamQueue(server.rootsListChanged);
+      addTearDown(notifications.cancel);
 
-        client.removeRoot(rootB);
-        expect(
-          (await server.listRoots(ListRootsRequest())).roots,
-          unorderedEquals([rootA]),
-        );
-      });
+      var next = notifications.next;
+      await addRoots([rootA.uri]);
+      await next;
+
+      next = notifications.next;
+      await removeRoots([rootA.uri]);
+      await next;
     });
   });
 }
-
-// A test client that does not support roots
-final class TestClientWithoutRoots extends MCPClient {
-  TestClientWithoutRoots()
-    : super(
-        Implementation(
-          name: 'test client with no roots support',
-          version: '0.1.0',
-        ),
-      );
-}
-
-/// A test client that supports roots
-final class TestClientWithRoots extends MCPClient with RootsSupport {
-  TestClientWithRoots()
-    : super(
-        Implementation(
-          name: 'test client with roots support',
-          version: '0.1.0',
-        ),
-      );
-}
-
-/// A test server that mixes in RootsFallbackSupport
-final class TestServer extends MCPServer
-    with
-        LoggingSupport,
-        ToolsSupport,
-        RootsTrackingSupport,
-        RootsFallbackSupport {
-  @override
-  final bool forceRootsFallback;
-
-  TestServer(
-    super.channel, {
-    super.protocolLogSink,
-    this.forceRootsFallback = false,
-  }) : super.fromStreamChannel(
-         implementation: Implementation(name: 'test server', version: '0.1.0'),
-         instructions: 'A test server with roots fallback support',
-       );
-}