Skip to content

Adds a check to pub tool to see if a root is a Flutter project. #114

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

Closed
wants to merge 2 commits into from
Closed
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
3 changes: 3 additions & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 0.2.1-wip

- Adds a check in the pub tool that checks to see if a project is a Flutter
project before it calls the pub tool. If it is, then it automatically runs
'flutter pub' instead of 'dart pub'.
- Update workflow example to show thinking spinner and input and output token
usage.

Expand Down
2 changes: 1 addition & 1 deletion pkgs/dart_tooling_mcp_server/lib/src/mixins/analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'dart:io' show Platform, Process;

import 'package:dart_mcp/server.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
Expand Down
5 changes: 4 additions & 1 deletion pkgs/dart_tooling_mcp_server/lib/src/mixins/dart_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:dart_mcp/server.dart';

import '../utils/cli_utils.dart';
import '../utils/constants.dart';
import '../utils/filesystem.dart';
import '../utils/process_manager.dart';

// TODO: migrate the analyze files tool to use this mixin and run the
Expand All @@ -19,7 +20,7 @@ import '../utils/process_manager.dart';
/// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
/// mixins applied.
base mixin DartCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
implements ProcessManagerSupport {
implements ProcessManagerSupport, FileSystemSupport {
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
try {
Expand All @@ -41,6 +42,7 @@ base mixin DartCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
commandDescription: 'dart fix',
processManager: processManager,
knownRoots: await roots,
fileSystem: fs,
);
}

Expand All @@ -53,6 +55,7 @@ base mixin DartCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
processManager: processManager,
defaultPaths: ['.'],
knownRoots: await roots,
fileSystem: fs,
);
}

Expand Down
40 changes: 35 additions & 5 deletions pkgs/dart_tooling_mcp_server/lib/src/mixins/pub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import 'dart:async';

import 'package:dart_mcp/server.dart';
import 'package:file/file.dart';
import 'package:yaml/yaml.dart';

import '../utils/cli_utils.dart';
import '../utils/constants.dart';
import '../utils/filesystem.dart';
import '../utils/process_manager.dart';

/// Mix this in to any MCPServer to add support for running Pub commands like
Expand All @@ -18,7 +21,7 @@ import '../utils/process_manager.dart';
/// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
/// mixins applied.
base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
implements ProcessManagerSupport {
implements ProcessManagerSupport, FileSystemSupport {
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
try {
Expand Down Expand Up @@ -69,17 +72,44 @@ base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
);
}

List<String> builder(Uri rootUri) {
return <String>[
_isFlutterPackage(rootUri) ? 'flutter' : 'dart',
'pub',
command,
if (packageName != null) packageName,
];
}

return runCommandInRoots(
request,
// TODO(https://github.com/dart-lang/ai/issues/81): conditionally use
// flutter when appropriate.
command: ['dart', 'pub', command, if (packageName != null) packageName],
commandBuilder: builder,
commandDescription: 'dart pub $command',
processManager: processManager,
knownRoots: await roots,
fileSystem: fs,
);
}

bool _isFlutterPackage(Uri rootUri) {
final projectRoot = fs.directory(rootUri.toFilePath());
final pubspec = fs.file(projectRoot.childFile('pubspec.yaml'));
if (pubspec.existsSync() &&
pubspec.statSync().type == FileSystemEntityType.file) {
try {
final pubspecYaml = loadYaml(pubspec.readAsStringSync())! as YamlMap;
if (pubspecYaml['dependencies'] != null &&
(pubspecYaml['dependencies']! as YamlMap)['flutter'] != null) {
return true;
}
} catch (e) {
// If the file is malformed, just assume it's not a flutter package.
return false;
}
}
return false;
}

static final pubTool = Tool(
name: 'pub',
description:
Expand Down Expand Up @@ -151,7 +181,7 @@ enum SupportedPubCommand {
if (i < commands.length - 2) {
buffer.write(', ');
} else if (i == commands.length - 2) {
buffer.write(' and ');
buffer.write(', and ');
}
}
return buffer.toString();
Expand Down
9 changes: 8 additions & 1 deletion pkgs/dart_tooling_mcp_server/lib/src/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import 'dart:async';

import 'package:dart_mcp/server.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import 'package:stream_channel/stream_channel.dart';
Expand All @@ -13,6 +15,7 @@ import 'mixins/analyzer.dart';
import 'mixins/dart_cli.dart';
import 'mixins/dtd.dart';
import 'mixins/pub.dart';
import 'utils/filesystem.dart';
import 'utils/process_manager.dart';

/// An MCP server for Dart and Flutter tooling.
Expand All @@ -26,10 +29,11 @@ final class DartToolingMCPServer extends MCPServer
DartCliSupport,
PubSupport,
DartToolingDaemonSupport
implements ProcessManagerSupport {
implements ProcessManagerSupport, FileSystemSupport {
DartToolingMCPServer(
super.channel, {
@visibleForTesting this.processManager = const LocalProcessManager(),
@visibleForTesting this.fs = const LocalFileSystem(),
}) : super.fromStreamChannel(
implementation: ServerImplementation(
name: 'dart and flutter tooling',
Expand All @@ -48,4 +52,7 @@ final class DartToolingMCPServer extends MCPServer

@override
final LocalProcessManager processManager;

@override
final FileSystem fs;
}
18 changes: 13 additions & 5 deletions pkgs/dart_tooling_mcp_server/lib/src/utils/cli_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io';

import 'package:dart_mcp/server.dart';
import 'package:file/file.dart';
import 'package:path/path.dart' as p;
import 'package:process/process.dart';

import 'constants.dart';

typedef CommandBuilder = List<String> Function(Uri rootUri);

/// Runs [command] in each of the project roots specified in the [request].
///
/// The [command] should be a list of strings that can be passed directly to
Expand All @@ -33,12 +34,18 @@ import 'constants.dart';
/// root's 'paths'.
Future<CallToolResult> runCommandInRoots(
CallToolRequest request, {
required List<String> command,
List<String>? command,
CommandBuilder? commandBuilder,
required String commandDescription,
required ProcessManager processManager,
required List<Root> knownRoots,
List<String> defaultPaths = const <String>[],
required FileSystem fileSystem,
}) async {
assert(
command != null || commandBuilder != null,
'One of command or commandBuilder must be specified.',
);
var rootConfigs =
(request.arguments?[ParameterNames.roots] as List?)
?.cast<Map<String, Object?>>();
Expand Down Expand Up @@ -89,9 +96,10 @@ Future<CallToolResult> runCommandInRoots(
isError: true,
);
}
final projectRoot = Directory(rootUri.toFilePath());
final projectRoot = fileSystem.directory(rootUri.toFilePath());

final commandWithPaths = List.of(command);
final commandWithPaths =
command != null ? List.of(command) : commandBuilder!(rootUri);
final paths =
(rootConfig[ParameterNames.paths] as List?)?.cast<String>() ??
defaultPaths;
Expand Down
17 changes: 17 additions & 0 deletions pkgs/dart_tooling_mcp_server/lib/src/utils/filesystem.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:file/file.dart';

/// An interface class that provides a single getter of type [FileSystem].
///
/// The `DartToolingMCPServer` class implements this class so that [File]
/// methods can be easily mocked during testing.
///
/// MCP support mixins like `DartCliSupport` that access files should also
/// implement this class and use [fs] instead of making direct calls to
/// dart:io's [File] and [Directory] classes.
abstract interface class FileSystemSupport {
FileSystem get fs;
}
2 changes: 2 additions & 0 deletions pkgs/dart_tooling_mcp_server/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
dds_service_extensions: ^2.0.1
devtools_shared: ^11.2.0
dtd: ^2.4.0
file: ^7.0.1
json_rpc_2: ^3.0.3
# TODO: Get this another way.
language_server_protocol:
Expand All @@ -33,6 +34,7 @@ dependencies:
test_process: ^2.1.1
vm_service: ^15.0.0
watcher: ^1.1.1
yaml: ^3.1.3

dev_dependencies:
dart_flutter_team_lints: ^3.2.1
Expand Down
31 changes: 25 additions & 6 deletions pkgs/dart_tooling_mcp_server/test/test_harness.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import 'package:dart_tooling_mcp_server/src/mixins/dtd.dart';
import 'package:dart_tooling_mcp_server/src/server.dart';
import 'package:dart_tooling_mcp_server/src/utils/constants.dart';
import 'package:dtd/dtd.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:path/path.dart' as p;
import 'package:process/process.dart';
import 'package:stream_channel/stream_channel.dart';
Expand All @@ -32,13 +34,16 @@ class TestHarness {
final FakeEditorExtension fakeEditorExtension;
final DartToolingMCPClient mcpClient;
final ServerConnectionPair serverConnectionPair;
final FileSystem fs;

ServerConnection get mcpServerConnection =>
serverConnectionPair.serverConnection;

TestHarness._(
this.mcpClient,
this.serverConnectionPair,
this.fakeEditorExtension,
this.fs,
);

/// Starts a Dart Tooling Daemon as well as an MCP client and server, and
Expand All @@ -56,13 +61,19 @@ class TestHarness {
/// MCP server is ran in process.
///
/// Use [startDebugSession] to start up apps and connect to them.
static Future<TestHarness> start({bool inProcess = false}) async {
static Future<TestHarness> start({
bool inProcess = false,
FileSystem? fs,
}) async {
final fileSystem = fs ?? const LocalFileSystem();

final mcpClient = DartToolingMCPClient();
addTearDown(mcpClient.shutdown);

final serverConnectionPair = await _initializeMCPServer(
mcpClient,
inProcess,
fileSystem,
);
final connection = serverConnectionPair.serverConnection;
connection.onLog.listen((log) {
Expand All @@ -72,7 +83,12 @@ class TestHarness {
final fakeEditorExtension = await FakeEditorExtension.connect();
addTearDown(fakeEditorExtension.shutdown);

return TestHarness._(mcpClient, serverConnectionPair, fakeEditorExtension);
return TestHarness._(
mcpClient,
serverConnectionPair,
fakeEditorExtension,
fileSystem,
);
}

/// Starts an app debug session.
Expand All @@ -89,6 +105,7 @@ class TestHarness {
args: args,
);
await fakeEditorExtension.addDebugSession(session);

final root = rootForPath(projectRoot);
final roots = (await mcpClient.handleListRoots(ListRootsRequest())).roots;
if (!roots.any((r) => r.uri == root.uri)) {
Expand All @@ -97,6 +114,10 @@ class TestHarness {
return session;
}

/// Creates a canonical [Root] object for a given [projectPath].
Root rootForPath(String projectPath) =>
Root(uri: fs.directory(projectPath).absolute.uri.toString());

/// Stops an app debug session.
Future<void> stopDebugSession(AppDebugSession session) async {
await fakeEditorExtension.removeDebugSession(session);
Expand Down Expand Up @@ -369,6 +390,7 @@ typedef ServerConnectionPair =
Future<ServerConnectionPair> _initializeMCPServer(
MCPClient client,
bool inProcess,
FileSystem fileSystem,
) async {
ServerConnection connection;
DartToolingMCPServer? server;
Expand All @@ -392,6 +414,7 @@ Future<ServerConnectionPair> _initializeMCPServer(
server = DartToolingMCPServer(
serverChannel,
processManager: TestProcessManager(),
fs: fileSystem,
);
addTearDown(server.shutdown);
connection = client.connectServer(clientChannel);
Expand All @@ -411,10 +434,6 @@ Future<ServerConnectionPair> _initializeMCPServer(
return (serverConnection: connection, server: server);
}

/// Creates a canonical [Root] object for a given [projectPath].
Root rootForPath(String projectPath) =>
Root(uri: Directory(projectPath).absolute.uri.toString());

final counterAppPath = p.join('test_fixtures', 'counter_app');

final dartCliAppsPath = p.join('test_fixtures', 'dart_cli_app');
Expand Down
8 changes: 4 additions & 4 deletions pkgs/dart_tooling_mcp_server/test/tools/analyzer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ void main() {
});

test('can analyze a project', () async {
final counterAppRoot = rootForPath(counterAppPath);
final counterAppRoot = testHarness.rootForPath(counterAppPath);
testHarness.mcpClient.addRoot(counterAppRoot);
// Allow the notification to propagate, and the server to ask for the new
// list of roots.
Expand All @@ -53,7 +53,7 @@ void main() {
d.file('main.dart', 'void main() => 1 + "2";'),
]);
await example.create();
final exampleRoot = rootForPath(example.io.path);
final exampleRoot = testHarness.rootForPath(example.io.path);
testHarness.mcpClient.addRoot(exampleRoot);

// Allow the notification to propagate, and the server to ask for the new
Expand Down Expand Up @@ -91,7 +91,7 @@ void main() {
});

test('can look up symbols in a workspace', () async {
final currentRoot = rootForPath(Directory.current.path);
final currentRoot = testHarness.rootForPath(Directory.current.path);
testHarness.mcpClient.addRoot(currentRoot);
await pumpEventQueue();

Expand All @@ -114,7 +114,7 @@ void main() {
});

test('can get signature help', () async {
final counterAppRoot = rootForPath(counterAppPath);
final counterAppRoot = testHarness.rootForPath(counterAppPath);
testHarness.mcpClient.addRoot(counterAppRoot);
await pumpEventQueue();

Expand Down
Loading