Skip to content

Commit c5582f4

Browse files
authored
Invoke dart/flutter in a more robust way (#143)
Closes #33 Adds --dart-sdk and --flutter-sdk arguments to the CLI, falling back on DART_SDK and FLUTTER_SDK environment variables, and finally `Platform.resolvedExecutable`. The flutter SDK will also be looked up relative to the Dart SDK, if a location is not given by command line arg or environment.
1 parent 099a3fc commit c5582f4

File tree

12 files changed

+235
-72
lines changed

12 files changed

+235
-72
lines changed

pkgs/dart_mcp_server/bin/main.dart

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ void main(List<String> args) async {
2020
}
2121

2222
DartMCPServer? server;
23-
await runZonedGuarded(
24-
() async {
25-
server = await DartMCPServer.connect(
23+
final dartSdkPath =
24+
parsedArgs.option(dartSdkOption) ?? io.Platform.environment['DART_SDK'];
25+
final flutterSdkPath =
26+
parsedArgs.option(flutterSdkOption) ??
27+
io.Platform.environment['FLUTTER_SDK'];
28+
runZonedGuarded(
29+
() {
30+
server = DartMCPServer(
2631
StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
2732
.transform(StreamChannelTransformer.fromCodec(utf8))
2833
.transformStream(const LineSplitter())
@@ -34,6 +39,7 @@ void main(List<String> args) async {
3439
),
3540
),
3641
forceRootsFallback: parsedArgs.flag(forceRootsFallback),
42+
sdk: Sdk.find(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath),
3743
);
3844
},
3945
(e, s) {
@@ -65,6 +71,19 @@ void main(List<String> args) async {
6571

6672
final argParser =
6773
ArgParser(allowTrailingOptions: false)
74+
..addOption(
75+
dartSdkOption,
76+
help:
77+
'The path to the root of the desired Dart SDK. Defaults to the '
78+
'DART_SDK environment variable.',
79+
)
80+
..addOption(
81+
flutterSdkOption,
82+
help:
83+
'The path to the root of the desired Flutter SDK. Defaults to '
84+
'the FLUTTER_SDK environment variable, then searching up from the '
85+
'Dart SDK.',
86+
)
6887
..addFlag(
6988
forceRootsFallback,
7089
negatable: true,
@@ -77,5 +96,7 @@ final argParser =
7796
)
7897
..addFlag(help, abbr: 'h', help: 'Show usage text');
7998

99+
const dartSdkOption = 'dart-sdk';
100+
const flutterSdkOption = 'flutter-sdk';
80101
const forceRootsFallback = 'force-roots-fallback';
81102
const help = 'help';

pkgs/dart_mcp_server/lib/dart_mcp_server.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
export 'src/server.dart';
6+
export 'src/utils/sdk.dart' show Sdk;

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import 'package:meta/meta.dart';
1414

1515
import '../lsp/wire_format.dart';
1616
import '../utils/constants.dart';
17+
import '../utils/sdk.dart';
1718

1819
/// Mix this in to any MCPServer to add support for analyzing Dart projects.
1920
///
2021
/// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
2122
/// mixins applied.
2223
base mixin DartAnalyzerSupport
23-
on ToolsSupport, LoggingSupport, RootsTrackingSupport {
24+
on ToolsSupport, LoggingSupport, RootsTrackingSupport
25+
implements SdkSupport {
2426
/// The LSP server connection for the analysis server.
2527
late final Peer _lspConnection;
2628

@@ -52,9 +54,8 @@ base mixin DartAnalyzerSupport
5254
if (!supportsRoots)
5355
'Project analysis requires the "roots" capability which is not '
5456
'supported. Analysis tools have been disabled.',
55-
if (Platform.environment['DART_SDK'] == null)
56-
'Project analysis requires a "DART_SDK" environment variable to be set '
57-
'(this should be the path to the root of the dart SDK). Analysis '
57+
if (sdk.dartSdkPath == null)
58+
'Project analysis requires a Dart SDK but none was given. Analysis '
5859
'tools have been disabled.',
5960
];
6061

@@ -90,7 +91,7 @@ base mixin DartAnalyzerSupport
9091
///
9192
/// On failure, returns a reason for the failure.
9293
Future<String?> _initializeAnalyzerLspServer() async {
93-
_lspServer = await Process.start('dart', [
94+
_lspServer = await Process.start(sdk.dartExecutablePath, [
9495
'language-server',
9596
// Required even though it is documented as the default.
9697
// https://github.com/dart-lang/sdk/issues/60574

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,22 @@ import '../utils/cli_utils.dart';
1111
import '../utils/constants.dart';
1212
import '../utils/file_system.dart';
1313
import '../utils/process_manager.dart';
14+
import '../utils/sdk.dart';
1415

1516
/// Mix this in to any MCPServer to add support for running Dart or Flutter CLI
1617
/// commands like `dart fix`, `dart format`, and `flutter test`.
1718
///
1819
/// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
1920
/// mixins applied.
2021
base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
21-
implements ProcessManagerSupport, FileSystemSupport {
22+
implements ProcessManagerSupport, FileSystemSupport, SdkSupport {
2223
@override
2324
FutureOr<InitializeResult> initialize(InitializeRequest request) {
2425
try {
2526
return super.initialize(request);
2627
} finally {
27-
// Can't call this until after `super.initialize`.
28-
if (supportsRoots) {
28+
// Can't call `supportsRoots` until after `super.initialize`.
29+
if (supportsRoots && sdk.dartSdkPath != null) {
2930
registerTool(dartFixTool, _runDartFixTool);
3031
registerTool(dartFormatTool, _runDartFormatTool);
3132
registerTool(runTestsTool, _runTests);
@@ -38,26 +39,28 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
3839
Future<CallToolResult> _runDartFixTool(CallToolRequest request) async {
3940
return runCommandInRoots(
4041
request,
41-
commandForRoot: (_, _) => 'dart',
42+
commandForRoot: (_, _, sdk) => sdk.dartExecutablePath,
4243
arguments: ['fix', '--apply'],
4344
commandDescription: 'dart fix',
4445
processManager: processManager,
4546
knownRoots: await roots,
4647
fileSystem: fileSystem,
48+
sdk: sdk,
4749
);
4850
}
4951

5052
/// Implementation of the [dartFormatTool].
5153
Future<CallToolResult> _runDartFormatTool(CallToolRequest request) async {
5254
return runCommandInRoots(
5355
request,
54-
commandForRoot: (_, _) => 'dart',
56+
commandForRoot: (_, _, sdk) => sdk.dartExecutablePath,
5557
arguments: ['format'],
5658
commandDescription: 'dart format',
5759
processManager: processManager,
5860
defaultPaths: ['.'],
5961
knownRoots: await roots,
6062
fileSystem: fileSystem,
63+
sdk: sdk,
6164
);
6265
}
6366

@@ -70,6 +73,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
7073
processManager: processManager,
7174
knownRoots: await roots,
7275
fileSystem: fileSystem,
76+
sdk: sdk,
7377
);
7478
}
7579

@@ -119,11 +123,19 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
119123
return runCommandInRoot(
120124
request,
121125
arguments: commandArgs,
122-
commandForRoot: (_, _) => projectType!,
126+
commandForRoot:
127+
(_, _, sdk) =>
128+
switch (projectType) {
129+
'dart' => sdk.dartExecutablePath,
130+
'flutter' => sdk.flutterExecutablePath,
131+
_ => StateError('Unknown project type: $projectType'),
132+
}
133+
as String,
123134
commandDescription: '$projectType create',
124135
fileSystem: fileSystem,
125136
processManager: processManager,
126137
knownRoots: await roots,
138+
sdk: sdk,
127139
);
128140
}
129141

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../utils/cli_utils.dart';
1010
import '../utils/constants.dart';
1111
import '../utils/file_system.dart';
1212
import '../utils/process_manager.dart';
13+
import '../utils/sdk.dart';
1314

1415
/// Mix this in to any MCPServer to add support for running Pub commands like
1516
/// like `pub add` and `pub get`.
@@ -19,7 +20,7 @@ import '../utils/process_manager.dart';
1920
/// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
2021
/// mixins applied.
2122
base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
22-
implements ProcessManagerSupport, FileSystemSupport {
23+
implements ProcessManagerSupport, FileSystemSupport, SdkSupport {
2324
@override
2425
FutureOr<InitializeResult> initialize(InitializeRequest request) {
2526
try {
@@ -79,6 +80,7 @@ base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
7980
processManager: processManager,
8081
knownRoots: await roots,
8182
fileSystem: fileSystem,
83+
sdk: sdk,
8284
);
8385
}
8486

pkgs/dart_mcp_server/lib/src/server.dart

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'dart:async';
6-
75
import 'package:dart_mcp/server.dart';
86
import 'package:file/file.dart';
97
import 'package:file/local.dart';
108
import 'package:meta/meta.dart';
119
import 'package:process/process.dart';
12-
import 'package:stream_channel/stream_channel.dart';
1310

1411
import 'mixins/analyzer.dart';
1512
import 'mixins/dash_cli.dart';
@@ -19,6 +16,7 @@ import 'mixins/pub_dev_search.dart';
1916
import 'mixins/roots_fallback_support.dart';
2017
import 'utils/file_system.dart';
2118
import 'utils/process_manager.dart';
19+
import 'utils/sdk.dart';
2220

2321
/// An MCP server for Dart and Flutter tooling.
2422
final class DartMCPServer extends MCPServer
@@ -33,9 +31,10 @@ final class DartMCPServer extends MCPServer
3331
PubSupport,
3432
PubDevSupport,
3533
DartToolingDaemonSupport
36-
implements ProcessManagerSupport, FileSystemSupport {
34+
implements ProcessManagerSupport, FileSystemSupport, SdkSupport {
3735
DartMCPServer(
3836
super.channel, {
37+
required this.sdk,
3938
@visibleForTesting this.processManager = const LocalProcessManager(),
4039
@visibleForTesting this.fileSystem = const LocalFileSystem(),
4140
this.forceRootsFallback = false,
@@ -49,13 +48,6 @@ final class DartMCPServer extends MCPServer
4948
'their development tools and running applications.',
5049
);
5150

52-
static Future<DartMCPServer> connect(
53-
StreamChannel<String> mcpChannel, {
54-
bool forceRootsFallback = false,
55-
}) async {
56-
return DartMCPServer(mcpChannel, forceRootsFallback: forceRootsFallback);
57-
}
58-
5951
@override
6052
final LocalProcessManager processManager;
6153

@@ -64,4 +56,7 @@ final class DartMCPServer extends MCPServer
6456

6557
@override
6658
final bool forceRootsFallback;
59+
60+
@override
61+
final Sdk sdk;
6762
}

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:process/process.dart';
1111
import 'package:yaml/yaml.dart';
1212

1313
import 'constants.dart';
14+
import 'sdk.dart';
1415

1516
/// The supported kinds of projects.
1617
enum ProjectKind {
@@ -71,14 +72,15 @@ Future<ProjectKind> inferProjectKind(Root root, FileSystem fileSystem) async {
7172
/// root's 'paths'.
7273
Future<CallToolResult> runCommandInRoots(
7374
CallToolRequest request, {
74-
FutureOr<String> Function(Root, FileSystem) commandForRoot =
75+
FutureOr<String> Function(Root, FileSystem, Sdk) commandForRoot =
7576
defaultCommandForRoot,
7677
List<String> arguments = const [],
7778
required String commandDescription,
7879
required FileSystem fileSystem,
7980
required ProcessManager processManager,
8081
required List<Root> knownRoots,
8182
List<String> defaultPaths = const <String>[],
83+
required Sdk sdk,
8284
}) async {
8385
var rootConfigs =
8486
(request.arguments?[ParameterNames.roots] as List?)
@@ -103,6 +105,7 @@ Future<CallToolResult> runCommandInRoots(
103105
processManager: processManager,
104106
knownRoots: knownRoots,
105107
defaultPaths: defaultPaths,
108+
sdk: sdk,
106109
);
107110
if (result.isError == true) return result;
108111
outputs.addAll(result.content);
@@ -134,14 +137,15 @@ Future<CallToolResult> runCommandInRoots(
134137
Future<CallToolResult> runCommandInRoot(
135138
CallToolRequest request, {
136139
Map<String, Object?>? rootConfig,
137-
FutureOr<String> Function(Root, FileSystem) commandForRoot =
140+
FutureOr<String> Function(Root, FileSystem, Sdk) commandForRoot =
138141
defaultCommandForRoot,
139142
List<String> arguments = const [],
140143
required String commandDescription,
141144
required FileSystem fileSystem,
142145
required ProcessManager processManager,
143146
required List<Root> knownRoots,
144147
List<String> defaultPaths = const <String>[],
148+
required Sdk sdk,
145149
}) async {
146150
rootConfig ??= request.arguments;
147151
final rootUriString = rootConfig?[ParameterNames.root] as String?;
@@ -185,7 +189,7 @@ Future<CallToolResult> runCommandInRoot(
185189
final projectRoot = fileSystem.directory(rootUri);
186190

187191
final commandWithPaths = <String>[
188-
await commandForRoot(root, fileSystem),
192+
await commandForRoot(root, fileSystem, sdk),
189193
...arguments,
190194
];
191195
final paths =
@@ -240,18 +244,21 @@ Future<CallToolResult> runCommandInRoot(
240244
/// Returns 'dart' or 'flutter' based on the pubspec contents.
241245
///
242246
/// Throws an [ArgumentError] if there is no pubspec.
243-
Future<String> defaultCommandForRoot(Root root, FileSystem fileSystem) async =>
244-
switch (await inferProjectKind(root, fileSystem)) {
245-
ProjectKind.dart => 'dart',
246-
ProjectKind.flutter => 'flutter',
247-
ProjectKind.unknown =>
248-
throw ArgumentError.value(
249-
root.uri,
250-
'root.uri',
251-
'Unknown project kind at root ${root.uri}. All projects must have a '
252-
'pubspec.',
253-
),
254-
};
247+
Future<String> defaultCommandForRoot(
248+
Root root,
249+
FileSystem fileSystem,
250+
Sdk sdk,
251+
) async => switch (await inferProjectKind(root, fileSystem)) {
252+
ProjectKind.dart => sdk.dartExecutablePath,
253+
ProjectKind.flutter => sdk.flutterExecutablePath,
254+
ProjectKind.unknown =>
255+
throw ArgumentError.value(
256+
root.uri,
257+
'root.uri',
258+
'Unknown project kind at root ${root.uri}. All projects must have a '
259+
'pubspec.',
260+
),
261+
};
255262

256263
/// Returns whether or not [rootUri] is an allowed root, either exactly matching
257264
/// or under on of the [knownRoots].

0 commit comments

Comments
 (0)