Skip to content

[dart_tooling_mcp_server] add tool for getting the selected widget #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2025
Merged
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
71 changes: 62 additions & 9 deletions pkgs/dart_tooling_mcp_server/lib/src/mixins/dtd.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';

import 'package:dart_mcp/server.dart';
import 'package:dds_service_extensions/dds_service_extensions.dart';
Expand Down Expand Up @@ -39,11 +40,25 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
@visibleForTesting
static bool debugAwaitVmServiceDisposal = false;

/// The id for the object group used when calling Flutter Widget
/// Inspector service extensions from DTD tools.
@visibleForTesting
static const inspectorObjectGroup = 'dart-tooling-mcp-server';

/// The prefix for Flutter Widget Inspector service extensions.
///
/// See https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/service_extensions.dart#L126
/// for full list of available Flutter Widget Inspector service extensions.
static const _inspectorServiceExtensionPrefix = 'ext.flutter.inspector';

/// Called when the DTD connection is lost, resets all associated state.
Future<void> _resetDtd() async {
_dtd = null;
_getDebugSessionsReady = false;

// TODO: determine whether we need to dispose the [inspectorObjectGroup] on
// the Flutter Widget Inspector for each VM service instance.

final future = Future.wait(
activeVmServices.values.map((vmService) => vmService.dispose()),
);
Expand Down Expand Up @@ -94,6 +109,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
registerTool(screenshotTool, takeScreenshot);
registerTool(hotReloadTool, hotReload);
registerTool(getWidgetTreeTool, widgetTree);
registerTool(getSelectedWidgetTool, selectedWidget);

return super.initialize(request);
}
Expand Down Expand Up @@ -357,14 +373,12 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
callback: (vmService) async {
final vm = await vmService.getVM();
final isolateId = vm.isolates!.first.id;
final groupId = 'dart-tooling-mcp-server';
const inspectorExtensionPrefix = 'ext.flutter.inspector';
try {
final result = await vmService.callServiceExtension(
'$inspectorExtensionPrefix.getRootWidgetTree',
'$_inspectorServiceExtensionPrefix.getRootWidgetTree',
isolateId: isolateId,
args: {
'groupName': groupId,
'groupName': inspectorObjectGroup,
// TODO: consider making these configurable or using defaults that
// are better for the LLM.
'isSummaryTree': 'true',
Expand All @@ -384,7 +398,7 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
],
);
}
return CallToolResult(content: [TextContent(text: tree.toString())]);
return CallToolResult(content: [TextContent(text: jsonEncode(tree))]);
} catch (e) {
return CallToolResult(
isError: true,
Expand All @@ -394,11 +408,41 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
),
],
);
} finally {
await vmService.callServiceExtension(
'$inspectorExtensionPrefix.disposeGroup',
}
},
);
}

/// Retrieves the selected widget from the currently running app.
///
/// If more than one debug session is active, then it just uses the first one.
// TODO: support passing a debug session id when there is more than one debug
// session.
Future<CallToolResult> selectedWidget(CallToolRequest request) async {
return _callOnVmService(
callback: (vmService) async {
final vm = await vmService.getVM();
final isolateId = vm.isolates!.first.id;
try {
final result = await vmService.callServiceExtension(
'$_inspectorServiceExtensionPrefix.getSelectedSummaryWidget',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this determine what things are "user code"? Do we need to call addPubRootDirectories? Or do we assume something else is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dart Code extension / Flutter IntelliJ plugin already do this today. Otherwise we assume the pub root directories is the root of the running app. CC @elliette to confirm this is how things still work today.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that matches my understanding. Though it looks like the call to addPubRootDirectories was removed from the IntelliJ plugin: flutter/flutter-intellij@1c1389f

FYI @pq @jwren this is another piece of code that might need to be added back

isolateId: isolateId,
args: {'objectGroup': groupId},
args: {'objectGroup': inspectorObjectGroup},
);

final widget = result.json?['result'];
if (widget == null) {
return CallToolResult(
content: [TextContent(text: 'No Widget selected.')],
);
}
return CallToolResult(
content: [TextContent(text: jsonEncode(widget))],
);
} catch (e) {
return CallToolResult(
isError: true,
content: [TextContent(text: 'Failed to get selected widget: $e')],
);
}
},
Expand Down Expand Up @@ -477,6 +521,15 @@ base mixin DartToolingDaemonSupport on ToolsSupport {
inputSchema: ObjectSchema(),
);

@visibleForTesting
static final getSelectedWidgetTool = Tool(
name: 'get_selected_widget',
description:
'Retrieves the selected widget from the active Flutter application. '
'Requires "${connectTool.name}" to be successfully called first.',
inputSchema: ObjectSchema(),
);

static final _dtdNotConnected = CallToolResult(
isError: true,
content: [
Expand Down
Loading
Loading