[dart_tooling_mcp_server] Add a dart pub tool (#76)
diff --git a/pkgs/dart_tooling_mcp_server/lib/src/mixins/dart_cli.dart b/pkgs/dart_tooling_mcp_server/lib/src/mixins/dart_cli.dart
index 2f18a8a..8b3ba0a 100644
--- a/pkgs/dart_tooling_mcp_server/lib/src/mixins/dart_cli.dart
+++ b/pkgs/dart_tooling_mcp_server/lib/src/mixins/dart_cli.dart
@@ -28,6 +28,7 @@
}
registerTool(dartFixTool, _runDartFixTool);
registerTool(dartFormatTool, _runDartFormatTool);
+ registerTool(dartPubTool, _runDartPubTool);
return super.initialize(request);
}
@@ -50,6 +51,51 @@
);
}
+ /// Implementation of the [dartPubTool].
+ Future<CallToolResult> _runDartPubTool(CallToolRequest request) async {
+ final command = request.arguments?['command'] as String?;
+ if (command == null) {
+ return CallToolResult(
+ content: [TextContent(text: 'Missing required argument `command`.')],
+ isError: true,
+ );
+ }
+ final matchingCommand = SupportedPubCommand.fromName(command);
+ if (matchingCommand == null) {
+ return CallToolResult(
+ content: [
+ TextContent(
+ text:
+ 'Unsupported pub command `$command`. Currently, the supported '
+ 'commands are: '
+ '${SupportedPubCommand.values.map((e) => e.name).join(', ')}',
+ ),
+ ],
+ isError: true,
+ );
+ }
+
+ final packageName = request.arguments?['packageName'] as String?;
+ if (matchingCommand.requiresPackageName && packageName == null) {
+ return CallToolResult(
+ content: [
+ TextContent(
+ text:
+ 'Missing required argument `packageName` for the `$command` '
+ 'command.',
+ ),
+ ],
+ isError: true,
+ );
+ }
+
+ return _runDartCommandInRoots(
+ request,
+ commandDescription: 'dart pub $command',
+ commandArgs: ['pub', command, if (packageName != null) packageName],
+ );
+ }
+
/// Helper to run a dart command in multiple project roots.
///
/// [defaultPaths] may be specified if one or more path arguments are required
@@ -137,6 +183,44 @@
return CallToolResult(content: outputs);
}
+ static final dartPubTool = Tool(
+ name: 'dart_pub',
+ description:
+ 'Runs a dart pub command for the given project roots, like `dart pub '
+ 'get` or `dart pub add`.',
+ inputSchema: ObjectSchema(
+ properties: {
+ 'command': StringSchema(
+ title: 'The dart pub command to run.',
+ description:
+ 'Currently only ${SupportedPubCommand.listAll} are supported.',
+ ),
+ 'packageName': StringSchema(
+ title: 'The package name to run the command for.',
+ description:
+ 'This is required for the '
+ '${SupportedPubCommand.listAllThatRequirePackageName} commands.',
+ ),
+ 'roots': ListSchema(
+ title: 'All projects roots to run the dart pub command in.',
+ description:
+ 'These must match a root returned by a call to "listRoots".',
+ items: ObjectSchema(
+ properties: {
+ 'root': StringSchema(
+ title:
+ 'The URI of the project root to run the dart pub command '
+ 'in.',
+ ),
+ },
+ required: ['root'],
+ ),
+ ),
+ },
+ required: ['command', 'roots'],
+ ),
+ );
+
static final dartFixTool = Tool(
name: 'dart_fix',
description: 'Runs `dart fix --apply` for the given project roots.',
@@ -190,3 +274,55 @@
),
);
}
+
+/// The set of supported `dart pub` subcommands.
+enum SupportedPubCommand {
+ // This is supported in a simplified form: `dart pub add <package-name>`.
+ // TODO(https://github.com/dart-lang/ai/issues/77): add support for adding
+ // dev dependencies.
+ add(requiresPackageName: true),
+
+ get,
+
+ // This is supported in a simplified form: `dart pub remove <package-name>`.
+ remove(requiresPackageName: true),
+
+ upgrade;
+
+ const SupportedPubCommand({this.requiresPackageName = false});
+
+ final bool requiresPackageName;
+
+ static SupportedPubCommand? fromName(String name) {
+ for (final command in SupportedPubCommand.values) {
+ if (command.name == name) {
+ return command;
+ }
+ }
+ return null;
+ }
+
+ static String get listAll {
+ return _writeCommandsAsList(SupportedPubCommand.values);
+ }
+
+ static String get listAllThatRequirePackageName {
+ return _writeCommandsAsList(
+ SupportedPubCommand.values.where((c) => c.requiresPackageName).toList(),
+ );
+ }
+
+ static String _writeCommandsAsList(List<SupportedPubCommand> commands) {
+ final buffer = StringBuffer();
+ for (var i = 0; i < commands.length; i++) {
+ final commandName = commands[i].name;
+ buffer.write('`$commandName`');
+ if (i < commands.length - 2) {
+ buffer.write(', ');
+ } else if (i == commands.length - 2) {
+ buffer.write(' and ');
+ }
+ }
+ return buffer.toString();
+ }
+}
diff --git a/pkgs/dart_tooling_mcp_server/test/test_harness.dart b/pkgs/dart_tooling_mcp_server/test/test_harness.dart
index ba93886..0dc4a76 100644
--- a/pkgs/dart_tooling_mcp_server/test/test_harness.dart
+++ b/pkgs/dart_tooling_mcp_server/test/test_harness.dart
@@ -133,6 +133,7 @@
Future<CallToolResult> callToolWithRetry(
CallToolRequest request, {
int maxTries = 5,
+ bool expectError = false,
}) async {
var tryCount = 0;
late CallToolResult lastResult;
@@ -143,7 +144,7 @@
}
expect(
lastResult.isError,
- isNot(true),
+ expectError ? true : isNot(true),
reason: lastResult.content.join('\n'),
);
return lastResult;
diff --git a/pkgs/dart_tooling_mcp_server/test/tools/dart_cli_test.dart b/pkgs/dart_tooling_mcp_server/test/tools/dart_cli_test.dart
index 843a0e5..3896ef4 100644
--- a/pkgs/dart_tooling_mcp_server/test/tools/dart_cli_test.dart
+++ b/pkgs/dart_tooling_mcp_server/test/tools/dart_cli_test.dart
@@ -32,6 +32,7 @@
group('dart cli tools', () {
late Tool dartFixTool;
late Tool dartFormatTool;
+ late Tool dartPubTool;
setUp(() async {
final tools = (await testHarness.mcpServerConnection.listTools()).tools;
@@ -41,6 +42,9 @@
dartFormatTool = tools.singleWhere(
(t) => t.name == DartCliSupport.dartFormatTool.name,
);
+ dartPubTool = tools.singleWhere(
+ (t) => t.name == DartCliSupport.dartPubTool.name,
+ );
});
test('dart fix', () async {
@@ -99,5 +103,157 @@
['dart', 'format', 'foo.dart', 'bar.dart'],
]);
});
+
+ group('dart pub', () {
+ test('add', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {
+ 'command': 'add',
+ 'packageName': 'foo',
+ 'roots': [
+ {'root': testRoot.uri},
+ ],
+ },
+ );
+ final result = await testHarness.callToolWithRetry(request);
+
+ // Verify the command was sent to the process maanger without error.
+ expect(result.isError, isNot(true));
+ expect(testProcessManager.commandsRan, [
+ ['dart', 'pub', 'add', 'foo'],
+ ]);
+ });
+
+ test('remove', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {
+ 'command': 'remove',
+ 'packageName': 'foo',
+ 'roots': [
+ {'root': testRoot.uri},
+ ],
+ },
+ );
+ final result = await testHarness.callToolWithRetry(request);
+
+ // Verify the command was sent to the process maanger without error.
+ expect(result.isError, isNot(true));
+ expect(testProcessManager.commandsRan, [
+ ['dart', 'pub', 'remove', 'foo'],
+ ]);
+ });
+
+ test('get', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {
+ 'command': 'get',
+ 'roots': [
+ {'root': testRoot.uri},
+ ],
+ },
+ );
+ final result = await testHarness.callToolWithRetry(request);
+
+ // Verify the command was sent to the process maanger without error.
+ expect(result.isError, isNot(true));
+ expect(testProcessManager.commandsRan, [
+ ['dart', 'pub', 'get'],
+ ]);
+ });
+
+ test('upgrade', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {
+ 'command': 'upgrade',
+ 'roots': [
+ {'root': testRoot.uri},
+ ],
+ },
+ );
+ final result = await testHarness.callToolWithRetry(request);
+
+ // Verify the command was sent to the process maanger without error.
+ expect(result.isError, isNot(true));
+ expect(testProcessManager.commandsRan, [
+ ['dart', 'pub', 'upgrade'],
+ ]);
+ });
+
+ group('returns error', () {
+ test('for missing command', () async {
+ final request = CallToolRequest(name: dartPubTool.name);
+ final result = await testHarness.callToolWithRetry(
+ request,
+ expectError: true,
+ );
+
+ expect(
+ (result.content.single as TextContent).text,
+ 'Missing required argument `command`.',
+ );
+ expect(testProcessManager.commandsRan, isEmpty);
+ });
+
+ test('for unsupported command', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {'command': 'publish'},
+ );
+ final result = await testHarness.callToolWithRetry(
+ request,
+ expectError: true,
+ );
+
+ expect(
+ (result.content.single as TextContent).text,
+ contains('Unsupported pub command `publish`.'),
+ );
+ expect(testProcessManager.commandsRan, isEmpty);
+ });
+
+ for (final command in SupportedPubCommand.values.where(
+ (c) => c.requiresPackageName,
+ )) {
+ test('for missing package name: $command', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {'command': command.name},
+ );
+ final result = await testHarness.callToolWithRetry(
+ request,
+ expectError: true,
+ );
+
+ expect(
+ (result.content.single as TextContent).text,
+ 'Missing required argument `packageName` for the '
+ '`${command.name}` command.',
+ );
+ expect(testProcessManager.commandsRan, isEmpty);
+ });
+ }
+
+ test('for missing roots', () async {
+ final request = CallToolRequest(
+ name: dartPubTool.name,
+ arguments: {'command': 'get'},
+ );
+ final result = await testHarness.callToolWithRetry(
+ request,
+ expectError: true,
+ );
+
+ expect(
+ (result.content.single as TextContent).text,
+ 'Missing required argument `roots`.',
+ );
+ expect(testProcessManager.commandsRan, isEmpty);
+ });
+ });
+ });
});
}