Skip to content

Commit 88d7a4d

Browse files
authored
Flutter driver updates, add screenshot support back (#241)
- All the flutter driver arguments are actually supposed to be string values so this fixes those schemas - Change the widget tree to not be the summary by default - summary trees hide nested text widgets which makes it hard for the LLM to find things. This can be explicitly toggled on though. - Added support for the flutter driver screenshot command, which works on simulators 🥳 - Rework tests a bit so there is a helper for normal tool calls without retries - Removed the `get_layer_tree` and `get_render_tree` flutter driver commands for now - I don't see these ever being used.
1 parent 40607dc commit 88d7a4d

File tree

8 files changed

+141
-112
lines changed

8 files changed

+141
-112
lines changed

pkgs/dart_mcp_server/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* Add a flutter_driver command for executing flutter driver commands on a device.
88
* Allow for multiple package arguments to `pub add` and `pub remove`.
99
* Require dart_mcp version 0.3.1.
10+
* Add support for the flutter_driver screenshot command.
11+
* Change the widget tree to the full version instead of the summary. The summary
12+
tends to hide nested text widgets which makes it difficult to find widgets
13+
based on their text values.
1014

1115
# 0.1.0 (Dart SDK 3.9.0)
1216

pkgs/dart_mcp_server/lib/src/mixins/dtd.dart

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ base mixin DartToolingDaemonSupport
181181
return _flutterDriverNotRegistered;
182182
}
183183
final vm = await vmService.getVM();
184+
final timeout = request.arguments?['timeout'] as String?;
185+
final isScreenshot = request.arguments?['command'] == 'screenshot';
186+
if (isScreenshot) {
187+
request.arguments?.putIfAbsent('format', () => '4' /*png*/);
188+
}
184189
final result = await vmService
185190
.callServiceExtension(
186191
_flutterDriverService,
@@ -189,17 +194,27 @@ base mixin DartToolingDaemonSupport
189194
)
190195
.timeout(
191196
Duration(
192-
milliseconds:
193-
(request.arguments?['timeout'] as int?) ??
194-
_defaultTimeoutMs,
197+
milliseconds: timeout != null
198+
? int.parse(timeout)
199+
: _defaultTimeoutMs,
195200
),
196201
onTimeout: () => Response.parse({
197202
'isError': true,
198203
'error': 'Timed out waiting for Flutter Driver response.',
199204
})!,
200205
);
201206
return CallToolResult(
202-
content: [Content.text(text: jsonEncode(result.json))],
207+
content: [
208+
isScreenshot && result.json?['isError'] == false
209+
? Content.image(
210+
data:
211+
(result.json!['response']
212+
as Map<String, Object?>)['data']
213+
as String,
214+
mimeType: 'image/png',
215+
)
216+
: Content.text(text: jsonEncode(result.json)),
217+
],
203218
isError: result.json?['isError'] as bool?,
204219
);
205220
},
@@ -461,15 +476,14 @@ base mixin DartToolingDaemonSupport
461476
callback: (vmService) async {
462477
final vm = await vmService.getVM();
463478
final isolateId = vm.isolates!.first.id;
479+
final summaryOnly = request.arguments?['summaryOnly'] as bool? ?? false;
464480
try {
465481
final result = await vmService.callServiceExtension(
466482
'$_inspectorServiceExtensionPrefix.getRootWidgetTree',
467483
isolateId: isolateId,
468484
args: {
469485
'groupName': inspectorObjectGroup,
470-
// TODO: consider making these configurable or using defaults that
471-
// are better for the LLM.
472-
'isSummaryTree': 'true',
486+
'isSummaryTree': summaryOnly ? 'true' : 'false',
473487
'withPreviews': 'true',
474488
'fullDetails': 'false',
475489
},
@@ -647,19 +661,19 @@ base mixin DartToolingDaemonSupport
647661
inputSchema: Schema.object(
648662
additionalProperties: true,
649663
description:
650-
'The flutter driver command to run. Command arguments should be '
651-
'passed as additional properties to this map.\n\nWhen searching for '
652-
'widgets, you should first inspect the widget tree in order to '
653-
'figure out how to find the widget instead of just guessing tooltip '
654-
'text or other things.',
664+
'Command arguments are passed as additional properties to this map.'
665+
'To specify a widgets, you should first use the '
666+
'"${getWidgetTreeTool.name}" tool to inspect the widget tree for the '
667+
'value id of the widget and then use the "ByValueKey" finder type '
668+
'with that id.',
655669
properties: {
656670
'command': Schema.string(
657671
// Commented out values are flutter_driver commands that are not
658672
// supported, but may be in the future.
659673
enumValues: [
660674
'get_health',
661-
'get_layer_tree',
662-
'get_render_tree',
675+
// 'get_layer_tree',
676+
// 'get_render_tree',
663677
'enter_text',
664678
'send_text_input_action',
665679
'get_text',
@@ -680,39 +694,40 @@ base mixin DartToolingDaemonSupport
680694
// 'get_semantics_id',
681695
'get_offset',
682696
'get_diagnostics_tree',
683-
// 'screenshot',
697+
'screenshot',
684698
],
685699
description: 'The name of the driver command',
686700
),
687-
'alignment': Schema.num(
701+
'alignment': Schema.string(
688702
description:
689-
'How the widget should be aligned. '
690-
'Required for the scrollIntoView command',
703+
'Required for the scrollIntoView command, how the widget should '
704+
'be aligned',
691705
),
692-
'duration': Schema.int(
706+
'duration': Schema.string(
693707
description:
694-
'The duration of the scrolling action in microseconds. '
695-
'Required for the scroll command',
708+
'Required for the scroll command, the duration of the '
709+
'scrolling action in microseconds as a stringified integer.',
696710
),
697-
'dx': Schema.int(
711+
'dx': Schema.string(
698712
description:
699-
'Delta X offset for move event. Required for the scroll command',
713+
'Required for the scroll command, the delta X offset for move '
714+
'event as a stringified double',
700715
),
701-
'dy': Schema.int(
716+
'dy': Schema.string(
702717
description:
703-
'Delta Y offset for move event. Required for the scroll command',
718+
'Required for the scroll command, the delta Y offset for move '
719+
'event as a stringified double',
704720
),
705-
'frequency': Schema.int(
721+
'frequency': Schema.string(
706722
description:
707-
'The frequency in Hz of the generated move events. '
708-
'Required for the scroll command',
723+
'Required for the scroll command, the frequency in Hz of the '
724+
'generated move events as a stringified integer',
709725
),
710726
'finderType': Schema.string(
711727
description:
712-
'The kind of finder to use, if required for the command. '
713728
'Required for get_text, scroll, scroll_into_view, tap, waitFor, '
714729
'waitForAbsent, waitForTappable, get_offset, and '
715-
'get_diagnostics_tree',
730+
'get_diagnostics_tree. The kind of finder to use.',
716731
enumValues: [
717732
'ByType',
718733
'ByValueKey',
@@ -733,10 +748,11 @@ base mixin DartToolingDaemonSupport
733748
description:
734749
'Required for the ByValueKey finder, the type of the key',
735750
),
736-
'isRegExp': Schema.bool(
751+
'isRegExp': Schema.string(
737752
description:
738753
'Used by the BySemanticsLabel finder, indicates whether '
739754
'the value should be treated as a regex',
755+
enumValues: ['true', 'false'],
740756
),
741757
'label': Schema.string(
742758
description:
@@ -745,8 +761,8 @@ base mixin DartToolingDaemonSupport
745761
),
746762
'text': Schema.string(
747763
description:
748-
'The relevant text for the command. Required for the ByText and '
749-
'ByTooltipMessage finders, as well as the enter_text command.',
764+
'Required for the ByText and ByTooltipMessage finders, as well '
765+
'as the enter_text command. The relevant text for the command',
750766
),
751767
'type': Schema.string(
752768
description:
@@ -807,9 +823,7 @@ base mixin DartToolingDaemonSupport
807823
'complete. Defaults to $_defaultTimeoutMs.',
808824
),
809825
'offsetType': Schema.string(
810-
description:
811-
'Offset types that can be requested by get_offset. '
812-
'Required for get_offset.',
826+
description: 'Required for get_offset, the offset type to get',
813827
enumValues: [
814828
'topLeft',
815829
'topRight',
@@ -820,22 +834,26 @@ base mixin DartToolingDaemonSupport
820834
),
821835
'diagnosticsType': Schema.string(
822836
description:
823-
'The type of diagnostics tree to request. '
824-
'Required for get_diagnostics_tree',
837+
'Required for get_diagnostics_tree, the type of diagnostics tree '
838+
'to request',
825839
enumValues: ['renderObject', 'widget'],
826840
),
827-
'subtreeDepth': Schema.int(
841+
'subtreeDepth': Schema.string(
828842
description:
829-
'How many levels of children to include in the result. '
830-
'Required for get_diagnostics_tree',
843+
'Required for get_diagnostics_tree, how many levels of children '
844+
'to include in the result, as a stringified integer',
831845
),
832-
'includeProperties': Schema.bool(
846+
'includeProperties': Schema.string(
833847
description:
834848
'Whether the properties of a diagnostics node should be included '
835849
'in get_diagnostics_tree results',
850+
enumValues: const ['true', 'false'],
836851
),
837-
'enabled': Schema.bool(
838-
description: 'Used by set_text_entry_emulation, defaults to false',
852+
'enabled': Schema.string(
853+
description:
854+
'Used by set_text_entry_emulation, defaults to '
855+
'false',
856+
enumValues: const ['true', 'false'],
839857
),
840858
},
841859
required: ['command'],
@@ -920,7 +938,15 @@ base mixin DartToolingDaemonSupport
920938
'Retrieves the widget tree from the active Flutter application. '
921939
'Requires "${connectTool.name}" to be successfully called first.',
922940
annotations: ToolAnnotations(title: 'Get widget tree', readOnlyHint: true),
923-
inputSchema: Schema.object(),
941+
inputSchema: Schema.object(
942+
properties: {
943+
'summaryOnly': Schema.bool(
944+
description:
945+
'Defaults to false. If true, only widgets created by user code '
946+
'are returned.',
947+
),
948+
},
949+
),
924950
);
925951

926952
@visibleForTesting

pkgs/dart_mcp_server/test/test_harness.dart

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,27 +163,38 @@ class TestHarness {
163163
expect(result.isError, isNot(true), reason: result.content.join('\n'));
164164
}
165165

166+
/// Helper to send [request] to [mcpServerConnection].
167+
///
168+
/// Some methods will fail if the DTD connection is not yet ready.
169+
Future<CallToolResult> callTool(
170+
CallToolRequest request, {
171+
bool expectError = false,
172+
}) async {
173+
final result = await mcpServerConnection.callTool(request);
174+
expect(
175+
result.isError,
176+
expectError ? true : isNot(true),
177+
reason: result.content.join('\n'),
178+
);
179+
return result;
180+
}
181+
166182
/// Sends [request] to [mcpServerConnection], retrying [maxTries] times.
167183
///
168184
/// Some methods will fail if the DTD connection is not yet ready.
169185
Future<CallToolResult> callToolWithRetry(
170186
CallToolRequest request, {
171187
int maxTries = 5,
172-
bool expectError = false,
173188
}) async {
174189
var tryCount = 0;
175-
late CallToolResult lastResult;
176-
while (tryCount++ < maxTries) {
177-
lastResult = await mcpServerConnection.callTool(request);
178-
if (lastResult.isError != true) return lastResult;
190+
while (true) {
191+
try {
192+
return await callTool(request);
193+
} catch (_) {
194+
if (tryCount++ >= maxTries) rethrow;
195+
}
179196
await Future<void>.delayed(Duration(milliseconds: 100 * tryCount));
180197
}
181-
expect(
182-
lastResult.isError,
183-
expectError ? true : isNot(true),
184-
reason: lastResult.content.join('\n'),
185-
);
186-
return lastResult;
187198
}
188199
}
189200

pkgs/dart_mcp_server/test/tools/analyzer_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ void printIt({required int x}) {
188188
});
189189

190190
test('cannot analyze without roots set', () async {
191-
final result = await testHarness.callToolWithRetry(
191+
final result = await testHarness.callTool(
192192
CallToolRequest(name: DartAnalyzerSupport.analyzeFilesTool.name),
193193
expectError: true,
194194
);
@@ -203,7 +203,7 @@ void printIt({required int x}) {
203203
});
204204

205205
test('cannot look up symbols without roots set', () async {
206-
final result = await testHarness.callToolWithRetry(
206+
final result = await testHarness.callTool(
207207
CallToolRequest(
208208
name: DartAnalyzerSupport.resolveWorkspaceSymbolTool.name,
209209
arguments: {ParameterNames.query: 'DartAnalyzerSupport'},
@@ -221,7 +221,7 @@ void printIt({required int x}) {
221221
});
222222

223223
test('cannot get hover information without roots set', () async {
224-
final result = await testHarness.callToolWithRetry(
224+
final result = await testHarness.callTool(
225225
CallToolRequest(
226226
name: DartAnalyzerSupport.hoverTool.name,
227227
arguments: {

pkgs/dart_mcp_server/test/tools/dart_cli_test.dart

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,7 @@ dependencies:
344344
ParameterNames.platform: ['atari_jaguar', 'web'], // One invalid
345345
},
346346
);
347-
final result = await testHarness.callToolWithRetry(
348-
request,
349-
expectError: true,
350-
);
347+
final result = await testHarness.callTool(request, expectError: true);
351348

352349
expect(result.isError, isTrue);
353350
expect(
@@ -372,10 +369,7 @@ dependencies:
372369
ParameterNames.directory: 'my_app_no_type',
373370
},
374371
);
375-
final result = await testHarness.callToolWithRetry(
376-
request,
377-
expectError: true,
378-
);
372+
final result = await testHarness.callTool(request, expectError: true);
379373

380374
expect(result.isError, isTrue);
381375
expect(
@@ -395,10 +389,7 @@ dependencies:
395389
ParameterNames.projectType: 'java', // Invalid type
396390
},
397391
);
398-
final result = await testHarness.callToolWithRetry(
399-
request,
400-
expectError: true,
401-
);
392+
final result = await testHarness.callTool(request, expectError: true);
402393

403394
expect(result.isError, isTrue);
404395
expect(
@@ -418,10 +409,7 @@ dependencies:
418409
ParameterNames.projectType: 'dart',
419410
},
420411
);
421-
final result = await testHarness.callToolWithRetry(
422-
request,
423-
expectError: true,
424-
);
412+
final result = await testHarness.callTool(request, expectError: true);
425413

426414
expect(result.isError, isTrue);
427415
expect(
@@ -441,10 +429,7 @@ dependencies:
441429
ParameterNames.template: 'cli',
442430
},
443431
);
444-
final result = await testHarness.callToolWithRetry(
445-
request,
446-
expectError: true,
447-
);
432+
final result = await testHarness.callTool(request, expectError: true);
448433

449434
expect(result.isError, true);
450435
expect(

0 commit comments

Comments
 (0)