| // Copyright (c) 2026, 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 'package:args/args.dart'; |
| |
| import 'arg_parser.dart'; |
| |
| /// The categories of features. |
| /// |
| /// Features may have multiple categories. |
| enum FeatureCategory { |
| /// The highest level category, can be used to disable all features |
| /// and then only enable the desired ones. |
| all(null), |
| |
| /// Features for interacting with the Dart Language Server. |
| analysis(all), |
| |
| /// Features which translate directly to a CLI command. |
| cli(all), |
| |
| /// Features that are specific to Dart projects only. |
| dart(all), |
| |
| /// Features that are specific to Flutter projects only. |
| flutter(all), |
| |
| /// Features for launching, stopping, and otherwise controlling the lifecycle |
| /// of Flutter apps. |
| flutterAppLifecycle(flutter, 'flutter_app_lifecycle'), |
| |
| /// Features that require use of flutter_driver to interact with the app. |
| flutterDriver(flutter, 'flutter_driver'), |
| |
| /// Features for interacting with running apps via the Dart Tooling Daemon. |
| dartToolingDaemon(all, 'dart_tooling_daemon'), |
| |
| /// Features for interacting with package dependencies, pub and/or pub.dev |
| packageDeps(all, 'package_deps'); |
| |
| const FeatureCategory(this.parent, [this._optionName]); |
| |
| final FeatureCategory? parent; |
| |
| final String? _optionName; |
| |
| String get name => _optionName ?? EnumName(this).name; |
| } |
| |
| /// Controls which features are enabled for a given configuration. |
| class FeaturesConfiguration { |
| final Set<String> enabledNames; |
| final Set<String> disabledNames; |
| |
| const FeaturesConfiguration({ |
| this.enabledNames = const {}, |
| this.disabledNames = const {}, |
| }); |
| |
| static FeaturesConfiguration fromArgs(ArgResults args) { |
| final disabled = args.multiOption(disabledFeaturesOption).toSet(); |
| if (args.option(toolsOption) == 'dart') { |
| // This is the equivalent of the deprecated --tools=dart option, |
| // just disable all flutter tools. |
| disabled.add(FeatureCategory.flutter.name); |
| } |
| return FeaturesConfiguration( |
| enabledNames: args.multiOption(enabledFeaturesOption).toSet(), |
| disabledNames: disabled, |
| ); |
| } |
| |
| /// Whether or not an MCP feature with [name] and [categories] should be |
| /// enabled based on precedence rules. |
| /// |
| /// The precedence order is: |
| /// |
| /// - Disabled by name |
| /// - Enabled by name |
| /// - For each transitive category, in order of their category depth and |
| /// then their given order: |
| /// - Disabled by category |
| /// - Enabled by category |
| bool isEnabled( |
| String name, |
| bool enabledByDefault, |
| List<FeatureCategory> categories, |
| ) { |
| if (disabledNames.contains(name)) return false; |
| if (enabledNames.contains(name)) return true; |
| for (var category in _categoriesInPrecedenceOrder(categories)) { |
| if (disabledNames.contains(category.name)) return false; |
| if (enabledNames.contains(category.name)) return true; |
| } |
| |
| // No configuration, return the default. |
| return enabledByDefault; |
| } |
| |
| /// Returns all the transitive categories from a list of categories in |
| /// precedence order. |
| /// |
| /// The precedence implementation is mostly a breadth first traversal of the |
| /// [categories] and their transitive parents, but with the following rules: |
| /// |
| /// - More specific ("deeper") categories are higher precedence. |
| /// - Earlier entries in [categories] are higher precedence than later |
| /// entries. |
| /// - When parent categories are the same distance away, the ones whose |
| /// children were earlier in [categories] are higher precedence. |
| Iterable<FeatureCategory> _categoriesInPrecedenceOrder( |
| List<FeatureCategory> categories, |
| ) sync* { |
| final seen = <FeatureCategory>{...categories}; |
| final queue = <FeatureCategory>[]; |
| |
| // Inserts `category` into queue ahead of any lower priority categories, |
| // categories are prioritized based on their distance to the top level |
| // category. |
| void insert(FeatureCategory category) { |
| final priority = distanceToTop(category); |
| for (var i = 0; i < queue.length; i++) { |
| final item = queue[i]; |
| if (distanceToTop(item) < priority) { |
| queue.insert(i, category); |
| return; |
| } |
| } |
| queue.add(category); |
| return; |
| } |
| |
| categories.forEach(insert); |
| while (queue.isNotEmpty) { |
| final category = queue.removeAt(0); |
| yield category; |
| if (category.parent case final parent?) { |
| if (seen.add(parent)) insert(parent); |
| } |
| } |
| } |
| |
| /// Returns the distance from [category] to the top of its `parent` |
| /// chain (ie: a `null` parent). |
| int distanceToTop(FeatureCategory category) { |
| var parent = category.parent; |
| var result = 0; |
| while (parent != null) { |
| result++; |
| parent = parent.parent; |
| } |
| return result; |
| } |
| } |
| |
| /// Extension that allows for setting/getting categories for MCP feature |
| /// definitions via an expando. |
| /// |
| /// These extension types do not have sensible shared subtypes so we have to |
| /// make this extension on [Object?]. |
| extension Categorized on Object? { |
| static final Expando<List<FeatureCategory>> _categories = Expando(); |
| |
| List<FeatureCategory> get categories { |
| assert(this is Map<String, Object?>); |
| // Every tool should have categories set. |
| assert(_categories[this as Object] != null); |
| return _categories[this as Object] ?? <FeatureCategory>[]; |
| } |
| |
| set categories(List<FeatureCategory> value) { |
| assert(this is Map<String, Object?>); |
| // Categories should only get set once. |
| assert(_categories[this as Object] == null); |
| // Every tool should have at least one category (can be `all`). |
| assert(value.isNotEmpty); |
| _categories[this as Object] = value; |
| } |
| } |
| |
| /// Extension that allows for setting/getting enabled by default configuration |
| /// for MCP tools via an expando. |
| /// |
| /// The default is true. |
| extension EnabledByDefault on Object? { |
| static final _enabledByDefault = Expando<bool>(); |
| |
| bool get enabledByDefault { |
| assert(this is Map<String, Object?>); |
| return _enabledByDefault[this as Object] ?? true; |
| } |
| |
| set enabledByDefault(bool value) { |
| assert(this is Map<String, Object?>); |
| _enabledByDefault[this as Object] = value; |
| } |
| } |