From 9d1d9425094c0263c86c041c2ac60929dfcde73a Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 21 Oct 2025 09:43:33 -0700 Subject: [PATCH 1/5] Parse output from flutter run with --machine --- .../lib/src/mixins/flutter_launcher.dart | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart index 01c9651c..7fd5f033 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -97,6 +97,7 @@ base mixin FlutterLauncherSupport sdk.flutterExecutablePath, 'run', '--print-dtd', + '--machine', '--device-id', device, if (target != null) '--target', @@ -121,10 +122,34 @@ base mixin FlutterLauncherSupport ); void checkForDtdUri(String line) { + line = line.trim(); + // Check for --machine output first. + if (line.startsWith('[') && line.endsWith(']')) { + // Looking for: + // [{"event":"app.dtd","params":{"appId":"cd6c66eb-35e9-4ac1-96df-727540138346","uri":"ws://127.0.0.1:59548/3OpAaPw9i34="}}] + try { + final json = + jsonDecode(line.substring(1, line.length - 1)) + as Map; + if (json['event'] == 'app.dtd' && json['params'] != null) { + final params = json['params'] as Map; + if (params['uri'] != null) { + final dtdUri = Uri.parse(params['uri'] as String); + log(LoggingLevel.debug, 'Found machine DTD URI: $dtdUri'); + completer.complete((dtdUri: dtdUri, pid: process!.pid)); + } + } + } on FormatException { + // Ignore failures to parse the JSON. + } + } final match = dtdUriRegex.firstMatch(line); + // Leaving this check in for earlier versions of Flutter that don't + // print the DTD URI as part of --machine output. It won't work on + // Chrome devices, but will on other devices. if (match != null && !completer.isCompleted) { final dtdUri = Uri.parse(match.group(1)!); - log(LoggingLevel.debug, 'Found DTD URI: $dtdUri'); + log(LoggingLevel.debug, 'Found stdout DTD URI: $dtdUri'); completer.complete((dtdUri: dtdUri, pid: process!.pid)); } } From 5f7b801d0400acfc3c0da702c3ed350d52c2a661 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 21 Oct 2025 10:17:02 -0700 Subject: [PATCH 2/5] Add test --- .../lib/src/mixins/flutter_launcher.dart | 15 +- .../test/tools/flutter_launcher_test.dart | 180 ++++++++++-------- 2 files changed, 100 insertions(+), 95 deletions(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart index 7fd5f033..fc8bc754 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -117,9 +117,6 @@ base mixin FlutterLauncherSupport late StreamSubscription stdoutSubscription; late StreamSubscription stderrSubscription; - final dtdUriRegex = RegExp( - r'The Dart Tooling Daemon is available at: (ws://.+:\d+/\S+=)', - ); void checkForDtdUri(String line) { line = line.trim(); @@ -140,18 +137,10 @@ base mixin FlutterLauncherSupport } } } on FormatException { - // Ignore failures to parse the JSON. + // Ignore failures to parse the JSON or the URI. + log(LoggingLevel.debug, 'Failed to parse $line for the DTD URI.'); } } - final match = dtdUriRegex.firstMatch(line); - // Leaving this check in for earlier versions of Flutter that don't - // print the DTD URI as part of --machine output. It won't work on - // Chrome devices, but will on other devices. - if (match != null && !completer.isCompleted) { - final dtdUri = Uri.parse(match.group(1)!); - log(LoggingLevel.debug, 'Found stdout DTD URI: $dtdUri'); - completer.complete((dtdUri: dtdUri, pid: process!.pid)); - } } stdoutSubscription = process.stdout diff --git a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart index 43666dd8..66fa9298 100644 --- a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart +++ b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart @@ -74,10 +74,14 @@ void main() { : '/path/to/flutter/sdk/bin/flutter', 'run', '--print-dtd', + '--machine', '--device-id', 'test-device', ], - stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n', + stdout: + '[{"event":"app.dtd","params":{' + '"appId":"cd6c66eb-35e9-4ac1-96df-727540138346",' + '"uri":"$dtdUri"}}]', pid: processPid, ), ); @@ -121,82 +125,84 @@ void main() { await client.shutdown(); }); - test.test( - 'launch_app tool returns DTD URI and PID on success from stderr', - () async { - final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; - final processPid = 54321; - final mockProcessManager = MockProcessManager(); - mockProcessManager.addCommand( - Command( - [ - Platform.isWindows - ? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe' - : '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', - 'language-server', - '--protocol', - 'lsp', - ], - stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', - ), - ); - mockProcessManager.addCommand( - Command( - [ - Platform.isWindows - ? r'C:\path\to\flutter\sdk\bin\flutter.bat' - : '/path/to/flutter/sdk/bin/flutter', - 'run', - '--print-dtd', - '--device-id', - 'test-device', - ], - stderr: 'The Dart Tooling Daemon is available at: $dtdUri\n', - pid: processPid, - ), - ); - final serverAndClient = await createServerAndClient( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - final server = serverAndClient.server; - final client = serverAndClient.client; - - // Initialize - final initResult = await client.initialize( - InitializeRequest( - protocolVersion: ProtocolVersion.latestSupported, - capabilities: ClientCapabilities(), - clientInfo: Implementation(name: 'test_client', version: '1.0.0'), - ), - ); - test.expect(initResult.serverInfo.name, 'dart and flutter tooling'); - client.notifyInitialized(); - - // Call the tool - final result = await client.callTool( - CallToolRequest( - name: 'launch_app', - arguments: {'root': projectRoot, 'device': 'test-device'}, - ), - ); - - test.expect(result.content, [ - Content.text( - text: - 'Flutter application launched successfully with PID 54321 with the DTD URI ws://127.0.0.1:12345/abcdefg=', - ), - ]); - test.expect(result.isError, test.isNot(true)); - test.expect(result.structuredContent, { - 'dtdUri': dtdUri, - 'pid': processPid, - }); - await server.shutdown(); - await client.shutdown(); - }, - ); + test.test('launch_app tool returns DTD URI and PID on success from ' + '--machine output', () async { + final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; + final processPid = 54321; + final mockProcessManager = MockProcessManager(); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe' + : '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', + 'language-server', + '--protocol', + 'lsp', + ], + stdout: + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', + ), + ); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\flutter.bat' + : '/path/to/flutter/sdk/bin/flutter', + 'run', + '--print-dtd', + '--machine', + '--device-id', + 'test-device', + ], + stdout: + '[{"event":"app.dtd","params":{' + '"appId":"cd6c66eb-35e9-4ac1-96df-727540138346",' + '"uri":"$dtdUri"}}]', + pid: processPid, + ), + ); + final serverAndClient = await createServerAndClient( + processManager: mockProcessManager, + fileSystem: fileSystem, + ); + final server = serverAndClient.server; + final client = serverAndClient.client; + + // Initialize + final initResult = await client.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation(name: 'test_client', version: '1.0.0'), + ), + ); + test.expect(initResult.serverInfo.name, 'dart and flutter tooling'); + client.notifyInitialized(); + + // Call the tool + final result = await client.callTool( + CallToolRequest( + name: 'launch_app', + arguments: {'root': projectRoot, 'device': 'test-device'}, + ), + ); + + test.expect(result.content, [ + Content.text( + text: + 'Flutter application launched successfully with PID 54321 with the DTD URI ws://127.0.0.1:12345/abcdefg=', + ), + ]); + test.expect(result.isError, test.isNot(true)); + test.expect(result.structuredContent, { + 'dtdUri': dtdUri, + 'pid': processPid, + }); + await server.shutdown(); + await client.shutdown(); + }); test.test('launch_app tool fails when process exits early', () async { final mockProcessManager = MockProcessManager(); @@ -211,7 +217,8 @@ void main() { 'lsp', ], stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}} +''', ), ); mockProcessManager.addCommand( @@ -222,6 +229,7 @@ void main() { : '/path/to/flutter/sdk/bin/flutter', 'run', '--print-dtd', + '--machine', '--device-id', 'test-device', ], @@ -295,10 +303,14 @@ void main() { : '/path/to/flutter/sdk/bin/flutter', 'run', '--print-dtd', + '--machine', '--device-id', 'test-device', ], - stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n', + stdout: + '[{"event":"app.dtd","params":{' + '"appId":"cd6c66eb-35e9-4ac1-96df-727540138346",' + '"uri":"$dtdUri"}}]', pid: processPid, ), ); @@ -352,7 +364,8 @@ void main() { 'lsp', ], stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}} +''', ), ); mockProcessManager.addCommand( @@ -363,12 +376,15 @@ void main() { : '/path/to/flutter/sdk/bin/flutter', 'run', '--print-dtd', + '--machine', '--device-id', 'test-device', ], stdout: 'line 1\nline 2\nline 3\n' - 'The Dart Tooling Daemon is available at: $dtdUri\n', + '[{"event":"app.dtd","params":{' + '"appId":"cd6c66eb-35e9-4ac1-96df-727540138346",' + '"uri":"$dtdUri"}}]', pid: processPid, ), ); @@ -408,7 +424,7 @@ void main() { 'logs': [ '[skipping 2 log lines]...', '[stdout] line 3', - '[stdout] The Dart Tooling Daemon is available at: ws://127.0.0.1:12345/abcdefg=', + '[stdout] [{"event":"app.dtd","params":{"appId":"cd6c66eb-35e9-4ac1-96df-727540138346","uri":"ws://127.0.0.1:12345/abcdefg="}}]', ], }); await server.shutdown(); From 4564dc641e4e5fb54d23cc8f8746fea2ffbd8ef0 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 21 Oct 2025 11:45:13 -0700 Subject: [PATCH 3/5] Fix tests --- .../lib/src/mixins/flutter_launcher.dart | 4 +- pkgs/dart_mcp_server/pubspec.yaml | 1 + .../test/tools/flutter_launcher_test.dart | 198 +++++++++++++++++- 3 files changed, 193 insertions(+), 10 deletions(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart index fc8bc754..ea47cfd6 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -207,7 +207,7 @@ base mixin FlutterLauncherSupport ); final result = await completer.future.timeout( - const Duration(seconds: 90), + const Duration(seconds: 60), ); _runningApps[result.pid]?.dtdUri = result.dtdUri.toString(); @@ -227,7 +227,7 @@ base mixin FlutterLauncherSupport } catch (e, s) { log(LoggingLevel.error, 'Error launching Flutter application: $e\n$s'); if (process != null) { - process.kill(); + processManager.killPid(process.pid); // The exitCode handler will perform the rest of the cleanup. } return CallToolResult( diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml index c7346639..0cf41090 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -42,6 +42,7 @@ dependencies: dev_dependencies: analyzer: ^7.5.2 dart_flutter_team_lints: ^3.2.1 + fake_async: ^1.3.3 pub_semver: ^2.2.0 test: ^1.25.15 test_descriptor: ^2.0.2 diff --git a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart index 66fa9298..5234aa20 100644 --- a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart +++ b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart @@ -10,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:dart_mcp/client.dart'; import 'package:dart_mcp_server/src/server.dart'; import 'package:dart_mcp_server/src/utils/sdk.dart'; +import 'package:fake_async/fake_async.dart'; import 'package:file/memory.dart'; import 'package:process/process.dart'; import 'package:stream_channel/stream_channel.dart'; @@ -125,6 +126,92 @@ void main() { await client.shutdown(); }); + test.test( + 'launch_app tool returns DTD URI and PID on success from stdout', + () { + fakeAsync((async) async { + final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; + final processPid = 54321; + final mockProcessManager = MockProcessManager(); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe' + : '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', + 'language-server', + '--protocol', + 'lsp', + ], + stdout: + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', + ), + ); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\flutter.bat' + : '/path/to/flutter/sdk/bin/flutter', + 'run', + '--print-dtd', + '--machine', + '--device-id', + 'test-device', + ], + stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n', + pid: processPid, + ), + ); + + final serverAndClient = await createServerAndClient( + processManager: mockProcessManager, + fileSystem: fileSystem, + ); + final server = serverAndClient.server; + final client = serverAndClient.client; + async.flushMicrotasks(); + + // Initialize + final initResult = await client.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation(name: 'test_client', version: '1.0.0'), + ), + ); + test.expect(initResult.serverInfo.name, 'dart and flutter tooling'); + client.notifyInitialized(); + async.flushMicrotasks(); + + // Call the tool + final result = await client.callTool( + CallToolRequest( + name: 'launch_app', + arguments: {'root': projectRoot, 'device': 'test-device'}, + ), + ); + async.flushMicrotasks(); + + test.expect(result.content, [ + Content.text( + text: + 'Flutter application launched successfully with PID 54321 with the DTD URI ws://127.0.0.1:12345/abcdefg=', + ), + ]); + test.expect(result.isError, test.isNot(true)); + test.expect(result.structuredContent, { + 'dtdUri': dtdUri, + 'pid': processPid, + }); + + await server.shutdown(); + await client.shutdown(); + async.flushMicrotasks(); + }); + }, + ); + test.test('launch_app tool returns DTD URI and PID on success from ' '--machine output', () async { final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; @@ -217,8 +304,7 @@ void main() { 'lsp', ], stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}} -''', + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', ), ); mockProcessManager.addCommand( @@ -276,6 +362,105 @@ void main() { await client.shutdown(); }); + test.test('launch_app tool times out if DTD URI is not found', () { + fakeAsync((async) { + // Setup mocks + final mockProcessManager = MockProcessManager(); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe' + : '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', + 'language-server', + '--protocol', + 'lsp', + ], + stdout: + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', + ), + ); + final processPid = 54321; + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\flutter.bat' + : '/path/to/flutter/sdk/bin/flutter', + 'run', + '--print-dtd', + '--machine', + '--device-id', + 'test-device', + ], + stdout: 'Some output without DTD URI', + pid: processPid, + ), + ); + + // Create server and client + late DartMCPServer server; + late ServerConnection client; + var serverAndClientReady = false; + createServerAndClient( + processManager: mockProcessManager, + fileSystem: fileSystem, + ).then((sc) { + server = sc.server; + client = sc.client; + serverAndClientReady = true; + }); + async.flushMicrotasks(); + test.expect(serverAndClientReady, true); + + // Initialize + var initialized = false; + client.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation(name: 'test_client', version: '1.0.0'), + ), + ).then((_) { + client.notifyInitialized(); + initialized = true; + }); + async.flushMicrotasks(); + test.expect(initialized, true); + + // Call the tool + late CallToolResult result; + var completed = false; + client.callTool( + CallToolRequest( + name: 'launch_app', + arguments: {'root': projectRoot, 'device': 'test-device'}, + ), + ).then((res) { + result = res; + completed = true; + }); + + // Elapse time to trigger timeout + async.elapse(const Duration(seconds: 61)); + async.flushMicrotasks(); + + test.expect(completed, true); + test.expect(result.isError, true); + final textOutput = result.content as List; + test.expect( + textOutput.first.text, + test.stringContainsInOrder( + ['Failed to launch Flutter application', 'TimeoutException']), + ); + test.expect(mockProcessManager.killedPids, [processPid]); + + server.shutdown(); + client.shutdown(); + async.flushMicrotasks(); + }); + }); + test.test('stop_app tool stops a running app', () async { final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; final processPid = 54321; @@ -291,8 +476,7 @@ void main() { 'lsp', ], stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}} -''', + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', ), ); mockProcessManager.addCommand( @@ -364,8 +548,7 @@ void main() { 'lsp', ], stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}} -''', + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', ), ); mockProcessManager.addCommand( @@ -444,8 +627,7 @@ void main() { 'lsp', ], stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}} -''', + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', ), ); mockProcessManager.addCommand( From e3e2fceddc93c4ecd368c0f513be841246f63fe1 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 21 Oct 2025 12:39:45 -0700 Subject: [PATCH 4/5] Fix formatting. --- .../lib/src/mixins/flutter_launcher.dart | 2 +- .../test/tools/flutter_launcher_test.dart | 219 +++++++++--------- 2 files changed, 114 insertions(+), 107 deletions(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart index ea47cfd6..00cf5e2e 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart @@ -207,7 +207,7 @@ base mixin FlutterLauncherSupport ); final result = await completer.future.timeout( - const Duration(seconds: 60), + const Duration(seconds: 90), ); _runningApps[result.pid]?.dtdUri = result.dtdUri.toString(); diff --git a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart index 5234aa20..09ae3656 100644 --- a/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart +++ b/pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart @@ -126,91 +126,89 @@ void main() { await client.shutdown(); }); - test.test( - 'launch_app tool returns DTD URI and PID on success from stdout', - () { - fakeAsync((async) async { - final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; - final processPid = 54321; - final mockProcessManager = MockProcessManager(); - mockProcessManager.addCommand( - Command( - [ - Platform.isWindows - ? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe' - : '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', - 'language-server', - '--protocol', - 'lsp', - ], - stdout: - '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', - ), - ); - mockProcessManager.addCommand( - Command( - [ - Platform.isWindows - ? r'C:\path\to\flutter\sdk\bin\flutter.bat' - : '/path/to/flutter/sdk/bin/flutter', - 'run', - '--print-dtd', - '--machine', - '--device-id', - 'test-device', - ], - stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n', - pid: processPid, - ), - ); - - final serverAndClient = await createServerAndClient( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - final server = serverAndClient.server; - final client = serverAndClient.client; - async.flushMicrotasks(); - - // Initialize - final initResult = await client.initialize( - InitializeRequest( - protocolVersion: ProtocolVersion.latestSupported, - capabilities: ClientCapabilities(), - clientInfo: Implementation(name: 'test_client', version: '1.0.0'), - ), - ); - test.expect(initResult.serverInfo.name, 'dart and flutter tooling'); - client.notifyInitialized(); - async.flushMicrotasks(); - - // Call the tool - final result = await client.callTool( - CallToolRequest( - name: 'launch_app', - arguments: {'root': projectRoot, 'device': 'test-device'}, - ), - ); - async.flushMicrotasks(); - - test.expect(result.content, [ - Content.text( - text: - 'Flutter application launched successfully with PID 54321 with the DTD URI ws://127.0.0.1:12345/abcdefg=', - ), - ]); - test.expect(result.isError, test.isNot(true)); - test.expect(result.structuredContent, { - 'dtdUri': dtdUri, - 'pid': processPid, - }); - - await server.shutdown(); - await client.shutdown(); - async.flushMicrotasks(); + test.test('launch_app tool returns DTD URI and PID on success from ' + 'stdout', () { + fakeAsync((async) async { + final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; + final processPid = 54321; + final mockProcessManager = MockProcessManager(); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe' + : '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', + 'language-server', + '--protocol', + 'lsp', + ], + stdout: + '''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''', + ), + ); + mockProcessManager.addCommand( + Command( + [ + Platform.isWindows + ? r'C:\path\to\flutter\sdk\bin\flutter.bat' + : '/path/to/flutter/sdk/bin/flutter', + 'run', + '--print-dtd', + '--machine', + '--device-id', + 'test-device', + ], + stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n', + pid: processPid, + ), + ); + + final serverAndClient = await createServerAndClient( + processManager: mockProcessManager, + fileSystem: fileSystem, + ); + final server = serverAndClient.server; + final client = serverAndClient.client; + async.flushMicrotasks(); + + // Initialize + final initResult = await client.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation(name: 'test_client', version: '1.0.0'), + ), + ); + test.expect(initResult.serverInfo.name, 'dart and flutter tooling'); + client.notifyInitialized(); + async.flushMicrotasks(); + + // Call the tool + final result = await client.callTool( + CallToolRequest( + name: 'launch_app', + arguments: {'root': projectRoot, 'device': 'test-device'}, + ), + ); + async.flushMicrotasks(); + + test.expect(result.content, [ + Content.text( + text: + 'Flutter application launched successfully with PID 54321 with the DTD URI ws://127.0.0.1:12345/abcdefg=', + ), + ]); + test.expect(result.isError, test.isNot(true)); + test.expect(result.structuredContent, { + 'dtdUri': dtdUri, + 'pid': processPid, }); - }, - ); + + await server.shutdown(); + await client.shutdown(); + async.flushMicrotasks(); + }); + }); test.test('launch_app tool returns DTD URI and PID on success from ' '--machine output', () async { @@ -415,34 +413,41 @@ void main() { // Initialize var initialized = false; - client.initialize( - InitializeRequest( - protocolVersion: ProtocolVersion.latestSupported, - capabilities: ClientCapabilities(), - clientInfo: Implementation(name: 'test_client', version: '1.0.0'), - ), - ).then((_) { - client.notifyInitialized(); - initialized = true; - }); + client + .initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation( + name: 'test_client', + version: '1.0.0', + ), + ), + ) + .then((_) { + client.notifyInitialized(); + initialized = true; + }); async.flushMicrotasks(); test.expect(initialized, true); // Call the tool late CallToolResult result; var completed = false; - client.callTool( - CallToolRequest( - name: 'launch_app', - arguments: {'root': projectRoot, 'device': 'test-device'}, - ), - ).then((res) { - result = res; - completed = true; - }); + client + .callTool( + CallToolRequest( + name: 'launch_app', + arguments: {'root': projectRoot, 'device': 'test-device'}, + ), + ) + .then((res) { + result = res; + completed = true; + }); // Elapse time to trigger timeout - async.elapse(const Duration(seconds: 61)); + async.elapse(const Duration(seconds: 91)); async.flushMicrotasks(); test.expect(completed, true); @@ -450,8 +455,10 @@ void main() { final textOutput = result.content as List; test.expect( textOutput.first.text, - test.stringContainsInOrder( - ['Failed to launch Flutter application', 'TimeoutException']), + test.stringContainsInOrder([ + 'Failed to launch Flutter application', + 'TimeoutException', + ]), ); test.expect(mockProcessManager.killedPids, [processPid]); From 55e1c1dc3a675fd85294badcb9c4dca94884c536 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 21 Oct 2025 12:50:22 -0700 Subject: [PATCH 5/5] Add Changelog entry. --- pkgs/dart_mcp_server/CHANGELOG.md | 121 +++++++++++++++--------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/pkgs/dart_mcp_server/CHANGELOG.md b/pkgs/dart_mcp_server/CHANGELOG.md index 0e4d47f8..11ef0327 100644 --- a/pkgs/dart_mcp_server/CHANGELOG.md +++ b/pkgs/dart_mcp_server/CHANGELOG.md @@ -1,73 +1,74 @@ # 0.1.1 (Dart SDK 3.10.0) - WIP -* Change tools that accept multiple roots to not return immediately on the first +- Change tools that accept multiple roots to not return immediately on the first failure. -* Add failure reason field to analytics events so we can know why tool calls are +- Add failure reason field to analytics events so we can know why tool calls are failing. -* Add a flutter_driver command for executing flutter driver commands on a device. -* Allow for multiple package arguments to `pub add` and `pub remove`. -* Require dart_mcp version 0.3.1. -* Add support for the flutter_driver screenshot command. -* Change the widget tree to the full version instead of the summary. The summary +- Add a flutter_driver command for executing flutter driver commands on a device. +- Allow for multiple package arguments to `pub add` and `pub remove`. +- Require dart_mcp version 0.3.1. +- Add support for the flutter_driver screenshot command. +- Change the widget tree to the full version instead of the summary. The summary tends to hide nested text widgets which makes it difficult to find widgets based on their text values. -* Add an `--exclude-tool` command line flag to exclude tools by name. -* Add the abillity to limit the output of `analyze_files` to a set of paths. -* Stop reporting non-zero exit codes from command line tools as tool errors. -* Add descriptions for pub tools, add support for `pub deps` and `pub outdated`. -* Fix a bug in hot_reload ([#290](https://github.com/dart-lang/ai/issues/290)). -* Add the `list_devices`, `launch_app`, `get_app_logs`, and `list_running_apps` +- Add an `--exclude-tool` command line flag to exclude tools by name. +- Add the abillity to limit the output of `analyze_files` to a set of paths. +- Stop reporting non-zero exit codes from command line tools as tool errors. +- Add descriptions for pub tools, add support for `pub deps` and `pub outdated`. +- Fix a bug in hot_reload ([#290](https://github.com/dart-lang/ai/issues/290)). +- Add the `list_devices`, `launch_app`, `get_app_logs`, and `list_running_apps` tools for running Flutter apps. -* Add the `hot_restart` tool for restarting running Flutter apps. -* Add extra log output to failed launches, and allow AI to specify the maxLines +- Add the `hot_restart` tool for restarting running Flutter apps. +- Add extra log output to failed launches, and allow AI to specify the maxLines of log output. +- Convert `launch_app` to use `--machine` output to capture the DTD URI. # 0.1.0 (Dart SDK 3.9.0) -* Add documentation/homepage/repository links to pub results. -* Handle relative paths under roots without trailing slashes. -* Fix executable paths for dart/flutter on windows. -* Pass the provided root instead of the resolved root for project type detection. -* Be more flexible about roots by comparing canonicalized paths. -* Create the working dir if it doesn't exist. -* Add the --platform and --empty arguments to the flutter create tool. -* Invoke dart/flutter in a more robust way. -* Remove qualifiedNames from the pub dev api search. -* Flutter/Dart create tool. -* Limit the tokens returned by the runtime errors tool/resource. -* Add RootsFallbackSupport mixin. -* Fix error handling around stream listeners. -* Add a 'pub-dev-search' mcp tool. -* Drop pubspec-parse, use yaml instead. -* Handle failing to listen to vm service streams during startup. -* Add tool for enabling/disabling the widget selector. -* Add a tool to get the active cursor location. -* Add hover tool support. -* Add a test command and project detection. -* Add signature_help tool. -* Add runtime errors resource and tool to clear errors. -* Require roots for all CLI tools. -* Require roots to be set for analyzer tools. -* Add debug logs for when DTD sees Editor.getDebugSessions get registered. -* Add tool annotations to tools. -* Implement a tool to resolve workspace symbols based on a query. -* Add a dart pub tool. -* Update analyze tool to use LSP, simplify tool. -* Add tool for getting the selected widget. -* Handle missing roots capability better. -* Add `get_widget_tree` tool. -* Add a tool for getting runtime errors. -* Add Dart CLI tool support. -* Add a hot reload tool. -* Add basic analysis support. -* Add the beginnings of a Dart tooling MCP server. -* Instruct clients to prefer MCP tools over running tools in the shell. -* Reduce output size of `run_tests` tool to save on input tokens. -* Add `--log-file` argument to log all protocol traffic to a file. -* Improve error text for failed DTD connections as well as the tool description. -* Add support for injecting an `Analytics` instance to track usage. -* Listen to the new DTD `ConnectedApp` service instead of the `Editor.DebugSessions` +- Add documentation/homepage/repository links to pub results. +- Handle relative paths under roots without trailing slashes. +- Fix executable paths for dart/flutter on windows. +- Pass the provided root instead of the resolved root for project type detection. +- Be more flexible about roots by comparing canonicalized paths. +- Create the working dir if it doesn't exist. +- Add the --platform and --empty arguments to the flutter create tool. +- Invoke dart/flutter in a more robust way. +- Remove qualifiedNames from the pub dev api search. +- Flutter/Dart create tool. +- Limit the tokens returned by the runtime errors tool/resource. +- Add RootsFallbackSupport mixin. +- Fix error handling around stream listeners. +- Add a 'pub-dev-search' mcp tool. +- Drop pubspec-parse, use yaml instead. +- Handle failing to listen to vm service streams during startup. +- Add tool for enabling/disabling the widget selector. +- Add a tool to get the active cursor location. +- Add hover tool support. +- Add a test command and project detection. +- Add signature_help tool. +- Add runtime errors resource and tool to clear errors. +- Require roots for all CLI tools. +- Require roots to be set for analyzer tools. +- Add debug logs for when DTD sees Editor.getDebugSessions get registered. +- Add tool annotations to tools. +- Implement a tool to resolve workspace symbols based on a query. +- Add a dart pub tool. +- Update analyze tool to use LSP, simplify tool. +- Add tool for getting the selected widget. +- Handle missing roots capability better. +- Add `get_widget_tree` tool. +- Add a tool for getting runtime errors. +- Add Dart CLI tool support. +- Add a hot reload tool. +- Add basic analysis support. +- Add the beginnings of a Dart tooling MCP server. +- Instruct clients to prefer MCP tools over running tools in the shell. +- Reduce output size of `run_tests` tool to save on input tokens. +- Add `--log-file` argument to log all protocol traffic to a file. +- Improve error text for failed DTD connections as well as the tool description. +- Add support for injecting an `Analytics` instance to track usage. +- Listen to the new DTD `ConnectedApp` service instead of the `Editor.DebugSessions` service, when available. -* Screenshot tool disabled until +- Screenshot tool disabled until https://github.com/flutter/flutter/issues/170357 is resolved. -* Add `arg_parser.dart` public library with minimal deps to be used by the dart tool. +- Add `arg_parser.dart` public library with minimal deps to be used by the dart tool.