Repository: dart-lang/ai
Pull request #43: Add basic analysis support
Author: login: jakemac53
Labels: name: type-infra
description: A repository infrastructure change or enhancement
name: package:dart_mcp
name: package:dart_mcp_server
name: skip-changelog-check
Created: 2025-04-08T23:49:30Z | Merged: 2025-04-09T21:51:49Z

Requires clients to implement "Roots", and creates one analysis context per root.

Then, requests to analyze groups of files can be issued via the "analyze files" command, which requires a root for each group of files.

We also run a file watcher, which is a bit right now because we could be a second or so behind if using a polling watcher. We might want to try and do some sort of "file overlay" situation for other scenarios, passing the file contents to analyze directly.

This is currently based on the input schema branch - I will wait to merge until that is merged and then change the target branch here.


Diff:
diff --git a/.github/workflows/dart_tooling_mcp_server.yaml b/.github/workflows/dart_tooling_mcp_server.yaml
index dc70cc04..b6cde99b 100644
--- a/.github/workflows/dart_tooling_mcp_server.yaml
+++ b/.github/workflows/dart_tooling_mcp_server.yaml
@@ -35,6 +35,11 @@ jobs:
       - uses: subosito/flutter-action@v1
         with:
           channel: ${{ matrix.flutterSdk }}
+          cache: true
+          cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
+      # Exposes the DART_SDK environment variable to all other actions.
+      # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-environment-variable
+      - run: echo "DART_SDK=$(which dart | xargs dirname | xargs dirname)" >> "$GITHUB_ENV"
 
       # Required to build for linux
       - run: |
diff --git a/pkgs/dart_mcp/lib/src/client/client.dart b/pkgs/dart_mcp/lib/src/client/client.dart
index 147e7858..9ace5bd2 100644
--- a/pkgs/dart_mcp/lib/src/client/client.dart
+++ b/pkgs/dart_mcp/lib/src/client/client.dart
@@ -57,6 +57,12 @@ base class MCPClient {
     List<String> arguments,
   ) async {
     var process = await Process.start(command, arguments);
+    process.stderr
+        .transform(utf8.decoder)
+        .transform(const LineSplitter())
+        .listen((line) {
+      stderr.writeln('[StdErr from server $command]: $line');
+    });
     var channel = StreamChannel.withCloseGuarantee(
       process.stdout,
       process.stdin,
diff --git a/pkgs/dart_mcp/lib/src/server/server.dart b/pkgs/dart_mcp/lib/src/server/server.dart
index 09188b5e..80dc455e 100644
--- a/pkgs/dart_mcp/lib/src/server/server.dart
+++ b/pkgs/dart_mcp/lib/src/server/server.dart
@@ -24,7 +24,7 @@ part 'tools_support.dart';
 abstract base class MCPServer extends MCPBase {
   /// Completes when this server has finished initialization and gotten the
   /// final ack from the client.
-  FutureOr<void> get initialized => _initialized.future;
+  Future<void> get initialized => _initialized.future;
   final Completer<void> _initialized = Completer<void>();
 
   /// Whether this server is still active and has completed initialization.
diff --git a/pkgs/dart_tooling_mcp_server/bin/main.dart b/pkgs/dart_tooling_mcp_server/bin/main.dart
index 66c38602..75a67acc 100644
--- a/pkgs/dart_tooling_mcp_server/bin/main.dart
+++ b/pkgs/dart_tooling_mcp_server/bin/main.dart
@@ -2,10 +2,12 @@
 // 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 'dart:convert';
 import 'dart:io' as io;
 
 import 'package:async/async.dart';
+import 'package:dart_mcp/server.dart';
 import 'package:dart_tooling_mcp_server/dart_tooling_mcp_server.dart';
 import 'package:stream_channel/stream_channel.dart';
 
@@ -18,16 +20,39 @@ void main(List<String> args) async {
     io.exit(1);
   }
 
-  await DartToolingMCPServer.connect(
-    StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
-        .transform(StreamChannelTransformer.fromCodec(utf8))
-        .transformStream(const LineSplitter())
-        .transformSink(
-      StreamSinkTransformer.fromHandlers(
-        handleData: (data, sink) {
-          sink.add('$data\n');
-        },
+  DartToolingMCPServer? server;
+  await runZonedGuarded(() async {
+    server = await DartToolingMCPServer.connect(
+      StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
+          .transform(StreamChannelTransformer.fromCodec(utf8))
+          .transformStream(const LineSplitter())
+          .transformSink(
+        StreamSinkTransformer.fromHandlers(
+          handleData: (data, sink) {
+            sink.add('$data\n');
+          },
+        ),
       ),
-    ),
-  );
+    );
+  }, (e, s) {
+    if (server != null) {
+      try {
+        // Log unhandled errors to the client, if we managed to connect.
+        server!.log(LoggingLevel.error, '$e\n$s');
+      } catch (_) {}
+    } else {
+      // Otherwise log to stderr.
+      io.stderr
+        ..writeln(e)
+        ..writeln(s);
+    }
+  }, zoneSpecification: ZoneSpecification(print: (_, __, ___, value) {
+    if (server != null) {
+      try {
+        // Don't allow `print` since this breaks stdio communication, but if we
+        // have a server we do log messages to the client.
+        server!.log(LoggingLevel.info, value);
+      } catch (_) {}
+    }
+  }));
 }
diff --git a/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart b/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
new file mode 100644
index 00000000..8f86300a
--- /dev/null
+++ b/pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
@@ -0,0 +1,184 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    _disposeWatchSubscriptions();
+  }
+
+  /// Cancels all [_watchSubscriptions] and clears the list.
+  void _disposeWatchSubscriptions() async {
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    _disposeWatchSubscriptions();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        _analysisContexts
+            ?.contextFor(event.path)
+            .changeFile(p.normalize(event.path));
+      }));
+    }
+
+    _analysisContexts =
+        AnalysisContextCollection(includedPaths: paths, sdkPath: sdkPath);
+  }
+
+  /// Implementation of the [analyzeFilesTool], analyzes the requested files
+  /// under the requested project roots.
+  Future<CallToolResult> _analyzeFiles(CallToolRequest request) async {
+    var contexts = _analysisContexts;
+    if (contexts == null) {
+      return CallToolResult(content: [
+        TextContent(
+            text: 'Analysis not yet ready, please wait a few seconds and try '
+                'again.')
+      ], isError: true);
+    }
+
+    var messages = <TextContent>[];
+    final rootConfigs =
+        (request.arguments!['roots'] as List).cast<Map<String, Object?>>();
+    for (var rootConfig in rootConfigs) {
+      var rootUri = Uri.parse(rootConfig['root'] as String);
+      if (rootUri.scheme != 'file') {
+        return CallToolResult(content: [
+          TextContent(
+              text: 'Only file scheme uris are allowed for roots, but got '
+                  '$rootUri')
+        ], isError: true);
+      }
+      var paths = (rootConfig['paths'] as List?)?.cast<String>();
+      if (paths == null) {
+        return CallToolResult(content: [
+          TextContent(
+              text:
+                  'Missing required argument `paths`, which should be the list '
+                  'of relative paths to analyze.')
+        ], isError: true);
+      }
+
+      var context = contexts.contextFor(p.normalize(rootUri.path));
+      await context.applyPendingFileChanges();
+
+      for (var path in paths) {
+        var normalized =
+            p.normalize(p.isAbsolute(path) ? path : p.join(rootUri.path, path));
+        var errorsResult = await context.currentSession.getErrors(normalized);
+        if (errorsResult is! ErrorsResult) {
+          return CallToolResult(content: [
+            TextContent(text: 'Error computing analyzer errors $errorsResult'),
+          ]);
+        }
+        for (var error in errorsResult.errors) {
+          messages.add(TextContent(text: 'Error: ${error.message}'));
+          if (error.correctionMessage case var correctionMessage?) {
+            messages.add(TextContent(text: correctionMessage));
+          }
+        }
+      }
+    }
+
+    return CallToolResult(content: messages);
+  }
+
+  @visibleForTesting
+  static final analyzeFilesTool = Tool(
+      name: 'analyze files',
+      description:
+          'Analyzes the requested file paths under the specified project roots '
+          'and returns the results as a list of messages.',
+      inputSchema: ObjectSchema(properties: {
+        'roots': ListSchema(
+            title: 'All projects roots to analyze',
+            description:
+                'These must match a root returned by a call to "listRoots".',
+            items: ObjectSchema(
+              properties: {
+                'root': StringSchema(
+                    title: 'The URI of the project root to analyze.'),
+                'paths': ListSchema(
+                    title: 'Relative or absolute paths to analyze under the '
+                        '"root", must correspond to files and not directories.',
+                    items: StringSchema()),
+              },
+              required: ['root'],
+            )),
+      }, required: [
+        'roots'
+      ]));
+}
diff --git a/pkgs/dart_tooling_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_tooling_mcp_server/lib/src/mixins/dtd.dart
index 75b53c36..512fe451 100644
--- a/pkgs/dart_tooling_mcp_server/lib/src/mixins/dtd.dart
+++ b/pkgs/dart_tooling_mcp_server/lib/src/mixins/dtd.dart
@@ -7,6 +7,7 @@ import 'dart:async';
 import 'package:dart_mcp/server.dart';
 import 'package:dtd/dtd.dart';
 import 'package:json_rpc_2/json_rpc_2.dart';
+import 'package:meta/meta.dart';
 import 'package:vm_service/vm_service_io.dart';
 
 /// Mix this in to any MCPServer to add support for connecting to the Dart
@@ -29,8 +30,8 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
 
   @override
   FutureOr<InitializeResult> initialize(InitializeRequest request) async {
-    registerTool(_connectTool, _connect);
-    registerTool(_screenshotTool, takeScreenshot);
+    registerTool(connectTool, _connect);
+    registerTool(screenshotTool, takeScreenshot);
     return super.initialize(request);
   }
 
@@ -146,7 +147,8 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
     }
   }
 
-  static final _connectTool = Tool(
+  @visibleForTesting
+  static final connectTool = Tool(
     inputSchema: ObjectSchema(
       properties: {
         'uri': StringSchema(),
@@ -160,10 +162,11 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
         'command. Do not just make up a random URI to pass.',
   );
 
-  static final _screenshotTool = Tool(
+  @visibleForTesting
+  static final screenshotTool = Tool(
     name: 'take_screenshot',
     description: 'Takes a screenshot of the active flutter application in its '
-        'current state. Requires "${_connectTool.name}" to be successfully '
+        'current state. Requires "${connectTool.name}" to be successfully '
         'called first.',
     inputSchema: ObjectSchema(),
   );
@@ -173,7 +176,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
     content: [
       TextContent(
         text: 'The dart tooling daemon is not connected, you need to call '
-            '"${_connectTool.name}" first.',
+            '"${connectTool.name}" first.',
       ),
     ],
   );
@@ -183,7 +186,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
     content: [
       TextContent(
         text: 'The dart tooling daemon is already connected, you cannot call '
-            '"${_connectTool.name}" again.',
+            '"${connectTool.name}" again.',
       ),
     ],
   );
diff --git a/pkgs/dart_tooling_mcp_server/lib/src/server.dart b/pkgs/dart_tooling_mcp_server/lib/src/server.dart
index 3ce553c3..a43467dd 100644
--- a/pkgs/dart_tooling_mcp_server/lib/src/server.dart
+++ b/pkgs/dart_tooling_mcp_server/lib/src/server.dart
@@ -7,11 +7,16 @@ import 'dart:async';
 import 'package:dart_mcp/server.dart';
 import 'package:stream_channel/stream_channel.dart';
 
+import 'mixins/analyzer.dart';
 import 'mixins/dtd.dart';
 
 /// An MCP server for Dart and Flutter tooling.
 final class DartToolingMCPServer extends MCPServer
-    with ToolsSupport, DartToolingDaemonSupport {
+    with
+        LoggingSupport,
+        ToolsSupport,
+        DartAnalyzerSupport,
+        DartToolingDaemonSupport {
   DartToolingMCPServer({required super.channel})
       : super.fromStreamChannel(
           implementation: ServerImplementation(
diff --git a/pkgs/dart_tooling_mcp_server/pubspec.yaml b/pkgs/dart_tooling_mcp_server/pubspec.yaml
index e59889ea..5c089acf 100644
--- a/pkgs/dart_tooling_mcp_server/pubspec.yaml
+++ b/pkgs/dart_tooling_mcp_server/pubspec.yaml
@@ -5,16 +5,20 @@ version: 0.1.0-wip
 environment:
   sdk: ^3.6.1 # The version of dart in the current flutter stable
 dependencies:
+  analyzer: ^7.3.0
   async: ^2.13.0
   dart_mcp:
     path: ../dart_mcp
   devtools_shared: ^11.2.0
   dtd: ^2.4.0
   json_rpc_2: ^3.0.3
+  meta: ^1.16.0
+  path: ^1.9.1
   stream_channel: ^2.1.4
   test_descriptor: ^2.0.2
   test_process: ^2.1.1
   vm_service: ^15.0.0
+  watcher: ^1.1.1
 executables:
   dart_tooling_mcp_server: main
 dev_dependencies:
diff --git a/pkgs/dart_tooling_mcp_server/test/dart_tooling_mcp_server_test.dart b/pkgs/dart_tooling_mcp_server/test/dart_tooling_mcp_server_test.dart
index a4a2ec7f..1846e39c 100644
--- a/pkgs/dart_tooling_mcp_server/test/dart_tooling_mcp_server_test.dart
+++ b/pkgs/dart_tooling_mcp_server/test/dart_tooling_mcp_server_test.dart
@@ -3,7 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:dart_mcp/server.dart';
+import 'package:dart_tooling_mcp_server/src/mixins/analyzer.dart';
+import 'package:dart_tooling_mcp_server/src/mixins/dtd.dart';
 import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
 
 import 'test_harness.dart';
 
@@ -20,7 +23,7 @@ void main() {
   test('can take a screenshot', () async {
     final tools = (await testHarness.mcpServerConnection.listTools()).tools;
     final screenshotTool = tools.singleWhere(
-      (t) => t.name == 'take_screenshot',
+      (t) => t.name == DartToolingDaemonSupport.screenshotTool.name,
     );
     final screenshotResult = await testHarness.callToolWithRetry(
       CallToolRequest(name: screenshotTool.name),
@@ -34,4 +37,73 @@ void main() {
       },
     );
   });
+
+  group('analysis', () {
+    late Tool analyzeTool;
+
+    setUp(() async {
+      final tools = (await testHarness.mcpServerConnection.listTools()).tools;
+      analyzeTool = tools.singleWhere(
+          (t) => t.name == DartAnalyzerSupport.analyzeFilesTool.name);
+    });
+
+    test('can analyze a project', () async {
+      final request = CallToolRequest(
+        name: analyzeTool.name,
+        arguments: {
+          'roots': [
+            {
+              'root': Uri.base.resolve(counterAppPath).toString(),
+              'paths': ['lib/main.dart']
+            }
+          ]
+        },
+      );
+      final result = await testHarness.callToolWithRetry(request);
+      expect(result.isError, isNot(true));
+      expect(result.content, isEmpty);
+    });
+
+    test('can handle project changes', () async {
+      final example =
+          d.dir('example', [d.file('main.dart', 'void main() => 1 + "2";')]);
+      await example.create();
+      final exampleRoot = Root(uri: example.io.absolute.uri.toString());
+      testHarness.mcpClient.addRoot(exampleRoot);
+
+      // Allow the notification to propagate, and the server to ask for the new
+      // list of roots.
+      await pumpEventQueue();
+
+      final request = CallToolRequest(
+        name: analyzeTool.name,
+        arguments: {
+          'roots': [
+            {
+              'root': exampleRoot.uri,
+              'paths': ['main.dart']
+            }
+          ]
+        },
+      );
+      var result = await testHarness.callToolWithRetry(request);
+      expect(result.isError, isNot(true));
+      expect(result.content, [
+        TextContent(
+            text: "Error: The argument type 'String' can't be assigned to the "
+                "parameter type 'num'. "),
+      ]);
+
+      // Change the file to fix the error
+      await d.dir(
+          'example', [d.file('main.dart', 'void main() => 1 + 2;')]).create();
+      // Wait for the file watcher to pick up the change, the default delay for
+      // a polling watcher is one second.
+      await Future<void>.delayed(const Duration(seconds: 1));
+
+      result = await testHarness.callToolWithRetry(request);
+      expect(result.isError, isNot(true));
+      expect(result.content, isEmpty);
+    });
+  });
 }
diff --git a/pkgs/dart_tooling_mcp_server/test/test_harness.dart b/pkgs/dart_tooling_mcp_server/test/test_harness.dart
index 35524271..b7b057f8 100644
--- a/pkgs/dart_tooling_mcp_server/test/test_harness.dart
+++ b/pkgs/dart_tooling_mcp_server/test/test_harness.dart
@@ -73,6 +73,9 @@ class TestHarness {
     final mcpClient = DartToolingMCPClient();
     addTearDown(mcpClient.shutdown);
     final connection = await _initializeMCPServer(mcpClient, debugMode);
+    connection.onLog.listen((log) {
+      printOnFailure('MCP Server Log: $log');
+    });
 
     final fakeEditorExtension = await FakeEditorExtension.connect(
       flutterProcess,
@@ -89,12 +92,12 @@ class TestHarness {
     final tools = (await mcpServerConnection.listTools()).tools;
 
     final connectTool = tools.singleWhere(
-      (t) => t.name == 'connectDartToolingDaemon',
-    );
+        (t) => t.name == DartToolingDaemonSupport.connectTool.name);
 
     final result = await callToolWithRetry(
       CallToolRequest(name: connectTool.name, arguments: {'uri': dtdUri}),
     );
+
     expect(result.isError, isNot(true), reason: result.content.join('\n'));
   }
 
@@ -119,14 +122,18 @@ class TestHarness {
   }
 }
 
-final class DartToolingMCPClient extends MCPClient {
+final class DartToolingMCPClient extends MCPClient with RootsSupport {
   DartToolingMCPClient()
       : super(
           ClientImplementation(
             name: 'test client for the dart tooling mcp server',
             version: '0.1.0',
           ),
-        );
+        ) {
+    addRoot(Root(
+        uri: Directory(counterAppPath).absolute.uri.toString(),
+        name: 'counter app test fixture'));
+  }
 }
 
 /// The dart tooling daemon currently expects to get vm service uris through


Reviews:
user: login: kenzieschmoll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:08:39Z
user: login: kenzieschmoll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:10:25Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:15:32Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:16:47Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:17:58Z
user: login: kenzieschmoll
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:29:49Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:35:33Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:37:10Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:39:01Z
user: login: kenzieschmoll
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:48:53Z
user: login: sigurdm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:39:08Z
user: login: sigurdm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:42:49Z
user: login: jakemac53
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:21:15Z
user: login: sigurdm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:03:04Z
user: login: sigurdm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:10:25Z
user: login: devoncarew
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T17:27:03Z

Review comments:
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: kenzieschmoll
body: should super.shutdown get called last like we do for the dispose lifecycle method in flutter? Or should this be the first line in the method?
created_at: 2025-04-09T21:08:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: kenzieschmoll
body: can we use Platform.resolvedExecutable to find this?
created_at: 2025-04-09T21:10:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: kenzieschmoll
body: this is duplicated above, please create a helper
created_at: 2025-04-09T21:11:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        final context = _analysisContexts?.contextFor(event.path);
+        if (context == null) return;
+        context.changeFile(p.normalize(event.path));
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: kenzieschmoll
body: nit: this can be reduced:
```
_analysisContexts?.contextFor(event.path)?.changeFile(p.normalize(event.path))
```
created_at: 2025-04-09T21:12:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: it doesn't matter, it just needs to get called (basically this just gives each class in the hierarchy a chance to clean things up but there is no ordering implied)
created_at: 2025-04-09T21:15:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: We can't because that doesn't work when AOT compiled (you just get the AOT script).
created_at: 2025-04-09T21:16:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: (and we have to AOT compile because pub breaks JSON servers communicating over stdin/stdout with compiling messages) cc @sigurdm @jonasfj this has been discussed in the past right?
created_at: 2025-04-09T21:17:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        final context = _analysisContexts?.contextFor(event.path);
+        if (context == null) return;
+        context.changeFile(p.normalize(event.path));
+      }));
+    }
+
+    _analysisContexts =
+        AnalysisContextCollection(includedPaths: paths, sdkPath: sdkPath);
+  }
+
+  /// Implementation of the [analyzeFilesTool], analyzes the requested files
+  /// under the requested project roots.
+  Future<CallToolResult> _analyzeFiles(CallToolRequest request) async {
+    var contexts = _analysisContexts;
+    if (contexts == null) {
+      return CallToolResult(content: [
+        TextContent(
+            text: 'Analysis not yet ready, please wait a few seconds and try '
+                'again.')
+      ], isError: true);
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: kenzieschmoll
body: Can we use and enforce the new Dart formatter on this codebase?
created_at: 2025-04-09T21:18:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        final context = _analysisContexts?.contextFor(event.path);
+        if (context == null) return;
+        context.changeFile(p.normalize(event.path));
+      }));
+    }
+
+    _analysisContexts =
+        AnalysisContextCollection(includedPaths: paths, sdkPath: sdkPath);
+  }
+
+  /// Implementation of the [analyzeFilesTool], analyzes the requested files
+  /// under the requested project roots.
+  Future<CallToolResult> _analyzeFiles(CallToolRequest request) async {
+    var contexts = _analysisContexts;
+    if (contexts == null) {
+      return CallToolResult(content: [
+        TextContent(
+            text: 'Analysis not yet ready, please wait a few seconds and try '
+                'again.')
+      ], isError: true);
+    }
+
+    var messages = <TextContent>[];
+    final rootConfigs =
+        (request.arguments!['roots'] as List).cast<Map<String, Object?>>();
+    for (var rootConfig in rootConfigs) {
+      var rootUri = Uri.parse(rootConfig['root'] as String);
+      if (rootUri.scheme != 'file') {
+        return CallToolResult(content: [
+          TextContent(
+              text: 'Only file scheme uris are allowed for roots, but got '
+                  '$rootUri')
+        ], isError: true);
+      }
+      var paths = (rootConfig['paths'] as List?)?.cast<String>();
+      if (paths == null) {
+        return CallToolResult(content: [
+          TextContent(
+              text:
+                  'Missing required argument `paths`, which should be the list '
+                  'of relative paths to analyze.')
+        ], isError: true);
+      }
+
+      var context = contexts.contextFor(p.normalize(rootUri.path));
+      await context.applyPendingFileChanges();
+
+      for (var path in paths) {
+        var normalized =
+            p.normalize(p.isAbsolute(path) ? path : p.join(rootUri.path, path));
+        var errorsResult = await context.currentSession.getErrors(normalized);
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: kenzieschmoll
body: nit: we can consider enabling the prefer finals lint
created_at: 2025-04-09T21:23:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,4 +37,73 @@ void main() {
       },
     );
   });
+
+  group('analysis', () {
+    late Tool analyzeTool;
+
+    setUp(() async {
+      final tools = (await testHarness.mcpServerConnection.listTools()).tools;
+      analyzeTool = tools.singleWhere(
+          (t) => t.name == DartAnalyzerSupport.analyzeFilesTool.name);
+    });
+
+    test('can analyze a project', () async {
+      final request = CallToolRequest(
+        name: analyzeTool.name,
+        arguments: {
+          'roots': [
+            {
+              'root': Uri.base.resolve(counterAppPath).toString(),
+              'paths': ['lib/main.dart']
+            }
+          ]
+        },
+      );
+      final result = await testHarness.callToolWithRetry(request);
+      expect(result.isError, isNot(true));
+      expect(result.content, isEmpty);
+    });
+
+    test('can handle project changes', () async {
+      final example =
+          d.dir('example', [d.file('main.dart', 'void main() => 1 + "2";')]);
+      await example.create();
+      final exampeRoot = Root(uri: example.io.absolute.uri.toString());
path: pkgs/dart_tooling_mcp_server/test/dart_tooling_mcp_server_test.dart
user: login: kenzieschmoll
body: typo "exampleRoot"
created_at: 2025-04-09T21:28:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        final context = _analysisContexts?.contextFor(event.path);
+        if (context == null) return;
+        context.changeFile(p.normalize(event.path));
+      }));
+    }
+
+    _analysisContexts =
+        AnalysisContextCollection(includedPaths: paths, sdkPath: sdkPath);
+  }
+
+  /// Implementation of the [analyzeFilesTool], analyzes the requested files
+  /// under the requested project roots.
+  Future<CallToolResult> _analyzeFiles(CallToolRequest request) async {
+    var contexts = _analysisContexts;
+    if (contexts == null) {
+      return CallToolResult(content: [
+        TextContent(
+            text: 'Analysis not yet ready, please wait a few seconds and try '
+                'again.')
+      ], isError: true);
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: Previously the flutter stable we were getting didn't support it but it looks like it should now so I can try upping our min SDK again to do that. Will do it as a separate CL to reduce churn here.
created_at: 2025-04-09T21:35:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        final context = _analysisContexts?.contextFor(event.path);
+        if (context == null) return;
+        context.changeFile(p.normalize(event.path));
+      }));
+    }
+
+    _analysisContexts =
+        AnalysisContextCollection(includedPaths: paths, sdkPath: sdkPath);
+  }
+
+  /// Implementation of the [analyzeFilesTool], analyzes the requested files
+  /// under the requested project roots.
+  Future<CallToolResult> _analyzeFiles(CallToolRequest request) async {
+    var contexts = _analysisContexts;
+    if (contexts == null) {
+      return CallToolResult(content: [
+        TextContent(
+            text: 'Analysis not yet ready, please wait a few seconds and try '
+                'again.')
+      ], isError: true);
+    }
+
+    var messages = <TextContent>[];
+    final rootConfigs =
+        (request.arguments!['roots'] as List).cast<Map<String, Object?>>();
+    for (var rootConfig in rootConfigs) {
+      var rootUri = Uri.parse(rootConfig['root'] as String);
+      if (rootUri.scheme != 'file') {
+        return CallToolResult(content: [
+          TextContent(
+              text: 'Only file scheme uris are allowed for roots, but got '
+                  '$rootUri')
+        ], isError: true);
+      }
+      var paths = (rootConfig['paths'] as List?)?.cast<String>();
+      if (paths == null) {
+        return CallToolResult(content: [
+          TextContent(
+              text:
+                  'Missing required argument `paths`, which should be the list '
+                  'of relative paths to analyze.')
+        ], isError: true);
+      }
+
+      var context = contexts.contextFor(p.normalize(rootUri.path));
+      await context.applyPendingFileChanges();
+
+      for (var path in paths) {
+        var normalized =
+            p.normalize(p.isAbsolute(path) ? path : p.join(rootUri.path, path));
+        var errorsResult = await context.currentSession.getErrors(normalized);
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: filed https://github.com/dart-lang/ai/issues/46
created_at: 2025-04-09T21:37:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
+    if (sdkPath == null) {
+      throw StateError('DART_SDK environment variable not set');
+    }
+
+    final paths = <String>[];
+    for (var root in result.roots) {
+      var uri = Uri.parse(root.uri);
+      if (uri.scheme != 'file') {
+        throw ArgumentError.value(
+            root.uri, 'uri', 'Only file scheme uris are allowed for roots');
+      }
+      paths.add(p.normalize(uri.path));
+    }
+
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+
+    for (var rootPath in paths) {
+      var watcher = DirectoryWatcher(rootPath);
+      _watchSubscriptions.add(watcher.events.listen((event) {
+        final context = _analysisContexts?.contextFor(event.path);
+        if (context == null) return;
+        context.changeFile(p.normalize(event.path));
+      }));
+    }
+
+    _analysisContexts =
+        AnalysisContextCollection(includedPaths: paths, sdkPath: sdkPath);
+  }
+
+  /// Implementation of the [analyzeFilesTool], analyzes the requested files
+  /// under the requested project roots.
+  Future<CallToolResult> _analyzeFiles(CallToolRequest request) async {
+    var contexts = _analysisContexts;
+    if (contexts == null) {
+      return CallToolResult(content: [
+        TextContent(
+            text: 'Analysis not yet ready, please wait a few seconds and try '
+                'again.')
+      ], isError: true);
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: (filed https://github.com/dart-lang/ai/issues/46)
created_at: 2025-04-09T21:39:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: sigurdm
body: It should not print any compilation messages if no terminal is attached.
If that is not the case, please file an issue.
created_at: 2025-04-14T13:39:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: sigurdm
body: I don't know how this is gonna be deployed.
You could also consider compiling to a jit-snapshot via. `dart --snapshot=mcp.snapshot` and running with `dart mcp.snapshot`. No pub involved, and still knows the dart executable...
created_at: 2025-04-14T13:42:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: jakemac53
body: I think we want to deploy it as a single executable anyways, that makes the most sense, it is mostly annoying for development.

I definitely was running into this but IIRC it was using the MCP inspector and I don't know exactly how it was spawning things? For instance if you were to pass `runInShell: true` when spawning from a dart program, would it still print to stdout?
created_at: 2025-04-14T14:21:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: sigurdm
body: `runInShell: true` should not make it print to stdout.
You need to attach a pty to the process, something like https://github.com/TerminalStudio/pty for it to print.

(there might be bugs, but they should be fixable). The logic is here: https://github.com/dart-lang/pub/blob/639536aa78f7382b4a948669bfdc283f830b2ae9/lib/src/io.dart#L742

created_at: 2025-04-14T15:03:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: sigurdm
body: There is an env-var for overriding whatever behavior you are otherwise getting...
created_at: 2025-04-14T15:10:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,182 @@
+// 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 'dart:io';
+
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+/// Mix this in to any MCPServer to add support for analyzing Dart projects.
+///
+/// The MCPServer must already have the [ToolsSupport] mixin applied.
+base mixin DartAnalyzerSupport on ToolsSupport, LoggingSupport {
+  /// The analyzed contexts.
+  AnalysisContextCollection? _analysisContexts;
+
+  /// All active directory watcher streams.
+  ///
+  /// The watcher package doesn't let you close watchers, you just have to
+  /// stop listening to their streams instead, so we store the stream
+  /// subscriptions instead of the watchers themselves.
+  final List<StreamSubscription> _watchSubscriptions = [];
+
+  @override
+  FutureOr<InitializeResult> initialize(InitializeRequest request) {
+    if (request.capabilities.roots == null) {
+      throw StateError(
+          'This server requires the "roots" capability to be implemented.');
+    }
+    registerTool(analyzeFilesTool, _analyzeFiles);
+    initialized.then(_listenForRoots);
+    return super.initialize(request);
+  }
+
+  @override
+  Future<void> shutdown() async {
+    await super.shutdown();
+    await _analysisContexts?.dispose();
+    for (var subscription in _watchSubscriptions) {
+      unawaited(subscription.cancel());
+    }
+    _watchSubscriptions.clear();
+  }
+
+  /// Lists the roots, and listens for changes to them.
+  ///
+  /// Whenever new roots are found, creates a new [AnalysisContextCollection].
+  void _listenForRoots([void _]) async {
+    rootsListChanged!.listen((event) async {
+      unawaited(_analysisContexts?.dispose());
+      _analysisContexts = null;
+      _createAnalysisContext(await listRoots(ListRootsRequest()));
+    });
+    _createAnalysisContext(await listRoots(ListRootsRequest()));
+  }
+
+  /// Creates an analysis context from a list of roots.
+  //
+  // TODO: Better configuration for the DART_SDK location.
+  void _createAnalysisContext(ListRootsResult result) async {
+    final sdkPath = Platform.environment['DART_SDK'];
path: pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
user: login: devoncarew
body: > (and we have to AOT compile because pub breaks JSON servers communicating over stdin/stdout with compiling messages) cc @sigurdm @jonasfj this has been discussed in the past right?

As an aside, whenever a tool has intermittent messages to stdio that's not normally part of the expected output, we should prefer printing that to `stderr`; that allows for better parsing of stdout.

created_at: 2025-04-14T17:27:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: bwilkerson
created_at: 2025-04-09T14:34:27Z
body: Is there a reason why this is using the analyzer package directly rather than going through the analysis server?

The server already has the ability to watch files (so some of this code is duplication of existing code), and by not going through the server we prevent analyzer plugins from contributing to the results. The latter seems like an especially unfortunate outcome.
author_association: MEMBER
user: login: jakemac53
created_at: 2025-04-09T16:02:45Z
body: So, I do think we might want to explore that in the future - especially if we can re-use the analysis server  that is already started by the IDE - but the main reason is that the language server protocol from what I could tell doesn't appear to be well suited to the task.

It appeared to me (happy to be corrected!) that the protocol is extremely tied to the IDE experience - errors etc are pushed to the IDE instead of pulled from the IDE. In this case we really want to just ask the analyzer for errors and get a result, not have them pushed to us asynchronously.
author_association: CONTRIBUTOR
user: login: bwilkerson
created_at: 2025-04-09T16:21:20Z
body: That's more true of LSP than it is of the legacy protocol.

We have the same need in the command-line analyzer. We solved that problem by having a notification from the server to indicate when analysis was complete and all of the diagnostics that are going to be generated have already been sent. That allows the command-line analyzer to know when to exit, and would allow this class to know when to return results to the mcp client.

If we want to use LSP then we'd probably need to add a custom protocol, either to ask for all of the diagnostics in a single operation or to notify clients when all the diagnostics have been computed. (We'll need to do that if we ever move the command-line analyzer to use LSP as well.)
author_association: MEMBER
user: login: jakemac53
created_at: 2025-04-09T17:18:14Z
body: As discussed in the meeting today, we will run with the current approach for now, and once we have a better idea of the real functionality that we end up implementing here we will re-asses whether interacting over LSP is a better approach.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-09T17:52:27Z
body: ## PR Health 



author_association: NONE