| // 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:async/async.dart'; |
| import 'package:dart_mcp/server.dart'; |
| import 'package:dart_mcp/stdio.dart'; |
| import 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:process/process.dart'; |
| import 'package:unified_analytics/unified_analytics.dart'; |
| |
| import 'arg_parser.dart'; |
| import 'mixins/analytics.dart'; |
| import 'mixins/analyzer.dart'; |
| import 'mixins/dash_cli.dart'; |
| import 'mixins/dtd.dart'; |
| import 'mixins/flutter_launcher.dart'; |
| import 'mixins/grep_packages.dart'; |
| import 'mixins/package_uri_reader.dart'; |
| import 'mixins/prompts.dart'; |
| import 'mixins/pub.dart'; |
| import 'mixins/pub_dev_search.dart'; |
| import 'mixins/roots_fallback_support.dart'; |
| import 'utils/analytics.dart'; |
| import 'utils/file_system.dart'; |
| import 'utils/process_manager.dart'; |
| import 'utils/sdk.dart'; |
| import 'utils/tools_configuration.dart'; |
| |
| /// An MCP server for Dart and Flutter tooling. |
| final class DartMCPServer extends MCPServer |
| with |
| LoggingSupport, |
| ToolsSupport, |
| ResourcesSupport, |
| RootsTrackingSupport, |
| RootsFallbackSupport, |
| DashCliSupport, |
| DartAnalyzerSupport, |
| PubSupport, |
| PubDevSupport, |
| DartToolingDaemonSupport, |
| FlutterLauncherSupport, |
| PromptsSupport, |
| DashPrompts, |
| PackageUriSupport, |
| ElicitationRequestSupport, |
| GrepSupport, |
| AnalyticsEvents |
| implements |
| AnalyticsSupport, |
| ProcessManagerSupport, |
| FileSystemSupport, |
| SdkSupport, |
| ToolsConfigurationSupport { |
| /// A list of tool names to exclude from this version of the server. |
| /// |
| /// Used in [registerTool] to skip registering these tools. |
| final List<String> excludedTools; |
| |
| @override |
| final ToolsConfiguration toolsConfig; |
| |
| DartMCPServer( |
| super.channel, { |
| required this.sdk, |
| required this.toolsConfig, |
| this.analytics, |
| this.excludedTools = const [], |
| @visibleForTesting this.processManager = const LocalProcessManager(), |
| @visibleForTesting this.fileSystem = const LocalFileSystem(), |
| this.forceRootsFallback = false, |
| // Disabled due to https://github.com/flutter/flutter/issues/170357 |
| this.enableScreenshots = false, |
| super.protocolLogSink, |
| }) : super.fromStreamChannel( |
| implementation: Implementation( |
| name: 'dart and flutter tooling', |
| version: '0.1.3', |
| ), |
| instructions: |
| 'This server helps to connect Dart and Flutter developers to ' |
| 'their development tools and running applications.\n' |
| 'IMPORTANT: Prefer using an MCP tool provided by this server ' |
| 'over using tools directly in a shell.', |
| ); |
| |
| @override |
| Future<InitializeResult> initialize(InitializeRequest request) async { |
| final result = await super.initialize(request); |
| analytics?.send( |
| Event.dartMCPEvent( |
| client: clientInfo.name, |
| clientVersion: clientInfo.version, |
| serverVersion: implementation.version, |
| type: AnalyticsEvent.initialize.name, |
| additionalData: InitializeMetrics( |
| supportsElicitation: request.capabilities.elicitation != null, |
| supportsRoots: request.capabilities.roots != null, |
| supportsSampling: request.capabilities.sampling != null, |
| ), |
| ), |
| ); |
| return result; |
| } |
| |
| /// Runs the MCP server given command line arguments and an optional |
| /// [Analytics] instance. |
| /// |
| /// Returns a [Future] that completes with an exit code after the server has |
| /// shut down. |
| static Future<int> run(List<String> args, {Analytics? analytics}) async { |
| final parsedArgs = argParser.parse(args); |
| if (parsedArgs.flag(helpFlag)) { |
| print(argParser.usage); |
| return 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( |
| stdioChannel(input: io.stdin, output: io.stdout), |
| excludedTools: parsedArgs.multiOption(excludeToolOption), |
| toolsConfig: |
| ToolsConfiguration.fromArgs(parsedArgs) ?? ToolsConfiguration.all, |
| forceRootsFallback: parsedArgs.flag(forceRootsFallbackFlag), |
| sdk: Sdk.find( |
| dartSdkPath: dartSdkPath, |
| flutterSdkPath: flutterSdkPath, |
| ), |
| analytics: analytics, |
| 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 (_) {} |
| } |
| }, |
| ), |
| ); |
| if (server == null) { |
| return 1; |
| } else { |
| await server!.done; |
| return 0; |
| } |
| } |
| |
| /// The default arg parser for the MCP Server. |
| static final argParser = createArgParser(); |
| |
| @override |
| final ProcessManager processManager; |
| |
| @override |
| final FileSystem fileSystem; |
| |
| @override |
| final bool forceRootsFallback; |
| |
| @override |
| final Sdk sdk; |
| |
| @override |
| final Analytics? analytics; |
| |
| @override |
| final bool enableScreenshots; |
| |
| @override |
| /// Automatically logs all tool calls via analytics by wrapping the [impl], |
| /// if [analytics] is not `null`. |
| void registerTool( |
| Tool tool, |
| FutureOr<CallToolResult> Function(CallToolRequest) impl, { |
| bool validateArguments = true, |
| }) { |
| // Check manually excluded tools and skip them. |
| if (excludedTools.contains(tool.name)) return; |
| |
| super.registerTool(tool, impl, validateArguments: validateArguments); |
| } |
| } |
| |
| /// 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)); |
| }, |
| handleDone: (innerSink) async { |
| innerSink.close(); |
| }, |
| handleError: (Object e, StackTrace s, _) { |
| io.stderr.writeln( |
| 'Error in writing to log file ${logFile.path}: $e\n$s', |
| ); |
| }, |
| ), |
| ); |
| } |