Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 3 additions & 2 deletions doc/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,13 @@ Runs tests in a Dart or Flutter project.
"platform": "chrome | vm | android | ios",
"dart-define": "foo=bar",
"dart-define-from-file": "config.json",
"test_randomize_ordering_seed": "random"
"test_randomize_ordering_seed": "random",
"timeout_seconds": 120
}
}
```

All parameters are optional. When `optimization` is not specified, `--no-optimization` is applied by default.
All parameters are optional. When `optimization` is not specified, `--no-optimization` is applied by default. When `timeout_seconds` is not specified, the test run is killed after 120 seconds.

### `packages_get`

Expand Down
47 changes: 45 additions & 2 deletions lib/src/mcp/mcp_server.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:io' show stderr;
import 'dart:io' show Process, stderr;

import 'package:args/command_runner.dart';
import 'package:dart_mcp/server.dart';
Expand Down Expand Up @@ -221,6 +221,13 @@ Only one value can be selected.
'(e.g. // coverage:ignore-line). '
'Only applies to Dart tests (dart: true).',
),
'timeout_seconds': IntegerSchema(
description:
'Maximum seconds to wait for the test run before killing '
'the Flutter test process. Flutter tests can hang '
'indefinitely when pumpAndSettle() is called without a '
'timeout argument (default is 10 minutes). Defaults to 120.',
),
},
),
),
Expand Down Expand Up @@ -425,8 +432,44 @@ Only one value can be selected.

Future<CallToolResult> _handleTest(CallToolRequest request) async {
final args = request.arguments ?? {};
final timeoutSeconds = (args['timeout_seconds'] as num?)?.toInt() ?? 120;
final cliArgs = _parseTest(args);
return _runToolCommand(cliArgs, toolName: 'test');
try {
return await _runToolCommand(
cliArgs,
toolName: 'test',
).timeout(Duration(seconds: timeoutSeconds));
} on TimeoutException {
stderr.writeln(
'[very_good_mcp] Test run timed out after ${timeoutSeconds}s. '
'Killing flutter_tester processes.',
);
// OS-level SIGKILL is the only reliable way to stop a hung
// flutter_tester. package:test timeouts fire internally but the
// Flutter process keeps running regardless.
unawaited(
Process.run(
'pkill',
['-KILL', '-f', 'flutter_tester'],
runInShell: true,
),
);
return CallToolResult(
content: [
TextContent(
text:
'Test run timed out after ${timeoutSeconds}s. '
'A Flutter test was hanging — likely an unbounded '
'pumpAndSettle() call (default timeout is 10 minutes). '
'The flutter_tester process was killed.\n'
'Fix: replace tester.pumpAndSettle() with '
'tester.pump(Duration(milliseconds: 500)) or '
'tester.pumpAndSettle(timeout: Duration(seconds: 5)).',
),
],
isError: true,
);
}
}

Future<CallToolResult> _handlePackagesGet(CallToolRequest request) async {
Expand Down
38 changes: 38 additions & 0 deletions test/src/mcp/mcp_server_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,44 @@ void main() {
contains('"test" failed with exit code'),
);
});

test(
'returns timeout error and kills flutter_tester when test hangs',
() async {
// Simulate a hung test — the future never completes.
final hangCompleter = Completer<int>();
when(
() => mockCommandRunner.run(any()),
).thenAnswer((_) => hangCompleter.future);

final response = await sendRequest(
CallToolRequest.methodName,
_params(
CallToolRequest(
name: 'test',
arguments: {'timeout_seconds': 1},
),
),
);

expect(response['error'], isNull);
final result = CallToolResult.fromMap(
response['result'] as Map<String, Object?>,
);
expect(result.isError, isTrue);
expect(
(result.content.first as TextContent).text,
allOf([
contains('timed out after 1s'),
contains('pumpAndSettle'),
]),
);

// Resolve the dangling future so it doesn't outlive the test.
hangCompleter.complete(0);
},
timeout: const Timeout(Duration(seconds: 5)),
);
});

group('Tool: packages_get', () {
Expand Down
Loading