Skip to content

Commit 706d224

Browse files
authored
move logic from bin/main.dart into the DartMCPServer.run (#185)
This will make the custom binary in the SDK simpler to write.
1 parent 5f9c50f commit 706d224

File tree

2 files changed

+143
-127
lines changed

2 files changed

+143
-127
lines changed

pkgs/dart_mcp_server/bin/main.dart

Lines changed: 2 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -2,134 +2,9 @@
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-
import 'dart:convert';
7-
import 'dart:io' as io;
8-
9-
import 'package:args/args.dart';
10-
import 'package:async/async.dart';
11-
import 'package:dart_mcp/server.dart';
5+
import 'dart:io';
126
import 'package:dart_mcp_server/dart_mcp_server.dart';
13-
import 'package:stream_channel/stream_channel.dart';
147

158
void main(List<String> args) async {
16-
final parsedArgs = argParser.parse(args);
17-
if (parsedArgs.flag(help)) {
18-
print(argParser.usage);
19-
io.exit(0);
20-
}
21-
22-
DartMCPServer? server;
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-
final logFilePath = parsedArgs.option(logFileOption);
29-
final logFileSink =
30-
logFilePath == null ? null : createLogSink(io.File(logFilePath));
31-
runZonedGuarded(
32-
() {
33-
server = DartMCPServer(
34-
StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
35-
.transform(StreamChannelTransformer.fromCodec(utf8))
36-
.transformStream(const LineSplitter())
37-
.transformSink(
38-
StreamSinkTransformer.fromHandlers(
39-
handleData: (data, sink) {
40-
sink.add('$data\n');
41-
},
42-
),
43-
),
44-
forceRootsFallback: parsedArgs.flag(forceRootsFallback),
45-
sdk: Sdk.find(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath),
46-
analytics: null,
47-
protocolLogSink: logFileSink,
48-
)..done.whenComplete(() => logFileSink?.close());
49-
},
50-
(e, s) {
51-
if (server != null) {
52-
try {
53-
// Log unhandled errors to the client, if we managed to connect.
54-
server!.log(LoggingLevel.error, '$e\n$s');
55-
} catch (_) {}
56-
} else {
57-
// Otherwise log to stderr.
58-
io.stderr
59-
..writeln(e)
60-
..writeln(s);
61-
}
62-
},
63-
zoneSpecification: ZoneSpecification(
64-
print: (_, _, _, value) {
65-
if (server != null) {
66-
try {
67-
// Don't allow `print` since this breaks stdio communication, but if
68-
// we have a server we do log messages to the client.
69-
server!.log(LoggingLevel.info, value);
70-
} catch (_) {}
71-
}
72-
},
73-
),
74-
);
75-
}
76-
77-
final argParser =
78-
ArgParser(allowTrailingOptions: false)
79-
..addOption(
80-
dartSdkOption,
81-
help:
82-
'The path to the root of the desired Dart SDK. Defaults to the '
83-
'DART_SDK environment variable.',
84-
)
85-
..addOption(
86-
flutterSdkOption,
87-
help:
88-
'The path to the root of the desired Flutter SDK. Defaults to '
89-
'the FLUTTER_SDK environment variable, then searching up from the '
90-
'Dart SDK.',
91-
)
92-
..addFlag(
93-
forceRootsFallback,
94-
negatable: true,
95-
defaultsTo: false,
96-
help:
97-
'Forces a behavior for project roots which uses MCP tools instead '
98-
'of the native MCP roots. This can be helpful for clients like '
99-
'cursor which claim to have roots support but do not actually '
100-
'support it.',
101-
)
102-
..addOption(
103-
logFileOption,
104-
help:
105-
'Path to a file to log all MPC protocol traffic to. File will be '
106-
'overwritten if it exists.',
107-
)
108-
..addFlag(help, abbr: 'h', help: 'Show usage text');
109-
110-
const dartSdkOption = 'dart-sdk';
111-
const flutterSdkOption = 'flutter-sdk';
112-
const forceRootsFallback = 'force-roots-fallback';
113-
const help = 'help';
114-
const logFileOption = 'log-file';
115-
116-
/// Creates a `Sink<String>` for [logFile].
117-
Sink<String> createLogSink(io.File logFile) {
118-
logFile.createSync(recursive: true);
119-
final fileByteSink = logFile.openWrite(
120-
mode: io.FileMode.write,
121-
encoding: utf8,
122-
);
123-
return fileByteSink.transform(
124-
StreamSinkTransformer.fromHandlers(
125-
handleData: (data, innerSink) {
126-
innerSink.add(utf8.encode(data));
127-
// It's a log, so we want to make sure it's always up-to-date.
128-
fileByteSink.flush();
129-
},
130-
handleDone: (innerSink) {
131-
innerSink.close();
132-
},
133-
),
134-
);
9+
exitCode = await DartMCPServer.run(args);
13510
}

pkgs/dart_mcp_server/lib/src/server.dart

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

55
import 'dart:async';
6+
import 'dart:convert';
7+
import 'dart:io' as io;
68

9+
import 'package:args/args.dart';
10+
import 'package:async/async.dart';
711
import 'package:dart_mcp/server.dart';
812
import 'package:file/file.dart';
913
import 'package:file/local.dart';
1014
import 'package:meta/meta.dart';
1115
import 'package:process/process.dart';
16+
import 'package:stream_channel/stream_channel.dart';
1217
import 'package:unified_analytics/unified_analytics.dart';
1318

1419
import 'mixins/analyzer.dart';
@@ -60,6 +65,82 @@ final class DartMCPServer extends MCPServer
6065
'over using tools directly in a shell.',
6166
);
6267

68+
/// Runs the MCP server given command line arguments and an optional
69+
/// [Analytics] instance.
70+
///
71+
/// Returns a [Future] that completes with an exit code after the server has
72+
/// shut down.
73+
static Future<int> run(List<String> args, {Analytics? analytics}) async {
74+
final parsedArgs = argParser.parse(args);
75+
if (parsedArgs.flag(helpFlag)) {
76+
print(argParser.usage);
77+
return 0;
78+
}
79+
80+
DartMCPServer? server;
81+
final dartSdkPath =
82+
parsedArgs.option(dartSdkOption) ?? io.Platform.environment['DART_SDK'];
83+
final flutterSdkPath =
84+
parsedArgs.option(flutterSdkOption) ??
85+
io.Platform.environment['FLUTTER_SDK'];
86+
final logFilePath = parsedArgs.option(logFileOption);
87+
final logFileSink =
88+
logFilePath == null ? null : _createLogSink(io.File(logFilePath));
89+
runZonedGuarded(
90+
() {
91+
server = DartMCPServer(
92+
StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
93+
.transform(StreamChannelTransformer.fromCodec(utf8))
94+
.transformStream(const LineSplitter())
95+
.transformSink(
96+
StreamSinkTransformer.fromHandlers(
97+
handleData: (data, sink) {
98+
sink.add('$data\n');
99+
},
100+
),
101+
),
102+
forceRootsFallback: parsedArgs.flag(forceRootsFallbackFlag),
103+
sdk: Sdk.find(
104+
dartSdkPath: dartSdkPath,
105+
flutterSdkPath: flutterSdkPath,
106+
),
107+
analytics: analytics,
108+
protocolLogSink: logFileSink,
109+
)..done.whenComplete(() => logFileSink?.close());
110+
},
111+
(e, s) {
112+
if (server != null) {
113+
try {
114+
// Log unhandled errors to the client, if we managed to connect.
115+
server!.log(LoggingLevel.error, '$e\n$s');
116+
} catch (_) {}
117+
} else {
118+
// Otherwise log to stderr.
119+
io.stderr
120+
..writeln(e)
121+
..writeln(s);
122+
}
123+
},
124+
zoneSpecification: ZoneSpecification(
125+
print: (_, _, _, value) {
126+
if (server != null) {
127+
try {
128+
// Don't allow `print` since this breaks stdio communication, but
129+
// if we have a server we do log messages to the client.
130+
server!.log(LoggingLevel.info, value);
131+
} catch (_) {}
132+
}
133+
},
134+
),
135+
);
136+
if (server == null) {
137+
return 1;
138+
} else {
139+
await server!.done;
140+
return 0;
141+
}
142+
}
143+
63144
@override
64145
final LocalProcessManager processManager;
65146

@@ -117,4 +198,64 @@ final class DartMCPServer extends MCPServer
117198
},
118199
);
119200
}
201+
202+
static final argParser =
203+
ArgParser(allowTrailingOptions: false)
204+
..addOption(
205+
dartSdkOption,
206+
help:
207+
'The path to the root of the desired Dart SDK. Defaults to the '
208+
'DART_SDK environment variable.',
209+
)
210+
..addOption(
211+
flutterSdkOption,
212+
help:
213+
'The path to the root of the desired Flutter SDK. Defaults to '
214+
'the FLUTTER_SDK environment variable, then searching up from '
215+
'the Dart SDK.',
216+
)
217+
..addFlag(
218+
forceRootsFallbackFlag,
219+
negatable: true,
220+
defaultsTo: false,
221+
help:
222+
'Forces a behavior for project roots which uses MCP tools '
223+
'instead of the native MCP roots. This can be helpful for '
224+
'clients like cursor which claim to have roots support but do '
225+
'not actually support it.',
226+
)
227+
..addOption(
228+
logFileOption,
229+
help:
230+
'Path to a file to log all MPC protocol traffic to. File will be '
231+
'overwritten if it exists.',
232+
)
233+
..addFlag(helpFlag, abbr: 'h', help: 'Show usage text');
234+
}
235+
236+
const dartSdkOption = 'dart-sdk';
237+
const flutterSdkOption = 'flutter-sdk';
238+
const forceRootsFallbackFlag = 'force-roots-fallback';
239+
const helpFlag = 'help';
240+
const logFileOption = 'log-file';
241+
242+
/// Creates a `Sink<String>` for [logFile].
243+
Sink<String> _createLogSink(io.File logFile) {
244+
logFile.createSync(recursive: true);
245+
final fileByteSink = logFile.openWrite(
246+
mode: io.FileMode.write,
247+
encoding: utf8,
248+
);
249+
return fileByteSink.transform(
250+
StreamSinkTransformer.fromHandlers(
251+
handleData: (data, innerSink) {
252+
innerSink.add(utf8.encode(data));
253+
// It's a log, so we want to make sure it's always up-to-date.
254+
fileByteSink.flush();
255+
},
256+
handleDone: (innerSink) {
257+
innerSink.close();
258+
},
259+
),
260+
);
120261
}

0 commit comments

Comments
 (0)