|
3 | 3 | // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
|
5 | 5 | import 'dart:async'; |
| 6 | +import 'dart:convert'; |
| 7 | +import 'dart:io' as io; |
6 | 8 |
|
| 9 | +import 'package:args/args.dart'; |
| 10 | +import 'package:async/async.dart'; |
7 | 11 | import 'package:dart_mcp/server.dart'; |
8 | 12 | import 'package:file/file.dart'; |
9 | 13 | import 'package:file/local.dart'; |
10 | 14 | import 'package:meta/meta.dart'; |
11 | 15 | import 'package:process/process.dart'; |
| 16 | +import 'package:stream_channel/stream_channel.dart'; |
12 | 17 | import 'package:unified_analytics/unified_analytics.dart'; |
13 | 18 |
|
14 | 19 | import 'mixins/analyzer.dart'; |
@@ -60,6 +65,82 @@ final class DartMCPServer extends MCPServer |
60 | 65 | 'over using tools directly in a shell.', |
61 | 66 | ); |
62 | 67 |
|
| 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 | + |
63 | 144 | @override |
64 | 145 | final LocalProcessManager processManager; |
65 | 146 |
|
@@ -117,4 +198,64 @@ final class DartMCPServer extends MCPServer |
117 | 198 | }, |
118 | 199 | ); |
119 | 200 | } |
| 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 | + ); |
120 | 261 | } |
0 commit comments