Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkgs/dart_mcp_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,6 @@ For more information, see the official VS Code documentation for
| `run_tests` | Run tests | Run Dart or Flutter tests with an agent centric UX. ALWAYS use instead of `dart test` or `flutter test` shell commands. |
| `set_widget_selection_mode` | Set Widget Selection Mode | Enables or disables widget selection mode in the active Flutter application. Requires "connect_dart_tooling_daemon" to be successfully called first. This is not necessary when using flutter driver, only use it when you want the user to select a widget. |
| `signature_help` | Signature help | Get signature help for an API being used at a given cursor position in a file. |
| `stop_app` | | Kills a running Flutter process started by the launch_app tool. |
| `stop_app` | | Stops a running Flutter process started by the launch_app tool. |

<!-- generated -->
35 changes: 30 additions & 5 deletions pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,19 +243,19 @@ base mixin FlutterLauncherSupport
final stopAppTool = Tool(
name: 'stop_app',
description:
'Kills a running Flutter process started by the launch_app tool.',
'Stops a running Flutter process started by the launch_app tool.',
inputSchema: Schema.object(
properties: {
'pid': Schema.int(
description: 'The process ID of the process to kill.',
description: 'The process ID of the process to stop.',
),
},
required: ['pid'],
),
outputSchema: Schema.object(
properties: {
'success': Schema.bool(
description: 'Whether the process was killed successfully.',
description: 'Whether the process was stopped successfully.',
),
},
required: ['success'],
Expand All @@ -274,8 +274,33 @@ base mixin FlutterLauncherSupport
content: [TextContent(text: 'Application with PID $pid not found.')],
);
}

final success = processManager.killPid(pid);
// On Unix, killing the flutter process doesn't kill the entire process
// group, so we have to look for the child processes.
if (Platform.isLinux) {
final ps = processManager.runSync([
'ps',
'--no-headers',
'--format',
'%p',
'--ppid',
'$pid',
]);
if (ps.exitCode == 0) {
final children = (ps.stdout as String).trim().split('\n');
if (children.isNotEmpty) {
for (final child in children) {
int childPid;
try {
childPid = int.parse(child);
} on FormatException {
continue;
}
processManager.killPid(childPid, ProcessSignal.sigterm);
}
}
}
}
final success = processManager.killPid(app.process.pid);
if (success) {
log(
LoggingLevel.info,
Expand Down
39 changes: 31 additions & 8 deletions pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ void main() {
'test-device',
],
stderr: 'Something went wrong',
exitCode: Future.value(1),
exitCode: 1,
),
);
final serverAndClient = await createServerAndClient(
Expand Down Expand Up @@ -505,6 +505,15 @@ void main() {
pid: processPid,
),
);
if (Platform.isLinux) {
mockProcessManager.addCommand(
Command(
['ps', '--no-headers', '--format', '%p', '--ppid', '$processPid'],
stdout: '11111\n22222\n',
pid: processPid,
),
);
}
final serverAndClient = await createServerAndClient(
processManager: mockProcessManager,
fileSystem: fileSystem,
Expand Down Expand Up @@ -533,9 +542,12 @@ void main() {
CallToolRequest(name: 'stop_app', arguments: {'pid': processPid}),
);

test.expect(result.isError, test.isNot(true));
test.expect(result.structuredContent, {'success': true});
test.expect(mockProcessManager.killedPids, [processPid]);
test.expect(mockProcessManager.killedPids, [
if (Platform.isLinux) ...[11111, 22222],
processPid,
]);
test.expect(result.isError, test.isNot(true));
await server.shutdown();
await client.shutdown();
});
Expand Down Expand Up @@ -688,7 +700,7 @@ class Command {
final List<String> command;
final String? stdout;
final String? stderr;
final Future<int>? exitCode;
final int? exitCode;
final int pid;

Command(
Expand Down Expand Up @@ -720,7 +732,9 @@ class MockProcessManager implements ProcessManager {
}
}
throw Exception(
'Command not mocked: $command. Mocked commands:\n${_commands.join('\n')}',
'Command not mocked: "${command.join(' ')}".\n'
'Mocked commands:\n'
'${_commands.map<String>((e) => e.command.join(' ')).join('\n')}',
);
}

Expand All @@ -743,7 +757,9 @@ class MockProcessManager implements ProcessManager {
stdout: Stream.value(utf8.encode(mockCommand.stdout ?? '')),
stderr: Stream.value(utf8.encode(mockCommand.stderr ?? '')),
pid: pid,
exitCodeFuture: mockCommand.exitCode,
exitCodeFuture: mockCommand.exitCode != null
? Future(() => mockCommand.exitCode!)
: null,
);
runningProcesses[pid] = process;
return process;
Expand All @@ -770,7 +786,7 @@ class MockProcessManager implements ProcessManager {
final mockCommand = _findCommand(command);
return ProcessResult(
mockCommand.pid,
await (mockCommand.exitCode ?? Future.value(0)),
mockCommand.exitCode ?? 0,
mockCommand.stdout ?? '',
mockCommand.stderr ?? '',
);
Expand All @@ -789,7 +805,14 @@ class MockProcessManager implements ProcessManager {
Encoding? stdoutEncoding = systemEncoding,
Encoding? stderrEncoding = systemEncoding,
}) {
throw UnimplementedError();
commands.add(command);
final mockCommand = _findCommand(command);
return ProcessResult(
mockCommand.pid,
mockCommand.exitCode ?? 0,
mockCommand.stdout ?? '',
mockCommand.stderr ?? '',
);
}
}

Expand Down