blob: 9870f902ebe01315693a0e3257f401bdc3583a09 [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:async/async.dart';
import 'package:dart_mcp/server.dart';
import 'package:dart_mcp_server/dart_mcp_server.dart';
import 'package:stream_channel/stream_channel.dart';
void main(List<String> args) async {
final parsedArgs = argParser.parse(args);
if (parsedArgs.flag(help)) {
print(argParser.usage);
io.exit(0);
}
DartMCPServer? server;
final dartSdkPath =
parsedArgs.option(dartSdkOption) ?? io.Platform.environment['DART_SDK'];
final flutterSdkPath =
parsedArgs.option(flutterSdkOption) ??
io.Platform.environment['FLUTTER_SDK'];
final logFilePath = parsedArgs.option(logFileOption);
final logFileSink =
logFilePath == null ? null : createLogSink(io.File(logFilePath));
runZonedGuarded(
() {
server = DartMCPServer(
StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
.transform(StreamChannelTransformer.fromCodec(utf8))
.transformStream(const LineSplitter())
.transformSink(
StreamSinkTransformer.fromHandlers(
handleData: (data, sink) {
sink.add('$data\n');
},
),
),
forceRootsFallback: parsedArgs.flag(forceRootsFallback),
sdk: Sdk.find(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath),
analytics: null,
protocolLogSink: logFileSink,
)..done.whenComplete(() => logFileSink?.close());
},
(e, s) {
if (server != null) {
try {
// Log unhandled errors to the client, if we managed to connect.
server!.log(LoggingLevel.error, '$e\n$s');
} catch (_) {}
} else {
// Otherwise log to stderr.
io.stderr
..writeln(e)
..writeln(s);
}
},
zoneSpecification: ZoneSpecification(
print: (_, _, _, value) {
if (server != null) {
try {
// Don't allow `print` since this breaks stdio communication, but if
// we have a server we do log messages to the client.
server!.log(LoggingLevel.info, value);
} catch (_) {}
}
},
),
);
}
final argParser =
ArgParser(allowTrailingOptions: false)
..addOption(
dartSdkOption,
help:
'The path to the root of the desired Dart SDK. Defaults to the '
'DART_SDK environment variable.',
)
..addOption(
flutterSdkOption,
help:
'The path to the root of the desired Flutter SDK. Defaults to '
'the FLUTTER_SDK environment variable, then searching up from the '
'Dart SDK.',
)
..addFlag(
forceRootsFallback,
negatable: true,
defaultsTo: false,
help:
'Forces a behavior for project roots which uses MCP tools instead '
'of the native MCP roots. This can be helpful for clients like '
'cursor which claim to have roots support but do not actually '
'support it.',
)
..addOption(
logFileOption,
help:
'Path to a file to log all MPC protocol traffic to. File will be '
'overwritten if it exists.',
)
..addFlag(help, abbr: 'h', help: 'Show usage text');
const dartSdkOption = 'dart-sdk';
const flutterSdkOption = 'flutter-sdk';
const forceRootsFallback = 'force-roots-fallback';
const help = 'help';
const logFileOption = 'log-file';
/// Creates a `Sink<String>` for [logFile].
Sink<String> createLogSink(io.File logFile) {
logFile.createSync(recursive: true);
final fileByteSink = logFile.openWrite(
mode: io.FileMode.write,
encoding: utf8,
);
return fileByteSink.transform(
StreamSinkTransformer.fromHandlers(
handleData: (data, innerSink) {
innerSink.add(utf8.encode(data));
// It's a log, so we want to make sure it's always up-to-date.
fileByteSink.flush();
},
handleDone: (innerSink) {
innerSink.close();
},
),
);
}