3
3
// BSD-style license that can be found in the LICENSE file.
4
4
5
5
import 'dart:async' ;
6
+ import 'dart:convert' ;
6
7
7
8
import 'package:dart_mcp/server.dart' ;
8
9
import 'package:dds_service_extensions/dds_service_extensions.dart' ;
@@ -39,11 +40,25 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
39
40
@visibleForTesting
40
41
static bool debugAwaitVmServiceDisposal = false ;
41
42
43
+ /// The id for the object group used when calling Flutter Widget
44
+ /// Inspector service extensions from DTD tools.
45
+ @visibleForTesting
46
+ static const inspectorObjectGroup = 'dart-tooling-mcp-server' ;
47
+
48
+ /// The prefix for Flutter Widget Inspector service extensions.
49
+ ///
50
+ /// See https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/service_extensions.dart#L126
51
+ /// for full list of available Flutter Widget Inspector service extensions.
52
+ static const _inspectorServiceExtensionPrefix = 'ext.flutter.inspector' ;
53
+
42
54
/// Called when the DTD connection is lost, resets all associated state.
43
55
Future <void > _resetDtd () async {
44
56
_dtd = null ;
45
57
_getDebugSessionsReady = false ;
46
58
59
+ // TODO: determine whether we need to dispose the [inspectorObjectGroup] on
60
+ // the Flutter Widget Inspector for each VM service instance.
61
+
47
62
final future = Future .wait (
48
63
activeVmServices.values.map ((vmService) => vmService.dispose ()),
49
64
);
@@ -94,6 +109,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
94
109
registerTool (screenshotTool, takeScreenshot);
95
110
registerTool (hotReloadTool, hotReload);
96
111
registerTool (getWidgetTreeTool, widgetTree);
112
+ registerTool (getSelectedWidgetTool, selectedWidget);
97
113
98
114
return super .initialize (request);
99
115
}
@@ -357,14 +373,12 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
357
373
callback: (vmService) async {
358
374
final vm = await vmService.getVM ();
359
375
final isolateId = vm.isolates! .first.id;
360
- final groupId = 'dart-tooling-mcp-server' ;
361
- const inspectorExtensionPrefix = 'ext.flutter.inspector' ;
362
376
try {
363
377
final result = await vmService.callServiceExtension (
364
- '$inspectorExtensionPrefix .getRootWidgetTree' ,
378
+ '$_inspectorServiceExtensionPrefix .getRootWidgetTree' ,
365
379
isolateId: isolateId,
366
380
args: {
367
- 'groupName' : groupId ,
381
+ 'groupName' : inspectorObjectGroup ,
368
382
// TODO: consider making these configurable or using defaults that
369
383
// are better for the LLM.
370
384
'isSummaryTree' : 'true' ,
@@ -384,7 +398,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
384
398
],
385
399
);
386
400
}
387
- return CallToolResult (content: [TextContent (text: tree. toString ( ))]);
401
+ return CallToolResult (content: [TextContent (text: jsonEncode (tree ))]);
388
402
} catch (e) {
389
403
return CallToolResult (
390
404
isError: true ,
@@ -394,11 +408,41 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
394
408
),
395
409
],
396
410
);
397
- } finally {
398
- await vmService.callServiceExtension (
399
- '$inspectorExtensionPrefix .disposeGroup' ,
411
+ }
412
+ },
413
+ );
414
+ }
415
+
416
+ /// Retrieves the selected widget from the currently running app.
417
+ ///
418
+ /// If more than one debug session is active, then it just uses the first one.
419
+ // TODO: support passing a debug session id when there is more than one debug
420
+ // session.
421
+ Future <CallToolResult > selectedWidget (CallToolRequest request) async {
422
+ return _callOnVmService (
423
+ callback: (vmService) async {
424
+ final vm = await vmService.getVM ();
425
+ final isolateId = vm.isolates! .first.id;
426
+ try {
427
+ final result = await vmService.callServiceExtension (
428
+ '$_inspectorServiceExtensionPrefix .getSelectedSummaryWidget' ,
400
429
isolateId: isolateId,
401
- args: {'objectGroup' : groupId},
430
+ args: {'objectGroup' : inspectorObjectGroup},
431
+ );
432
+
433
+ final widget = result.json? ['result' ];
434
+ if (widget == null ) {
435
+ return CallToolResult (
436
+ content: [TextContent (text: 'No Widget selected.' )],
437
+ );
438
+ }
439
+ return CallToolResult (
440
+ content: [TextContent (text: jsonEncode (widget))],
441
+ );
442
+ } catch (e) {
443
+ return CallToolResult (
444
+ isError: true ,
445
+ content: [TextContent (text: 'Failed to get selected widget: $e ' )],
402
446
);
403
447
}
404
448
},
@@ -477,6 +521,15 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
477
521
inputSchema: ObjectSchema (),
478
522
);
479
523
524
+ @visibleForTesting
525
+ static final getSelectedWidgetTool = Tool (
526
+ name: 'get_selected_widget' ,
527
+ description:
528
+ 'Retrieves the selected widget from the active Flutter application. '
529
+ 'Requires "${connectTool .name }" to be successfully called first.' ,
530
+ inputSchema: ObjectSchema (),
531
+ );
532
+
480
533
static final _dtdNotConnected = CallToolResult (
481
534
isError: true ,
482
535
content: [
0 commit comments