From e15bcea6b2951f90a1b02c5281d7bd4cb6f9bb0a Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Tue, 7 Oct 2025 15:00:25 -0700 Subject: [PATCH] Add hot restart tool --- pkgs/dart_mcp_server/CHANGELOG.md | 1 + pkgs/dart_mcp_server/README.md | 3 +- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 66 ++++++++++++++++++- pkgs/dart_mcp_server/test/tools/dtd_test.dart | 21 ++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/pkgs/dart_mcp_server/CHANGELOG.md b/pkgs/dart_mcp_server/CHANGELOG.md index 3802d734..fb0318fa 100644 --- a/pkgs/dart_mcp_server/CHANGELOG.md +++ b/pkgs/dart_mcp_server/CHANGELOG.md @@ -18,6 +18,7 @@ * 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. # 0.1.0 (Dart SDK 3.9.0) diff --git a/pkgs/dart_mcp_server/README.md b/pkgs/dart_mcp_server/README.md index 6759d682..8431c3e0 100644 --- a/pkgs/dart_mcp_server/README.md +++ b/pkgs/dart_mcp_server/README.md @@ -147,7 +147,8 @@ For more information, see the official VS Code documentation for | `get_runtime_errors` | Get runtime errors | Retrieves the most recent runtime errors that have occurred in the active Dart or Flutter application. Requires "connect_dart_tooling_daemon" to be successfully called first. | | `get_selected_widget` | Get selected widget | Retrieves the selected widget from the active Flutter application. Requires "connect_dart_tooling_daemon" to be successfully called first. | | `get_widget_tree` | Get widget tree | Retrieves the widget tree from the active Flutter application. Requires "connect_dart_tooling_daemon" to be successfully called first. | -| `hot_reload` | Hot reload | Performs a hot reload of the active Flutter application. This is to apply the latest code changes to the running application. Requires "connect_dart_tooling_daemon" to be successfully called first. | +| `hot_reload` | Hot reload | Performs a hot reload of the active Flutter application. This will apply the latest code changes to the running application, while maintaining application state. Reload will not update const definitions of global values. Requires "connect_dart_tooling_daemon" to be successfully called first. | +| `hot_restart` | Hot restart | Performs a hot restart of the active Flutter application. This applies the latest code changes to the running application, including changes to global const values, while resetting application state. Requires "connect_dart_tooling_daemon" to be successfully called first. Doesn't work for Non-Flutter Dart CLI programs. | | `hover` | Hover information | Get hover information at a given cursor position in a file. This can include documentation, type information, etc for the text at that position. | | `launch_app` | | Launches a Flutter application and returns its DTD URI. | | `list_devices` | | Lists available Flutter devices. | diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index 693823be..f808fc5f 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -157,6 +157,7 @@ base mixin DartToolingDaemonSupport // Flutter app that does not support the operation, e.g. hot reload is not // supported in profile mode). if (enableScreenshots) registerTool(screenshotTool, takeScreenshot); + registerTool(hotRestartTool, hotRestart); registerTool(hotReloadTool, hotReload); registerTool(getWidgetTreeTool, widgetTree); registerTool(getSelectedWidgetTool, selectedWidget); @@ -351,6 +352,51 @@ base mixin DartToolingDaemonSupport ); } + /// Performs a hot restart on the currently running app. + /// + /// If more than one debug session is active, then it just uses the first + /// one. + // TODO: support passing a debug session id when there is more than one + // debug session. + Future hotRestart(CallToolRequest request) async { + return _callOnVmService( + callback: (vmService) async { + final appListener = await _AppListener.forVmService(vmService, this); + appListener.errorLog.clear(); + + final vm = await vmService.getVM(); + var success = false; + try { + final hotRestartMethodName = + (await appListener.waitForServiceRegistration('hotRestart')) ?? + 'hotRestart'; + + /// If we haven't seen a specific one, we just call the default one. + final result = await vmService.callMethod( + hotRestartMethodName, + isolateId: vm.isolates!.first.id, + ); + final resultType = result.json?['type']; + success = resultType == 'Success'; + } catch (e) { + // Handle potential errors during the process + return CallToolResult( + isError: true, + content: [TextContent(text: 'Hot restart failed: $e')], + ); + } + return CallToolResult( + isError: !success ? true : null, + content: [ + TextContent( + text: 'Hot restart ${success ? 'succeeded' : 'failed'}.', + ), + ], + ); + }, + ); + } + /// Performs a hot reload on the currently running app. /// /// If more than one debug session is active, then it just uses the first one. @@ -902,8 +948,10 @@ base mixin DartToolingDaemonSupport name: 'hot_reload', description: 'Performs a hot reload of the active Flutter application. ' - 'This is to apply the latest code changes to the running application. ' - 'Requires "${connectTool.name}" to be successfully called first.', + 'This will apply the latest code changes to the running application, ' + 'while maintaining application state. Reload will not update const ' + 'definitions of global values. Requires "${connectTool.name}" to be ' + 'successfully called first.', annotations: ToolAnnotations(title: 'Hot reload', destructiveHint: true), inputSchema: Schema.object( properties: { @@ -918,6 +966,20 @@ base mixin DartToolingDaemonSupport ), ); + @visibleForTesting + static final hotRestartTool = Tool( + name: 'hot_restart', + description: + 'Performs a hot restart of the active Flutter application. ' + 'This applies the latest code changes to the running application, ' + 'including changes to global const values, while resetting ' + 'application state. Requires "${connectTool.name}" to be ' + "successfully called first. Doesn't work for Non-Flutter Dart CLI " + 'programs.', + annotations: ToolAnnotations(title: 'Hot restart', destructiveHint: true), + inputSchema: Schema.object(properties: {}, required: []), + ); + @visibleForTesting static final getWidgetTreeTool = Tool( name: 'get_widget_tree', diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 79ef04cf..8543f352 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -75,6 +75,27 @@ void main() { TextContent(text: 'Hot reload succeeded.'), ]); }); + + test('can perform a hot restart', () async { + await testHarness.startDebugSession( + counterAppPath, + 'lib/main.dart', + isFlutter: true, + ); + final tools = + (await testHarness.mcpServerConnection.listTools()).tools; + final hotRestartTool = tools.singleWhere( + (t) => t.name == DartToolingDaemonSupport.hotRestartTool.name, + ); + final hotRestartResult = await testHarness.callToolWithRetry( + CallToolRequest(name: hotRestartTool.name), + ); + + expect(hotRestartResult.isError, isNot(true)); + expect(hotRestartResult.content, [ + TextContent(text: 'Hot restart succeeded.'), + ]); + }); }); group('dart cli tests', () {