Skip to content

Commit 7e07e62

Browse files
[dart_tooling_mcp_server] add tool for getting the selected widget (#71)
1 parent bc0bd0a commit 7e07e62

File tree

2 files changed

+286
-148
lines changed

2 files changed

+286
-148
lines changed

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

+62-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

78
import 'package:dart_mcp/server.dart';
89
import 'package:dds_service_extensions/dds_service_extensions.dart';
@@ -39,11 +40,25 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
3940
@visibleForTesting
4041
static bool debugAwaitVmServiceDisposal = false;
4142

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+
4254
/// Called when the DTD connection is lost, resets all associated state.
4355
Future<void> _resetDtd() async {
4456
_dtd = null;
4557
_getDebugSessionsReady = false;
4658

59+
// TODO: determine whether we need to dispose the [inspectorObjectGroup] on
60+
// the Flutter Widget Inspector for each VM service instance.
61+
4762
final future = Future.wait(
4863
activeVmServices.values.map((vmService) => vmService.dispose()),
4964
);
@@ -94,6 +109,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
94109
registerTool(screenshotTool, takeScreenshot);
95110
registerTool(hotReloadTool, hotReload);
96111
registerTool(getWidgetTreeTool, widgetTree);
112+
registerTool(getSelectedWidgetTool, selectedWidget);
97113

98114
return super.initialize(request);
99115
}
@@ -357,14 +373,12 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
357373
callback: (vmService) async {
358374
final vm = await vmService.getVM();
359375
final isolateId = vm.isolates!.first.id;
360-
final groupId = 'dart-tooling-mcp-server';
361-
const inspectorExtensionPrefix = 'ext.flutter.inspector';
362376
try {
363377
final result = await vmService.callServiceExtension(
364-
'$inspectorExtensionPrefix.getRootWidgetTree',
378+
'$_inspectorServiceExtensionPrefix.getRootWidgetTree',
365379
isolateId: isolateId,
366380
args: {
367-
'groupName': groupId,
381+
'groupName': inspectorObjectGroup,
368382
// TODO: consider making these configurable or using defaults that
369383
// are better for the LLM.
370384
'isSummaryTree': 'true',
@@ -384,7 +398,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
384398
],
385399
);
386400
}
387-
return CallToolResult(content: [TextContent(text: tree.toString())]);
401+
return CallToolResult(content: [TextContent(text: jsonEncode(tree))]);
388402
} catch (e) {
389403
return CallToolResult(
390404
isError: true,
@@ -394,11 +408,41 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
394408
),
395409
],
396410
);
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',
400429
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')],
402446
);
403447
}
404448
},
@@ -477,6 +521,15 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
477521
inputSchema: ObjectSchema(),
478522
);
479523

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+
480533
static final _dtdNotConnected = CallToolResult(
481534
isError: true,
482535
content: [

0 commit comments

Comments
 (0)