Skip to content

Commit 47fa2fc

Browse files
authored
Add the --platform and --empty arguments to the flutter create tool (#144)
1 parent c5582f4 commit 47fa2fc

File tree

5 files changed

+119
-3
lines changed

5 files changed

+119
-3
lines changed

pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,29 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
102102
),
103103
);
104104
}
105+
final platforms =
106+
((args[ParameterNames.platform] as List?)?.cast<String>() ?? [])
107+
.toSet();
108+
if (projectType == 'flutter') {
109+
// Platforms are ignored for Dart, so no need to validate them.
110+
final invalidPlatforms = platforms.difference(_allowedFlutterPlatforms);
111+
if (invalidPlatforms.isNotEmpty) {
112+
final plural =
113+
invalidPlatforms.length > 1
114+
? 'are not valid platforms'
115+
: 'is not a valid platform';
116+
errors.add(
117+
ValidationError(
118+
ValidationErrorType.itemInvalid,
119+
path: [ParameterNames.platform],
120+
details:
121+
'${invalidPlatforms.join(',')} $plural. Platforms '
122+
'${_allowedFlutterPlatforms.map((e) => '`$e`').join(', ')} '
123+
'are the only allowed values for the platform list argument.',
124+
),
125+
);
126+
}
127+
}
105128

106129
if (errors.isNotEmpty) {
107130
return CallToolResult(
@@ -117,6 +140,13 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
117140
final commandArgs = [
118141
'create',
119142
if (template != null && template.isNotEmpty) ...['--template', template],
143+
if (projectType == 'flutter' && platforms.isNotEmpty)
144+
'--platform=${platforms.join(',')}',
145+
// Create an "empty" project by default so the LLM doesn't have to deal
146+
// with all the boilerplate and comments.
147+
if (projectType == 'flutter' &&
148+
(args[ParameterNames.empty] as bool? ?? true))
149+
'--empty',
120150
directory,
121151
];
122152

@@ -188,8 +218,30 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
188218
description:
189219
'The project template to use (e.g., "console-full", "app").',
190220
),
221+
ParameterNames.platform: Schema.list(
222+
items: Schema.string(),
223+
description:
224+
'The list of platforms this project supports. Only valid '
225+
'for Flutter projects. The allowed values are '
226+
'${_allowedFlutterPlatforms.map((e) => '`$e`').join(', ')}. '
227+
'Defaults to creating a project for all platforms.',
228+
),
229+
ParameterNames.empty: Schema.bool(
230+
description:
231+
'Whether or not to create an "empty" project with minimized '
232+
'boilerplate and example code. Defaults to true.',
233+
),
191234
},
192235
required: [ParameterNames.directory, ParameterNames.projectType],
193236
),
194237
);
238+
239+
static const _allowedFlutterPlatforms = {
240+
'web',
241+
'linux',
242+
'macos',
243+
'windows',
244+
'android',
245+
'ios',
246+
};
195247
}

pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,11 @@ ListSchema rootsSchema({bool supportsPaths = false}) => Schema.list(
292292
);
293293

294294
final rootSchema = Schema.string(
295-
title: 'The URI of the project root to run this tool in.',
295+
title: 'The file URI of the project root to run this tool in.',
296296
description:
297297
'This must be equal to or a subdirectory of one of the roots '
298-
'returned by a call to "listRoots".',
298+
'allowed by the client. Must be a URI with a `file:` '
299+
'scheme (e.g. file:///absolute/path/to/root).',
299300
);
300301

301302
/// Very thin extension type for a pubspec just containing what we need.

pkgs/dart_mcp_server/lib/src/utils/constants.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ extension ParameterNames on Never {
99
static const column = 'column';
1010
static const command = 'command';
1111
static const directory = 'directory';
12+
static const empty = 'empty';
1213
static const line = 'line';
1314
static const name = 'name';
1415
static const packageName = 'packageName';
1516
static const paths = 'paths';
17+
static const platform = 'platform';
1618
static const position = 'position';
1719
static const projectType = 'projectType';
1820
static const query = 'query';

pkgs/dart_mcp_server/lib/src/utils/process_manager.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ import 'package:process/process.dart';
1616
/// implement this class and use [processManager] instead of making direct calls
1717
/// to dart:io's [Process] class.
1818
abstract interface class ProcessManagerSupport {
19-
LocalProcessManager get processManager;
19+
ProcessManager get processManager;
2020
}

pkgs/dart_mcp_server/test/tools/dart_cli_test.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,74 @@ dependencies:
217217
'create',
218218
'--template',
219219
'app',
220+
'--empty',
220221
'new_app',
221222
],
222223
workingDirectory: exampleFlutterAppRoot.path,
223224
)),
224225
]);
225226
});
226227

228+
test('creates a non-empty Flutter project', () async {
229+
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
230+
final request = CallToolRequest(
231+
name: createProjectTool.name,
232+
arguments: {
233+
ParameterNames.root: exampleFlutterAppRoot.uri,
234+
ParameterNames.directory: 'new_full_app',
235+
ParameterNames.projectType: 'flutter',
236+
ParameterNames.template: 'app',
237+
ParameterNames.empty:
238+
false, // Explicitly create a non-empty project
239+
},
240+
);
241+
await testHarness.callToolWithRetry(request);
242+
243+
expect(testProcessManager.commandsRan, [
244+
equalsCommand((
245+
command: [
246+
endsWith('flutter'),
247+
'create',
248+
'--template',
249+
'app',
250+
// Note: --empty is NOT present
251+
'new_full_app',
252+
],
253+
workingDirectory: exampleFlutterAppRoot.path,
254+
)),
255+
]);
256+
});
257+
258+
test('fails with invalid platform for Flutter project', () async {
259+
testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
260+
final request = CallToolRequest(
261+
name: createProjectTool.name,
262+
arguments: {
263+
ParameterNames.root: exampleFlutterAppRoot.uri,
264+
ParameterNames.directory: 'my_app_invalid_platform',
265+
ParameterNames.projectType: 'flutter',
266+
ParameterNames.platform: ['atari_jaguar', 'web'], // One invalid
267+
},
268+
);
269+
final result = await testHarness.callToolWithRetry(
270+
request,
271+
expectError: true,
272+
);
273+
274+
expect(result.isError, isTrue);
275+
expect(
276+
(result.content.first as TextContent).text,
277+
allOf(
278+
contains('atari_jaguar is not a valid platform.'),
279+
contains(
280+
'Platforms `web`, `linux`, `macos`, `windows`, `android`, `ios` '
281+
'are the only allowed values',
282+
),
283+
),
284+
);
285+
expect(testProcessManager.commandsRan, isEmpty);
286+
});
287+
227288
test('fails if projectType is missing', () async {
228289
testHarness.mcpClient.addRoot(dartCliAppRoot);
229290
final request = CallToolRequest(

0 commit comments

Comments
 (0)