From daaa9f66782175a9eea96ca5e243d4e18d06cf89 Mon Sep 17 00:00:00 2001 From: Stefano Amorelli Date: Sat, 4 Oct 2025 16:30:03 +0300 Subject: [PATCH 1/4] fix(test): implement MockIOSink to enable stdin testing Fix broken test infrastructure where MockProcess.stdin threw UnimplementedError, making it impossible to test any functionality that writes to process stdin. This was a bug in the test mock that prevented testing stdin-based features like hot restart commands. Changes: - Replace UnimplementedError with functional MockIOSink implementation - Implement writeln, flush, and close methods - Add state tracking for verification in tests --- flutter_launcher_mcp/test/sdk_test.dart | 59 ++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/flutter_launcher_mcp/test/sdk_test.dart b/flutter_launcher_mcp/test/sdk_test.dart index 6564ce5..4c0c159 100644 --- a/flutter_launcher_mcp/test/sdk_test.dart +++ b/flutter_launcher_mcp/test/sdk_test.dart @@ -245,6 +245,9 @@ class MockProcess implements Process { bool killed = false; + @override + late final IOSink stdin = _MockIOSink(); + MockProcess({ required this.stdout, required this.stderr, @@ -268,7 +271,61 @@ class MockProcess implements Process { } return true; } +} + +class _MockIOSink implements IOSink { + final List writtenLines = []; + bool _closed = false; + + @override + Encoding encoding = utf8; + + @override + void add(List data) { + if (_closed) throw StateError('IOSink is closed'); + } @override - late final IOSink stdin = throw UnimplementedError(); + void write(Object? object) { + if (_closed) throw StateError('IOSink is closed'); + } + + @override + void writeln([Object? object = '']) { + if (_closed) throw StateError('IOSink is closed'); + writtenLines.add(object.toString()); + } + + @override + Future flush() async { + if (_closed) throw StateError('IOSink is closed'); + } + + @override + Future close() async { + _closed = true; + } + + @override + void addError(Object error, [StackTrace? stackTrace]) { + throw UnimplementedError(); + } + + @override + Future addStream(Stream> stream) { + throw UnimplementedError(); + } + + @override + Future get done => Future.value(); + + @override + void writeAll(Iterable objects, [String separator = '']) { + throw UnimplementedError(); + } + + @override + void writeCharCode(int charCode) { + throw UnimplementedError(); + } } From 55525b491fcd1112dc114ccf37c34f9976e6e502 Mon Sep 17 00:00:00 2001 From: Stefano Amorelli Date: Sat, 4 Oct 2025 16:30:10 +0300 Subject: [PATCH 2/4] feat(flutter_launcher): add hot_restart tool for running applications Implement hot restart functionality to allow restarting Flutter applications without terminating the debug session. The tool sends the 'R' command to the running process's stdin, triggering a full application restart while maintaining the connection. Changes: - Add hotRestartTool definition with input/output schemas - Implement _hotRestart handler method - Register hot_restart tool in initialize method - Add error handling for non-existent process IDs --- .../lib/src/mixins/flutter_launcher.dart | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart b/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart index 294034b..f5b535a 100644 --- a/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart +++ b/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart @@ -39,6 +39,7 @@ base mixin FlutterLauncherSupport registerTool(listDevicesTool, _listDevices); registerTool(getAppLogsTool, _getAppLogs); registerTool(listRunningAppsTool, _listRunningApps); + registerTool(hotRestartTool, _hotRestart); return super.initialize(request); } @@ -461,6 +462,73 @@ base mixin FlutterLauncherSupport ); } + /// A tool to perform a hot restart on a running Flutter application. + final hotRestartTool = Tool( + name: 'hot_restart', + description: + 'Performs a hot restart on a running Flutter application. This restarts the app while maintaining the current session.', + inputSchema: Schema.object( + properties: { + 'pid': Schema.int( + description: + 'The process ID of the flutter run process to hot restart.', + ), + }, + required: ['pid'], + ), + outputSchema: Schema.object( + properties: { + 'success': Schema.bool( + description: 'Whether the hot restart was successful.', + ), + }, + required: ['success'], + ), + ); + + Future _hotRestart(CallToolRequest request) async { + final pid = request.arguments!['pid'] as int; + log(LoggingLevel.info, 'Attempting hot restart for application PID: $pid'); + final app = _runningApps[pid]; + + if (app == null) { + log(LoggingLevel.error, 'Application with PID $pid not found.'); + return CallToolResult( + isError: true, + content: [TextContent(text: 'Application with PID $pid not found.')], + ); + } + + try { + app.process.stdin.writeln('R'); + await app.process.stdin.flush(); + log(LoggingLevel.info, 'Hot restart command sent to application $pid.'); + + return CallToolResult( + content: [ + TextContent( + text: 'Hot restart initiated for application with PID $pid.', + ), + ], + structuredContent: {'success': true}, + ); + } catch (e, s) { + log( + LoggingLevel.error, + 'Error performing hot restart for application $pid: $e\n$s', + ); + return CallToolResult( + isError: true, + content: [ + TextContent( + text: 'Failed to perform hot restart for application $pid: $e', + ), + ], + structuredContent: {'success': false}, + ); + } + } + @override Future shutdown() { log(LoggingLevel.info, 'Shutting down server, killing all processes.'); From 2ec055120b197d4a8a16da2f82c662d87f64c41b Mon Sep 17 00:00:00 2001 From: Stefano Amorelli Date: Sat, 4 Oct 2025 16:30:16 +0300 Subject: [PATCH 3/4] test(flutter_launcher): add tests for hot_restart functionality Add comprehensive test coverage for the hot_restart tool, including success and error scenarios. Tests added: - Successful hot restart on running application - Error handling for non-existent process ID --- flutter_launcher_mcp/test/server_test.dart | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/flutter_launcher_mcp/test/server_test.dart b/flutter_launcher_mcp/test/server_test.dart index acc9fee..712ab2d 100644 --- a/flutter_launcher_mcp/test/server_test.dart +++ b/flutter_launcher_mcp/test/server_test.dart @@ -155,5 +155,91 @@ void main() { await client.shutdown(); }, ); + + test.test( + 'hot_restart tool sends restart command to running app', + () async { + final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; + final processPid = 54321; + final mockProcessManager = MockProcessManager(); + mockProcessManager.addCommand( + Command( + [ + '/path/to/flutter/sdk/bin/flutter', + 'run', + '--print-dtd', + '--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; + + // Initialize + await client.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation(name: 'test_client', version: '1.0.0'), + ), + ); + client.notifyInitialized(); + + // Launch the app first + final launchResult = await client.callTool( + CallToolRequest( + name: 'launch_app', + arguments: {'root': '/test/project', 'device': 'test-device'}, + ), + ); + test.expect(launchResult.isError, test.isNot(true)); + + // Perform hot restart + final restartResult = await client.callTool( + CallToolRequest(name: 'hot_restart', arguments: {'pid': processPid}), + ); + + test.expect(restartResult.isError, test.isNot(true)); + test.expect(restartResult.structuredContent, {'success': true}); + await server.shutdown(); + await client.shutdown(); + }, + ); + + test.test('hot_restart tool returns error for non-existent app', () async { + final mockProcessManager = MockProcessManager(); + final serverAndClient = await createServerAndClient( + processManager: mockProcessManager, + fileSystem: fileSystem, + ); + final server = serverAndClient.server; + final client = serverAndClient.client; + + // Initialize + await client.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: ClientCapabilities(), + clientInfo: Implementation(name: 'test_client', version: '1.0.0'), + ), + ); + client.notifyInitialized(); + + // Try to hot restart a non-existent app + final restartResult = await client.callTool( + CallToolRequest(name: 'hot_restart', arguments: {'pid': 99999}), + ); + + test.expect(restartResult.isError, true); + await server.shutdown(); + await client.shutdown(); + }); }); } From b35ba66c6a885890329f6e9c347a28cd57e64aa0 Mon Sep 17 00:00:00 2001 From: Stefano Amorelli Date: Sat, 4 Oct 2025 16:30:22 +0300 Subject: [PATCH 4/4] docs: add hot_restart tool to documentation Update documentation to include the new hot_restart tool in the list of available MCP server tools. Changes: - Add hot_restart to tool list in main README - Add hot_restart API documentation in flutter_launcher_mcp README - Include input/output schemas for the tool --- README.md | 1 + flutter_launcher_mcp/README.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/README.md b/README.md index df37052..abb70c5 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ This extension also installs an MCP server (`flutter_launcher`) that provides to - `launch_app`: Launches a Flutter application on a specified device. - `stop_app`: Stops a running Flutter application. +- `hot_restart`: Performs a hot restart on a running Flutter application, resetting the app state while maintaining the current session. - `list_devices`: Lists all available devices that can run Flutter applications. - `get_app_logs`: Retrieves the logs from a running Flutter application. - `list_running_apps`: Lists all Flutter applications currently running that were started by this extension. diff --git a/flutter_launcher_mcp/README.md b/flutter_launcher_mcp/README.md index 2018eb4..79b1e66 100644 --- a/flutter_launcher_mcp/README.md +++ b/flutter_launcher_mcp/README.md @@ -89,6 +89,40 @@ Launches a Flutter application with specified arguments and returns its DTD URI } ``` +#### `hot_restart` + +Performs a hot restart on a running Flutter application. This restarts the app while maintaining the current session. + +- **Input Schema:** + + ```json + { + "type": "object", + "properties": { + "pid": { + "type": "integer", + "description": "The process ID of the flutter run process to hot restart." + } + }, + "required": ["pid"] + } + ``` + +- **Output Schema:** + + ```json + { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "description": "Whether the hot restart was successful." + } + }, + "required": ["success"] + } + ``` + #### `stop_app` Kills a running Flutter process started by the `launch_app` tool.