diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart index 605ba1f1..be32e73b 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart @@ -102,6 +102,29 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport ), ); } + final platforms = + ((args[ParameterNames.platform] as List?)?.cast() ?? []) + .toSet(); + if (projectType == 'flutter') { + // Platforms are ignored for Dart, so no need to validate them. + final invalidPlatforms = platforms.difference(_allowedFlutterPlatforms); + if (invalidPlatforms.isNotEmpty) { + final plural = + invalidPlatforms.length > 1 + ? 'are not valid platforms' + : 'is not a valid platform'; + errors.add( + ValidationError( + ValidationErrorType.itemInvalid, + path: [ParameterNames.platform], + details: + '${invalidPlatforms.join(',')} $plural. Platforms ' + '${_allowedFlutterPlatforms.map((e) => '`$e`').join(', ')} ' + 'are the only allowed values for the platform list argument.', + ), + ); + } + } if (errors.isNotEmpty) { return CallToolResult( @@ -117,6 +140,13 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport final commandArgs = [ 'create', if (template != null && template.isNotEmpty) ...['--template', template], + if (projectType == 'flutter' && platforms.isNotEmpty) + '--platform=${platforms.join(',')}', + // Create an "empty" project by default so the LLM doesn't have to deal + // with all the boilerplate and comments. + if (projectType == 'flutter' && + (args[ParameterNames.empty] as bool? ?? true)) + '--empty', directory, ]; @@ -188,8 +218,30 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport description: 'The project template to use (e.g., "console-full", "app").', ), + ParameterNames.platform: Schema.list( + items: Schema.string(), + description: + 'The list of platforms this project supports. Only valid ' + 'for Flutter projects. The allowed values are ' + '${_allowedFlutterPlatforms.map((e) => '`$e`').join(', ')}. ' + 'Defaults to creating a project for all platforms.', + ), + ParameterNames.empty: Schema.bool( + description: + 'Whether or not to create an "empty" project with minimized ' + 'boilerplate and example code. Defaults to true.', + ), }, required: [ParameterNames.directory, ParameterNames.projectType], ), ); + + static const _allowedFlutterPlatforms = { + 'web', + 'linux', + 'macos', + 'windows', + 'android', + 'ios', + }; } diff --git a/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart b/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart index 9310546b..44e23894 100644 --- a/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart +++ b/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart @@ -292,10 +292,11 @@ ListSchema rootsSchema({bool supportsPaths = false}) => Schema.list( ); final rootSchema = Schema.string( - title: 'The URI of the project root to run this tool in.', + title: 'The file URI of the project root to run this tool in.', description: 'This must be equal to or a subdirectory of one of the roots ' - 'returned by a call to "listRoots".', + 'allowed by the client. Must be a URI with a `file:` ' + 'scheme (e.g. file:///absolute/path/to/root).', ); /// Very thin extension type for a pubspec just containing what we need. diff --git a/pkgs/dart_mcp_server/lib/src/utils/constants.dart b/pkgs/dart_mcp_server/lib/src/utils/constants.dart index f6fc543d..a5a27292 100644 --- a/pkgs/dart_mcp_server/lib/src/utils/constants.dart +++ b/pkgs/dart_mcp_server/lib/src/utils/constants.dart @@ -9,10 +9,12 @@ extension ParameterNames on Never { static const column = 'column'; static const command = 'command'; static const directory = 'directory'; + static const empty = 'empty'; static const line = 'line'; static const name = 'name'; static const packageName = 'packageName'; static const paths = 'paths'; + static const platform = 'platform'; static const position = 'position'; static const projectType = 'projectType'; static const query = 'query'; diff --git a/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart b/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart index af6786c5..e8c52c13 100644 --- a/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart +++ b/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart @@ -16,5 +16,5 @@ import 'package:process/process.dart'; /// implement this class and use [processManager] instead of making direct calls /// to dart:io's [Process] class. abstract interface class ProcessManagerSupport { - LocalProcessManager get processManager; + ProcessManager get processManager; } diff --git a/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart b/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart index 98caf8de..14737411 100644 --- a/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart @@ -217,6 +217,7 @@ dependencies: 'create', '--template', 'app', + '--empty', 'new_app', ], workingDirectory: exampleFlutterAppRoot.path, @@ -224,6 +225,66 @@ dependencies: ]); }); + test('creates a non-empty Flutter project', () async { + testHarness.mcpClient.addRoot(exampleFlutterAppRoot); + final request = CallToolRequest( + name: createProjectTool.name, + arguments: { + ParameterNames.root: exampleFlutterAppRoot.uri, + ParameterNames.directory: 'new_full_app', + ParameterNames.projectType: 'flutter', + ParameterNames.template: 'app', + ParameterNames.empty: + false, // Explicitly create a non-empty project + }, + ); + await testHarness.callToolWithRetry(request); + + expect(testProcessManager.commandsRan, [ + equalsCommand(( + command: [ + endsWith('flutter'), + 'create', + '--template', + 'app', + // Note: --empty is NOT present + 'new_full_app', + ], + workingDirectory: exampleFlutterAppRoot.path, + )), + ]); + }); + + test('fails with invalid platform for Flutter project', () async { + testHarness.mcpClient.addRoot(exampleFlutterAppRoot); + final request = CallToolRequest( + name: createProjectTool.name, + arguments: { + ParameterNames.root: exampleFlutterAppRoot.uri, + ParameterNames.directory: 'my_app_invalid_platform', + ParameterNames.projectType: 'flutter', + ParameterNames.platform: ['atari_jaguar', 'web'], // One invalid + }, + ); + final result = await testHarness.callToolWithRetry( + request, + expectError: true, + ); + + expect(result.isError, isTrue); + expect( + (result.content.first as TextContent).text, + allOf( + contains('atari_jaguar is not a valid platform.'), + contains( + 'Platforms `web`, `linux`, `macos`, `windows`, `android`, `ios` ' + 'are the only allowed values', + ), + ), + ); + expect(testProcessManager.commandsRan, isEmpty); + }); + test('fails if projectType is missing', () async { testHarness.mcpClient.addRoot(dartCliAppRoot); final request = CallToolRequest(