diff --git a/.github/workflows/dart_tooling_mcp_server.yaml b/.github/workflows/dart_mcp_server.yaml
similarity index 100%
rename from .github/workflows/dart_tooling_mcp_server.yaml
rename to .github/workflows/dart_mcp_server.yaml
diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml
index 54e3df5..1333e86 100644
--- a/.github/workflows/pull_request_label.yml
+++ b/.github/workflows/pull_request_label.yml
@@ -13,6 +13,7 @@
 jobs:
   label:
     permissions:
+      issues: write
       pull-requests: write
     runs-on: ubuntu-latest
     steps:
diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md
index 7ad45d7..2e5da88 100644
--- a/pkgs/dart_mcp/CHANGELOG.md
+++ b/pkgs/dart_mcp/CHANGELOG.md
@@ -1,4 +1,4 @@
-## 0.2.1-wip
+## 0.2.1
 
 - Fix the `protocolLogSink` support when using `MCPClient.connectStdioServer`.
 - Update workflow example to show thinking spinner and input and output token
diff --git a/pkgs/dart_mcp/pubspec.yaml b/pkgs/dart_mcp/pubspec.yaml
index 44dd545..2c5df2d 100644
--- a/pkgs/dart_mcp/pubspec.yaml
+++ b/pkgs/dart_mcp/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart_mcp
-version: 0.2.1-wip
+version: 0.2.1
 description: A package for making MCP servers and clients.
 repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp
 issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp
diff --git a/pkgs/dart_mcp_server/README.md b/pkgs/dart_mcp_server/README.md
index b4ae9d0..12e4adc 100644
--- a/pkgs/dart_mcp_server/README.md
+++ b/pkgs/dart_mcp_server/README.md
@@ -23,6 +23,8 @@
 | `hot_reload` | `runtime tool` | Performs a hot reload of the active Flutter application. |
 | `connect_dart_tooling_daemon`* | `configuration` | Connects to the locally running Dart Tooling Daemon. |
 | `get_active_location` | `editor` | Gets the active cursor position in the connected editor (if available). |
+| `run_tests` | `static tool` | Runs tests for the given project roots. |
+| `create_project` | `static tool` | Creates a new Dart or Flutter project. |
 
 > *Experimental: may be removed.
 
diff --git a/pkgs/dart_mcp_server/bin/main.dart b/pkgs/dart_mcp_server/bin/main.dart
index 6050251..351dc72 100644
--- a/pkgs/dart_mcp_server/bin/main.dart
+++ b/pkgs/dart_mcp_server/bin/main.dart
@@ -20,9 +20,14 @@
   }
 
   DartMCPServer? server;
-  await runZonedGuarded(
-    () async {
-      server = await DartMCPServer.connect(
+  final dartSdkPath =
+      parsedArgs.option(dartSdkOption) ?? io.Platform.environment['DART_SDK'];
+  final flutterSdkPath =
+      parsedArgs.option(flutterSdkOption) ??
+      io.Platform.environment['FLUTTER_SDK'];
+  runZonedGuarded(
+    () {
+      server = DartMCPServer(
         StreamChannel.withCloseGuarantee(io.stdin, io.stdout)
             .transform(StreamChannelTransformer.fromCodec(utf8))
             .transformStream(const LineSplitter())
@@ -34,6 +39,7 @@
               ),
             ),
         forceRootsFallback: parsedArgs.flag(forceRootsFallback),
+        sdk: Sdk.find(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath),
       );
     },
     (e, s) {
@@ -65,6 +71,19 @@
 
 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,
@@ -77,5 +96,7 @@
       )
       ..addFlag(help, abbr: 'h', help: 'Show usage text');
 
+const dartSdkOption = 'dart-sdk';
+const flutterSdkOption = 'flutter-sdk';
 const forceRootsFallback = 'force-roots-fallback';
 const help = 'help';
diff --git a/pkgs/dart_mcp_server/lib/dart_mcp_server.dart b/pkgs/dart_mcp_server/lib/dart_mcp_server.dart
index c1e1ead..badafe8 100644
--- a/pkgs/dart_mcp_server/lib/dart_mcp_server.dart
+++ b/pkgs/dart_mcp_server/lib/dart_mcp_server.dart
@@ -3,3 +3,4 @@
 // BSD-style license that can be found in the LICENSE file.
 
 export 'src/server.dart';
+export 'src/utils/sdk.dart' show Sdk;
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart b/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart
index da870e7..10ae558 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart
@@ -14,13 +14,15 @@
 
 import '../lsp/wire_format.dart';
 import '../utils/constants.dart';
+import '../utils/sdk.dart';
 
 /// Mix this in to any MCPServer to add support for analyzing Dart projects.
 ///
 /// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
 /// mixins applied.
 base mixin DartAnalyzerSupport
-    on ToolsSupport, LoggingSupport, RootsTrackingSupport {
+    on ToolsSupport, LoggingSupport, RootsTrackingSupport
+    implements SdkSupport {
   /// The LSP server connection for the analysis server.
   late final Peer _lspConnection;
 
@@ -52,9 +54,8 @@
       if (!supportsRoots)
         'Project analysis requires the "roots" capability which is not '
             'supported. Analysis tools have been disabled.',
-      if (Platform.environment['DART_SDK'] == null)
-        'Project analysis requires a "DART_SDK" environment variable to be set '
-            '(this should be the path to the root of the dart SDK). Analysis '
+      if (sdk.dartSdkPath == null)
+        'Project analysis requires a Dart SDK but none was given. Analysis '
             'tools have been disabled.',
     ];
 
@@ -90,7 +91,7 @@
   ///
   /// On failure, returns a reason for the failure.
   Future<String?> _initializeAnalyzerLspServer() async {
-    _lspServer = await Process.start('dart', [
+    _lspServer = await Process.start(sdk.dartExecutablePath, [
       'language-server',
       // Required even though it is documented as the default.
       // https://github.com/dart-lang/sdk/issues/60574
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart
index 7784dee..be32e73 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart
@@ -5,11 +5,13 @@
 import 'dart:async';
 
 import 'package:dart_mcp/server.dart';
+import 'package:path/path.dart' as p;
 
 import '../utils/cli_utils.dart';
 import '../utils/constants.dart';
 import '../utils/file_system.dart';
 import '../utils/process_manager.dart';
+import '../utils/sdk.dart';
 
 /// Mix this in to any MCPServer to add support for running Dart or Flutter CLI
 /// commands like `dart fix`, `dart format`, and `flutter test`.
@@ -17,17 +19,18 @@
 /// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
 /// mixins applied.
 base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
-    implements ProcessManagerSupport, FileSystemSupport {
+    implements ProcessManagerSupport, FileSystemSupport, SdkSupport {
   @override
   FutureOr<InitializeResult> initialize(InitializeRequest request) {
     try {
       return super.initialize(request);
     } finally {
-      // Can't call this until after `super.initialize`.
-      if (supportsRoots) {
+      // Can't call `supportsRoots` until after `super.initialize`.
+      if (supportsRoots && sdk.dartSdkPath != null) {
         registerTool(dartFixTool, _runDartFixTool);
         registerTool(dartFormatTool, _runDartFormatTool);
         registerTool(runTestsTool, _runTests);
+        registerTool(createProjectTool, _runCreateProjectTool);
       }
     }
   }
@@ -36,12 +39,13 @@
   Future<CallToolResult> _runDartFixTool(CallToolRequest request) async {
     return runCommandInRoots(
       request,
-      commandForRoot: (_, _) => 'dart',
+      commandForRoot: (_, _, sdk) => sdk.dartExecutablePath,
       arguments: ['fix', '--apply'],
       commandDescription: 'dart fix',
       processManager: processManager,
       knownRoots: await roots,
       fileSystem: fileSystem,
+      sdk: sdk,
     );
   }
 
@@ -49,13 +53,14 @@
   Future<CallToolResult> _runDartFormatTool(CallToolRequest request) async {
     return runCommandInRoots(
       request,
-      commandForRoot: (_, _) => 'dart',
+      commandForRoot: (_, _, sdk) => sdk.dartExecutablePath,
       arguments: ['format'],
       commandDescription: 'dart format',
       processManager: processManager,
       defaultPaths: ['.'],
       knownRoots: await roots,
       fileSystem: fileSystem,
+      sdk: sdk,
     );
   }
 
@@ -68,6 +73,99 @@
       processManager: processManager,
       knownRoots: await roots,
       fileSystem: fileSystem,
+      sdk: sdk,
+    );
+  }
+
+  /// Implementation of the [createProjectTool].
+  Future<CallToolResult> _runCreateProjectTool(CallToolRequest request) async {
+    final args = request.arguments;
+
+    final errors = createProjectTool.inputSchema.validate(args);
+    final projectType = args?[ParameterNames.projectType] as String?;
+    if (projectType != 'dart' && projectType != 'flutter') {
+      errors.add(
+        ValidationError(
+          ValidationErrorType.itemInvalid,
+          path: [ParameterNames.projectType],
+          details: 'Only `dart` and `flutter` are allowed values.',
+        ),
+      );
+    }
+    final directory = args![ParameterNames.directory] as String;
+    if (p.isAbsolute(directory)) {
+      errors.add(
+        ValidationError(
+          ValidationErrorType.itemInvalid,
+          path: [ParameterNames.directory],
+          details: 'Directory must be a relative path.',
+        ),
+      );
+    }
+    final platforms =
+        ((args[ParameterNames.platform] as List?)?.cast<String>() ?? [])
+            .toSet();
+    if (projectType == 'flutter') {
+      // Platforms are ignored for Dart, so no need to validate them.
+      final invalidPlatforms = platforms.difference(_allowedFlutterPlatforms);
+      if (invalidPlatforms.isNotEmpty) {
+        final plural =
+            invalidPlatforms.length > 1
+                ? 'are not valid platforms'
+                : 'is not a valid platform';
+        errors.add(
+          ValidationError(
+            ValidationErrorType.itemInvalid,
+            path: [ParameterNames.platform],
+            details:
+                '${invalidPlatforms.join(',')} $plural. Platforms '
+                '${_allowedFlutterPlatforms.map((e) => '`$e`').join(', ')} '
+                'are the only allowed values for the platform list argument.',
+          ),
+        );
+      }
+    }
+
+    if (errors.isNotEmpty) {
+      return CallToolResult(
+        content: [
+          for (final error in errors) Content.text(text: error.toErrorString()),
+        ],
+        isError: true,
+      );
+    }
+
+    final template = args[ParameterNames.template] as String?;
+
+    final commandArgs = [
+      'create',
+      if (template != null && template.isNotEmpty) ...['--template', template],
+      if (projectType == 'flutter' && platforms.isNotEmpty)
+        '--platform=${platforms.join(',')}',
+      // Create an "empty" project by default so the LLM doesn't have to deal
+      // with all the boilerplate and comments.
+      if (projectType == 'flutter' &&
+          (args[ParameterNames.empty] as bool? ?? true))
+        '--empty',
+      directory,
+    ];
+
+    return runCommandInRoot(
+      request,
+      arguments: commandArgs,
+      commandForRoot:
+          (_, _, sdk) =>
+              switch (projectType) {
+                    'dart' => sdk.dartExecutablePath,
+                    'flutter' => sdk.flutterExecutablePath,
+                    _ => StateError('Unknown project type: $projectType'),
+                  }
+                  as String,
+      commandDescription: '$projectType create',
+      fileSystem: fileSystem,
+      processManager: processManager,
+      knownRoots: await roots,
+      sdk: sdk,
     );
   }
 
@@ -97,4 +195,53 @@
       properties: {ParameterNames.roots: rootsSchema(supportsPaths: true)},
     ),
   );
+
+  static final createProjectTool = Tool(
+    name: 'create_project',
+    description: 'Creates a new Dart or Flutter project.',
+    annotations: ToolAnnotations(
+      title: 'Create project',
+      destructiveHint: true,
+    ),
+    inputSchema: Schema.object(
+      properties: {
+        ParameterNames.root: rootSchema,
+        ParameterNames.directory: Schema.string(
+          description:
+              'The subdirectory in which to create the project, must '
+              'be a relative path.',
+        ),
+        ParameterNames.projectType: Schema.string(
+          description: "The type of project: 'dart' or 'flutter'.",
+        ),
+        ParameterNames.template: Schema.string(
+          description:
+              'The project template to use (e.g., "console-full", "app").',
+        ),
+        ParameterNames.platform: Schema.list(
+          items: Schema.string(),
+          description:
+              'The list of platforms this project supports. Only valid '
+              'for Flutter projects. The allowed values are '
+              '${_allowedFlutterPlatforms.map((e) => '`$e`').join(', ')}. '
+              'Defaults to creating a project for all platforms.',
+        ),
+        ParameterNames.empty: Schema.bool(
+          description:
+              'Whether or not to create an "empty" project with minimized '
+              'boilerplate and example code. Defaults to true.',
+        ),
+      },
+      required: [ParameterNames.directory, ParameterNames.projectType],
+    ),
+  );
+
+  static const _allowedFlutterPlatforms = {
+    'web',
+    'linux',
+    'macos',
+    'windows',
+    'android',
+    'ios',
+  };
 }
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
index efd9524..1dfffe4 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
@@ -109,12 +109,14 @@
         final resource = Resource(
           uri: '$runtimeErrorsScheme://${debugSession.id}',
           name: debugSession.name,
+          description:
+              'Recent runtime errors seen for debug session '
+              '"${debugSession.name}".',
         );
         addResource(resource, (request) async {
-          final errors = errorService.errors;
           return ReadResourceResult(
             contents: [
-              for (var error in errors)
+              for (var error in errorService.errorLog.errors)
                 TextResourceContents(uri: resource.uri, text: error),
             ],
           );
@@ -294,7 +296,7 @@
           (await _AppErrorsListener.forVmService(
             vmService,
             this,
-          )).errors.clear();
+          )).errorLog.clear();
         }
 
         final vm = await vmService.getVM();
@@ -372,9 +374,9 @@
             vmService,
             this,
           );
-          final errors = errorService.errors;
+          final errorLog = errorService.errorLog;
 
-          if (errors.isEmpty) {
+          if (errorLog.errors.isEmpty) {
             return CallToolResult(
               content: [TextContent(text: 'No runtime errors found.')],
             );
@@ -383,14 +385,14 @@
             content: [
               TextContent(
                 text:
-                    'Found ${errors.length} '
-                    'error${errors.length == 1 ? '' : 's'}:\n',
+                    'Found ${errorLog.errors.length} '
+                    'error${errorLog.errors.length == 1 ? '' : 's'}:\n',
               ),
-              ...errors.map((e) => TextContent(text: e.toString())),
+              for (final e in errorLog.errors) TextContent(text: e.toString()),
             ],
           );
           if (request.arguments?['clearRuntimeErrors'] == true) {
-            errorService.errors.clear();
+            errorService.errorLog.clear();
           }
           return result;
         } catch (e) {
@@ -614,9 +616,9 @@
   static final getRuntimeErrorsTool = Tool(
     name: 'get_runtime_errors',
     description:
-        'Retrieves the list of runtime errors that have occurred in the active '
-        'Dart or Flutter application. Requires "${connectTool.name}" to be '
-        'successfully called first.',
+        'Retrieves the most recent runtime errors that have occurred in the '
+        'active Dart or Flutter application. Requires "${connectTool.name}" to '
+        'be successfully called first.',
     annotations: ToolAnnotations(
       title: 'Get runtime errors',
       readOnlyHint: true,
@@ -767,7 +769,7 @@
 /// Listens on a VM service for errors.
 class _AppErrorsListener {
   /// All the errors recorded so far (may be cleared explicitly).
-  final List<String> errors;
+  final ErrorLog errorLog;
 
   /// A broadcast stream of all errors that come in after you start listening.
   Stream<String> get errorsStream => _errorsController.stream;
@@ -785,7 +787,7 @@
   final VmService _vmService;
 
   _AppErrorsListener._(
-    this.errors,
+    this.errorLog,
     this._errorsController,
     this._extensionEventsListener,
     this._stderrEventsListener,
@@ -809,8 +811,8 @@
       // list but also expose it to clients so they can know when new errors
       // are added.
       final errorsController = StreamController<String>.broadcast();
-      final errors = <String>[];
-      errorsController.stream.listen(errors.add);
+      final errorLog = ErrorLog();
+      errorsController.stream.listen(errorLog.add);
       // We need to listen to streams with history so that we can get errors
       // that occurred before this tool call.
       // TODO(https://github.com/dart-lang/ai/issues/57): this can result in
@@ -849,7 +851,7 @@
         logger.log(LoggingLevel.error, 'Error subscribing to app errors: $e');
       }
       return _AppErrorsListener._(
-        errors,
+        errorLog,
         errorsController,
         extensionEvents,
         stderrEvents,
@@ -859,7 +861,7 @@
   }
 
   Future<void> shutdown() async {
-    errors.clear();
+    errorLog.clear();
     await _errorsController.close();
     await _extensionEventsListener.cancel();
     await _stderrEventsListener.cancel();
@@ -958,3 +960,52 @@
     if (vmServiceUri != null) 'vmServiceUri': vmServiceUri,
   });
 }
+
+/// Manages a log of errors with a maximum size in terms of total characters.
+@visibleForTesting
+class ErrorLog {
+  Iterable<String> get errors => _errors;
+  final List<String> _errors = [];
+  int _characters = 0;
+
+  /// The number of characters used by all errors in the log.
+  @visibleForTesting
+  int get characters => _characters;
+
+  final int _maxSize;
+
+  ErrorLog({
+    // One token is ~4 characters. Allow up to 5k tokens by default, so 20k
+    // characters.
+    int maxSize = 20000,
+  }) : _maxSize = maxSize;
+
+  /// Adds a new [error] to the log.
+  void add(String error) {
+    if (error.length > _maxSize) {
+      // If we get a single error over the max size, just trim it and clear
+      // all other errors.
+      final trimmed = error.substring(0, _maxSize);
+      _errors.clear();
+      _characters = trimmed.length;
+      _errors.add(trimmed);
+    } else {
+      // Otherwise, we append the error and then remove as many errors from the
+      // front as we need to in order to get under the max size.
+      _characters += error.length;
+      _errors.add(error);
+      var removeCount = 0;
+      while (_characters > _maxSize) {
+        _characters -= _errors[removeCount].length;
+        removeCount++;
+      }
+      _errors.removeRange(0, removeCount);
+    }
+  }
+
+  /// Clears all errors.
+  void clear() {
+    _characters = 0;
+    _errors.clear();
+  }
+}
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
index eaca4bc..ab48500 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
@@ -10,6 +10,7 @@
 import '../utils/constants.dart';
 import '../utils/file_system.dart';
 import '../utils/process_manager.dart';
+import '../utils/sdk.dart';
 
 /// Mix this in to any MCPServer to add support for running Pub commands like
 /// like `pub add` and `pub get`.
@@ -19,7 +20,7 @@
 /// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
 /// mixins applied.
 base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport
-    implements ProcessManagerSupport, FileSystemSupport {
+    implements ProcessManagerSupport, FileSystemSupport, SdkSupport {
   @override
   FutureOr<InitializeResult> initialize(InitializeRequest request) {
     try {
@@ -79,6 +80,7 @@
       processManager: processManager,
       knownRoots: await roots,
       fileSystem: fileSystem,
+      sdk: sdk,
     );
   }
 
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart
index fabed58..02b6ecd 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/pub_dev_search.dart
@@ -14,14 +14,11 @@
 /// Limit the number of concurrent requests.
 final _pool = Pool(10);
 
-/// The number of reults to return for a query.
+/// The number of results to return for a query.
 // If this should be set higher than 10 we need to implement paging of the
 // http://pub.dev/api/search endpoint.
 final _resultsLimit = 10;
 
-/// The number of identifiers we list per packages.
-final _maxIdentifiersListed = 200;
-
 /// Mix this in to any MCPServer to add support for doing searches on pub.dev.
 base mixin PubDevSupport on ToolsSupport {
   final _client = Client();
@@ -56,7 +53,7 @@
         return CallToolResult(
           content: [
             TextContent(
-              text: 'No packages mached the query, consider simplifying it.',
+              text: 'No packages matched the query, consider simplifying it.',
             ),
           ],
           isError: true,
@@ -80,9 +77,6 @@
                 (packageName) => (
                   versionListing: retrieve('api/packages/$packageName'),
                   score: retrieve('api/packages/$packageName/score'),
-                  docIndex: retrieve(
-                    'documentation/$packageName/latest/index.json',
-                  ),
                 ),
               )
               .toList();
@@ -94,18 +88,6 @@
         final packageName = packageNames[i];
         final versionListing = await subQueryFutures[i].versionListing;
         final scoreResult = await subQueryFutures[i].score;
-        final docIndex = await subQueryFutures[i].docIndex;
-
-        Map<String, Object?> identifiers(Object index) {
-          final items = dig<List>(index, []);
-          return {
-            'qualifiedNames': [
-              for (final item in items.take(_maxIdentifiersListed))
-                dig<String>(item, ['qualifiedName']),
-            ],
-          };
-        }
-
         results.add(
           TextContent(
             text: jsonEncode({
@@ -144,7 +126,6 @@
                         .where((t) => (t as String).startsWith('publisher:'))
                         .firstOrNull,
               },
-              if (docIndex != null) ...{'api': identifiers(docIndex)},
             }),
           ),
         );
@@ -164,8 +145,7 @@
     description:
         'Searches pub.dev for packages relevant to a given search query. '
         'The response will describe each result with its download count, '
-        'package description, topics, license, publisher, and a list of '
-        'identifiers in the public api.',
+        'package description, topics, license, and publisher.',
     annotations: ToolAnnotations(title: 'pub.dev search', readOnlyHint: true),
     inputSchema: Schema.object(
       properties: {
diff --git a/pkgs/dart_mcp_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart
index 072052e..d9d573d 100644
--- a/pkgs/dart_mcp_server/lib/src/server.dart
+++ b/pkgs/dart_mcp_server/lib/src/server.dart
@@ -2,14 +2,11 @@
 // 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 'package:dart_mcp/server.dart';
 import 'package:file/file.dart';
 import 'package:file/local.dart';
 import 'package:meta/meta.dart';
 import 'package:process/process.dart';
-import 'package:stream_channel/stream_channel.dart';
 
 import 'mixins/analyzer.dart';
 import 'mixins/dash_cli.dart';
@@ -19,6 +16,7 @@
 import 'mixins/roots_fallback_support.dart';
 import 'utils/file_system.dart';
 import 'utils/process_manager.dart';
+import 'utils/sdk.dart';
 
 /// An MCP server for Dart and Flutter tooling.
 final class DartMCPServer extends MCPServer
@@ -33,9 +31,10 @@
         PubSupport,
         PubDevSupport,
         DartToolingDaemonSupport
-    implements ProcessManagerSupport, FileSystemSupport {
+    implements ProcessManagerSupport, FileSystemSupport, SdkSupport {
   DartMCPServer(
     super.channel, {
+    required this.sdk,
     @visibleForTesting this.processManager = const LocalProcessManager(),
     @visibleForTesting this.fileSystem = const LocalFileSystem(),
     this.forceRootsFallback = false,
@@ -49,13 +48,6 @@
              'their development tools and running applications.',
        );
 
-  static Future<DartMCPServer> connect(
-    StreamChannel<String> mcpChannel, {
-    bool forceRootsFallback = false,
-  }) async {
-    return DartMCPServer(mcpChannel, forceRootsFallback: forceRootsFallback);
-  }
-
   @override
   final LocalProcessManager processManager;
 
@@ -64,4 +56,7 @@
 
   @override
   final bool forceRootsFallback;
+
+  @override
+  final Sdk sdk;
 }
diff --git a/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart b/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart
index 1ecf8b3..44e2389 100644
--- a/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart
+++ b/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart
@@ -11,6 +11,7 @@
 import 'package:yaml/yaml.dart';
 
 import 'constants.dart';
+import 'sdk.dart';
 
 /// The supported kinds of projects.
 enum ProjectKind {
@@ -71,7 +72,7 @@
 /// root's 'paths'.
 Future<CallToolResult> runCommandInRoots(
   CallToolRequest request, {
-  FutureOr<String> Function(Root, FileSystem) commandForRoot =
+  FutureOr<String> Function(Root, FileSystem, Sdk) commandForRoot =
       defaultCommandForRoot,
   List<String> arguments = const [],
   required String commandDescription,
@@ -79,6 +80,7 @@
   required ProcessManager processManager,
   required List<Root> knownRoots,
   List<String> defaultPaths = const <String>[],
+  required Sdk sdk,
 }) async {
   var rootConfigs =
       (request.arguments?[ParameterNames.roots] as List?)
@@ -91,120 +93,172 @@
     ];
   }
 
-  final outputs = <TextContent>[];
+  final outputs = <Content>[];
   for (var rootConfig in rootConfigs) {
-    final rootUriString = rootConfig[ParameterNames.root] as String?;
-    if (rootUriString == null) {
-      // This shouldn't happen based on the schema, but handle defensively.
-      return CallToolResult(
-        content: [
-          TextContent(text: 'Invalid root configuration: missing `root` key.'),
-        ],
-        isError: true,
-      );
-    }
-
-    final root = _findRoot(rootUriString, knownRoots);
-    if (root == null) {
-      return CallToolResult(
-        content: [
-          TextContent(
-            text:
-                'Invalid root $rootUriString, must be under one of the '
-                'registered project roots:\n\n${knownRoots.join('\n')}',
-          ),
-        ],
-        isError: true,
-      );
-    }
-
-    final rootUri = Uri.parse(rootUriString);
-    if (rootUri.scheme != 'file') {
-      return CallToolResult(
-        content: [
-          TextContent(
-            text:
-                'Only file scheme uris are allowed for roots, but got '
-                '$rootUri',
-          ),
-        ],
-        isError: true,
-      );
-    }
-    final projectRoot = fileSystem.directory(rootUri);
-
-    final commandWithPaths = <String>[
-      await commandForRoot(root, fileSystem),
-      ...arguments,
-    ];
-    final paths =
-        (rootConfig[ParameterNames.paths] as List?)?.cast<String>() ??
-        defaultPaths;
-    final invalidPaths = paths.where((path) {
-      final resolvedPath = rootUri.resolve(path).toString();
-      return rootUriString != resolvedPath &&
-          !p.isWithin(rootUriString, resolvedPath);
-    });
-    if (invalidPaths.isNotEmpty) {
-      return CallToolResult(
-        content: [
-          TextContent(
-            text:
-                'Paths are not allowed to escape their project root:\n'
-                '${invalidPaths.join('\n')}',
-          ),
-        ],
-        isError: true,
-      );
-    }
-    commandWithPaths.addAll(paths);
-
-    final result = await processManager.run(
-      commandWithPaths,
-      workingDirectory: projectRoot.path,
-      runInShell: true,
+    final result = await runCommandInRoot(
+      request,
+      rootConfig: rootConfig,
+      commandForRoot: commandForRoot,
+      arguments: arguments,
+      commandDescription: commandDescription,
+      fileSystem: fileSystem,
+      processManager: processManager,
+      knownRoots: knownRoots,
+      defaultPaths: defaultPaths,
+      sdk: sdk,
     );
-
-    final output = (result.stdout as String).trim();
-    final errors = (result.stderr as String).trim();
-    if (result.exitCode != 0) {
-      return CallToolResult(
-        content: [
-          TextContent(
-            text:
-                '$commandDescription failed in ${projectRoot.path}:\n'
-                '$output\n\nErrors\n$errors',
-          ),
-        ],
-        isError: true,
-      );
-    }
-    if (output.isNotEmpty) {
-      outputs.add(
-        TextContent(
-          text: '$commandDescription in ${projectRoot.path}:\n$output',
-        ),
-      );
-    }
+    if (result.isError == true) return result;
+    outputs.addAll(result.content);
   }
   return CallToolResult(content: outputs);
 }
 
+/// Runs [commandForRoot] in a single project root specified in the
+/// [request], with [arguments].
+///
+/// If [rootConfig] is passed, this will be used to read the root configuration,
+/// otherwise it is read directly off of `request.arguments`.
+///
+/// These [commandForRoot] plus [arguments] are passed directly to
+/// [ProcessManager.run].
+///
+/// The [commandDescription] is used in the output to describe the command
+/// being run. For example, if the command is `['dart', 'fix', '--apply']`, the
+/// command description might be `dart fix`.
+///
+/// [defaultPaths] may be specified if one or more path arguments are required
+/// for the command (e.g. `dart format <default paths>`). The paths can be
+/// absolute or relative paths that point to the directories on which the
+/// command should be run. For example, the `dart format` command may pass a
+/// default path of '.', which indicates that every Dart file in the working
+/// directory should be formatted. The value of `defaultPaths` will only be used
+/// if the [request]'s root configuration does not contain a set value for a
+/// root's 'paths'.
+Future<CallToolResult> runCommandInRoot(
+  CallToolRequest request, {
+  Map<String, Object?>? rootConfig,
+  FutureOr<String> Function(Root, FileSystem, Sdk) commandForRoot =
+      defaultCommandForRoot,
+  List<String> arguments = const [],
+  required String commandDescription,
+  required FileSystem fileSystem,
+  required ProcessManager processManager,
+  required List<Root> knownRoots,
+  List<String> defaultPaths = const <String>[],
+  required Sdk sdk,
+}) async {
+  rootConfig ??= request.arguments;
+  final rootUriString = rootConfig?[ParameterNames.root] as String?;
+  if (rootUriString == null) {
+    // This shouldn't happen based on the schema, but handle defensively.
+    return CallToolResult(
+      content: [
+        TextContent(text: 'Invalid root configuration: missing `root` key.'),
+      ],
+      isError: true,
+    );
+  }
+
+  final root = _findRoot(rootUriString, knownRoots);
+  if (root == null) {
+    return CallToolResult(
+      content: [
+        TextContent(
+          text:
+              'Invalid root $rootUriString, must be under one of the '
+              'registered project roots:\n\n${knownRoots.join('\n')}',
+        ),
+      ],
+      isError: true,
+    );
+  }
+
+  final rootUri = Uri.parse(rootUriString);
+  if (rootUri.scheme != 'file') {
+    return CallToolResult(
+      content: [
+        TextContent(
+          text:
+              'Only file scheme uris are allowed for roots, but got '
+              '$rootUri',
+        ),
+      ],
+      isError: true,
+    );
+  }
+  final projectRoot = fileSystem.directory(rootUri);
+
+  final commandWithPaths = <String>[
+    await commandForRoot(root, fileSystem, sdk),
+    ...arguments,
+  ];
+  final paths =
+      (rootConfig?[ParameterNames.paths] as List?)?.cast<String>() ??
+      defaultPaths;
+  final invalidPaths = paths.where((path) {
+    final resolvedPath = rootUri.resolve(path).toString();
+    return rootUriString != resolvedPath &&
+        !p.isWithin(rootUriString, resolvedPath);
+  });
+  if (invalidPaths.isNotEmpty) {
+    return CallToolResult(
+      content: [
+        TextContent(
+          text:
+              'Paths are not allowed to escape their project root:\n'
+              '${invalidPaths.join('\n')}',
+        ),
+      ],
+      isError: true,
+    );
+  }
+  commandWithPaths.addAll(paths);
+
+  final result = await processManager.run(
+    commandWithPaths,
+    workingDirectory: projectRoot.path,
+    runInShell: true,
+  );
+
+  final output = (result.stdout as String).trim();
+  final errors = (result.stderr as String).trim();
+  if (result.exitCode != 0) {
+    return CallToolResult(
+      content: [
+        TextContent(
+          text:
+              '$commandDescription failed in ${projectRoot.path}:\n'
+              '$output\n\nErrors\n$errors',
+        ),
+      ],
+      isError: true,
+    );
+  }
+  return CallToolResult(
+    content: [
+      TextContent(text: '$commandDescription in ${projectRoot.path}:\n$output'),
+    ],
+  );
+}
+
 /// Returns 'dart' or 'flutter' based on the pubspec contents.
 ///
 /// Throws an [ArgumentError] if there is no pubspec.
-Future<String> defaultCommandForRoot(Root root, FileSystem fileSystem) async =>
-    switch (await inferProjectKind(root, fileSystem)) {
-      ProjectKind.dart => 'dart',
-      ProjectKind.flutter => 'flutter',
-      ProjectKind.unknown =>
-        throw ArgumentError.value(
-          root.uri,
-          'root.uri',
-          'Unknown project kind at root ${root.uri}. All projects must have a '
-              'pubspec.',
-        ),
-    };
+Future<String> defaultCommandForRoot(
+  Root root,
+  FileSystem fileSystem,
+  Sdk sdk,
+) async => switch (await inferProjectKind(root, fileSystem)) {
+  ProjectKind.dart => sdk.dartExecutablePath,
+  ProjectKind.flutter => sdk.flutterExecutablePath,
+  ProjectKind.unknown =>
+    throw ArgumentError.value(
+      root.uri,
+      'root.uri',
+      'Unknown project kind at root ${root.uri}. All projects must have a '
+          'pubspec.',
+    ),
+};
 
 /// Returns whether or not [rootUri] is an allowed root, either exactly matching
 /// or under on of the [knownRoots].
@@ -224,12 +278,7 @@
   title: 'All projects roots to run this tool in.',
   items: Schema.object(
     properties: {
-      ParameterNames.root: Schema.string(
-        title: 'The URI of the project root to run this tool in.',
-        description:
-            'This must be equal to or a subdirectory of one of the roots '
-            'returned by a call to "listRoots".',
-      ),
+      ParameterNames.root: rootSchema,
       if (supportsPaths)
         ParameterNames.paths: Schema.list(
           title:
@@ -242,6 +291,14 @@
   ),
 );
 
+final rootSchema = Schema.string(
+  title: 'The file URI of the project root to run this tool in.',
+  description:
+      'This must be equal to or a subdirectory of one of the roots '
+      'allowed by the client. Must be a URI with a `file:` '
+      'scheme (e.g. file:///absolute/path/to/root).',
+);
+
 /// Very thin extension type for a pubspec just containing what we need.
 ///
 /// We assume a valid pubspec.
diff --git a/pkgs/dart_mcp_server/lib/src/utils/constants.dart b/pkgs/dart_mcp_server/lib/src/utils/constants.dart
index 78d3edf..a5a2729 100644
--- a/pkgs/dart_mcp_server/lib/src/utils/constants.dart
+++ b/pkgs/dart_mcp_server/lib/src/utils/constants.dart
@@ -8,14 +8,19 @@
 extension ParameterNames on Never {
   static const column = 'column';
   static const command = 'command';
+  static const directory = 'directory';
+  static const empty = 'empty';
   static const line = 'line';
   static const name = 'name';
   static const packageName = 'packageName';
   static const paths = 'paths';
+  static const platform = 'platform';
   static const position = 'position';
+  static const projectType = 'projectType';
   static const query = 'query';
   static const root = 'root';
   static const roots = 'roots';
+  static const template = 'template';
   static const uri = 'uri';
   static const uris = 'uris';
 }
diff --git a/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart b/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart
index af6786c..e8c52c1 100644
--- a/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart
+++ b/pkgs/dart_mcp_server/lib/src/utils/process_manager.dart
@@ -16,5 +16,5 @@
 /// implement this class and use [processManager] instead of making direct calls
 /// to dart:io's [Process] class.
 abstract interface class ProcessManagerSupport {
-  LocalProcessManager get processManager;
+  ProcessManager get processManager;
 }
diff --git a/pkgs/dart_mcp_server/lib/src/utils/sdk.dart b/pkgs/dart_mcp_server/lib/src/utils/sdk.dart
new file mode 100644
index 0000000..f3f5523
--- /dev/null
+++ b/pkgs/dart_mcp_server/lib/src/utils/sdk.dart
@@ -0,0 +1,86 @@
+// 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:io';
+
+import 'package:path/path.dart' as p;
+
+/// An interface class that provides a single getter of type [Sdk].
+///
+/// This provides information about the Dart and Flutter sdks, if available.
+abstract interface class SdkSupport {
+  Sdk get sdk;
+}
+
+/// Information about the Dart and Flutter SDKs, if available.
+class Sdk {
+  /// The path to the root of the Dart SDK.
+  final String? dartSdkPath;
+
+  /// The path to the root of the Flutter SDK.
+  final String? flutterSdkPath;
+
+  Sdk({this.dartSdkPath, this.flutterSdkPath});
+
+  /// Creates an [Sdk] from the path to the Dart SDK.
+  ///
+  /// If no [dartSdkPath] is given, this will attempt to find one using
+  /// [Platform.resolvedExecutable], assuming that is the `dart` binary
+  /// under the `bin` dir of a Dart SDK.
+  ///
+  /// Validates that the path is valid by checking for the `version` file.
+  ///
+  /// If no [flutterSdkPath] is given, this will search up from the resolved
+  /// Dart SDK path to see if it is nested inside a Flutter SDK.
+  factory Sdk.find({String? dartSdkPath, String? flutterSdkPath}) {
+    // Assume that we are running from the Dart SDK bin dir if not given any
+    // other configuration.
+    dartSdkPath ??= p.dirname(p.dirname(Platform.resolvedExecutable));
+
+    final versionFile = dartSdkPath.child('version');
+    if (!File(versionFile).existsSync()) {
+      throw ArgumentError('Invalid Dart SDK path: $dartSdkPath');
+    }
+
+    // Check if this is nested inside a Flutter SDK.
+    if (dartSdkPath.parent case final cacheDir
+        when cacheDir.basename == 'cache' && flutterSdkPath == null) {
+      if (cacheDir.parent case final binDir when binDir.basename == 'bin') {
+        final flutterExecutable = binDir.child('flutter');
+        if (File(flutterExecutable).existsSync()) {
+          flutterSdkPath = binDir.parent;
+        }
+      }
+    }
+
+    return Sdk(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath);
+  }
+
+  /// The path to the `dart` executable.
+  ///
+  /// Throws an [ArgumentError] if [dartSdkPath] is `null`.
+  String get dartExecutablePath =>
+      dartSdkPath?.child('bin').child('dart') ??
+      (throw ArgumentError(
+        'Dart SDK location unknown, try setting the DART_SDK environment '
+        'variable.',
+      ));
+
+  /// The path to the `flutter` executable.
+  ///
+  /// Throws an [ArgumentError] if [flutterSdkPath] is `null`.
+  String get flutterExecutablePath =>
+      flutterSdkPath?.child('bin').child('flutter') ??
+      (throw ArgumentError(
+        'Flutter SDK location unknown. To work on flutter projects, you must '
+        'spawn the server using `dart` from the flutter SDK and not a Dart '
+        'SDK, or set a FLUTTER_SDK environment variable.',
+      ));
+}
+
+extension on String {
+  String get basename => p.basename(this);
+  String child(String path) => p.join(this, path);
+  String get parent => p.dirname(this);
+}
diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml
index 3eacc6a..5b257e4 100644
--- a/pkgs/dart_mcp_server/pubspec.yaml
+++ b/pkgs/dart_mcp_server/pubspec.yaml
@@ -15,7 +15,7 @@
 dependencies:
   args: ^2.7.0
   async: ^2.13.0
-  dart_mcp: ^0.2.0
+  dart_mcp: ^0.2.1
   dds_service_extensions: ^2.0.1
   devtools_shared: ^11.2.0
   dtd: ^2.4.0
diff --git a/pkgs/dart_mcp_server/test/test_harness.dart b/pkgs/dart_mcp_server/test/test_harness.dart
index 1a9bdae..a0c0f1c 100644
--- a/pkgs/dart_mcp_server/test/test_harness.dart
+++ b/pkgs/dart_mcp_server/test/test_harness.dart
@@ -11,6 +11,7 @@
 import 'package:dart_mcp_server/src/mixins/dtd.dart';
 import 'package:dart_mcp_server/src/server.dart';
 import 'package:dart_mcp_server/src/utils/constants.dart';
+import 'package:dart_mcp_server/src/utils/sdk.dart';
 import 'package:dtd/dtd.dart';
 import 'package:file/file.dart';
 import 'package:file/local.dart';
@@ -34,6 +35,7 @@
   final DartToolingMCPClient mcpClient;
   final ServerConnectionPair serverConnectionPair;
   final FileSystem fileSystem;
+  final Sdk sdk;
 
   ServerConnection get mcpServerConnection =>
       serverConnectionPair.serverConnection;
@@ -43,6 +45,7 @@
     this.serverConnectionPair,
     this.fakeEditorExtension,
     this.fileSystem,
+    this.sdk,
   );
 
   /// Starts a Dart Tooling Daemon as well as an MCP client and server, and
@@ -64,6 +67,10 @@
     bool inProcess = false,
     FileSystem? fileSystem,
   }) async {
+    final sdk = Sdk.find(
+      dartSdkPath: Platform.environment['DART_SDK'],
+      flutterSdkPath: Platform.environment['FLUTTER_SDK'],
+    );
     fileSystem ??= const LocalFileSystem();
 
     final mcpClient = DartToolingMCPClient();
@@ -73,13 +80,14 @@
       mcpClient,
       inProcess,
       fileSystem,
+      sdk,
     );
     final connection = serverConnectionPair.serverConnection;
     connection.onLog.listen((log) {
       printOnFailure('MCP Server Log: $log');
     });
 
-    final fakeEditorExtension = await FakeEditorExtension.connect();
+    final fakeEditorExtension = await FakeEditorExtension.connect(sdk);
     addTearDown(fakeEditorExtension.shutdown);
 
     return TestHarness._(
@@ -87,6 +95,7 @@
       serverConnectionPair,
       fakeEditorExtension,
       fileSystem,
+      sdk,
     );
   }
 
@@ -102,6 +111,7 @@
       appPath,
       isFlutter: isFlutter,
       args: args,
+      sdk: sdk,
     );
     await fakeEditorExtension.addDebugSession(session);
     final root = rootForPath(projectRoot);
@@ -192,15 +202,20 @@
     String appPath, {
     List<String> args = const [],
     required bool isFlutter,
+    required Sdk sdk,
   }) async {
-    final process = await TestProcess.start(isFlutter ? 'flutter' : 'dart', [
-      'run',
-      '--no${isFlutter ? '' : '-serve'}-devtools',
-      if (!isFlutter) '--enable-vm-service=0',
-      if (isFlutter) ...['-d', 'flutter-tester'],
-      appPath,
-      ...args,
-    ], workingDirectory: projectRoot);
+    final process = await TestProcess.start(
+      isFlutter ? sdk.flutterExecutablePath : sdk.dartExecutablePath,
+      [
+        'run',
+        '--no${isFlutter ? '' : '-serve'}-devtools',
+        if (!isFlutter) '--enable-vm-service=0',
+        if (isFlutter) ...['-d', 'flutter-tester'],
+        appPath,
+        ...args,
+      ],
+      workingDirectory: projectRoot,
+    );
 
     addTearDown(() async {
       await kill(process, isFlutter);
@@ -284,8 +299,10 @@
   static int get nextId => ++_nextId;
   static int _nextId = 0;
 
-  static Future<FakeEditorExtension> connect() async {
-    final dtdProcess = await TestProcess.start('dart', ['tooling-daemon']);
+  static Future<FakeEditorExtension> connect(Sdk sdk) async {
+    final dtdProcess = await TestProcess.start(sdk.dartExecutablePath, [
+      'tooling-daemon',
+    ]);
     final dtdUri = await _getDTDUri(dtdProcess);
     final dtd = await DartToolingDaemon.connect(Uri.parse(dtdUri));
     final extension = FakeEditorExtension._(dtd, dtdProcess, dtdUri);
@@ -369,6 +386,7 @@
   MCPClient client,
   bool inProcess,
   FileSystem fileSystem,
+  Sdk sdk,
 ) async {
   ServerConnection connection;
   DartMCPServer? server;
@@ -393,11 +411,12 @@
       serverChannel,
       processManager: TestProcessManager(),
       fileSystem: fileSystem,
+      sdk: sdk,
     );
     addTearDown(server.shutdown);
     connection = client.connectServer(clientChannel);
   } else {
-    connection = await client.connectStdioServer('dart', [
+    connection = await client.connectStdioServer(sdk.dartExecutablePath, [
       'pub', // Using `pub` gives us incremental compilation
       'run',
       'bin/main.dart',
@@ -470,12 +489,9 @@
     if (item.workingDirectory != value.workingDirectory) {
       return false;
     }
-    if (item.command.length != value.command.length) {
+    if (!equals(value.command).matches(item.command, matchState)) {
       return false;
     }
-    for (var i = 0; i < item.command.length; i++) {
-      if (item.command[i] != value.command[i]) return false;
-    }
     return true;
   }
 }
diff --git a/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart b/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart
index d63677f..1473741 100644
--- a/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/dart_cli_test.dart
@@ -39,13 +39,13 @@
     exampleFlutterAppRoot = testHarness.rootForPath(flutterExample.io.path);
     dartCliAppRoot = testHarness.rootForPath(dartCliAppsPath);
 
-    testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
     await pumpEventQueue();
   });
 
   group('cli tools', () {
     late Tool dartFixTool;
     late Tool dartFormatTool;
+    late Tool createProjectTool;
 
     setUp(() async {
       final tools = (await testHarness.mcpServerConnection.listTools()).tools;
@@ -55,9 +55,13 @@
       dartFormatTool = tools.singleWhere(
         (t) => t.name == DashCliSupport.dartFormatTool.name,
       );
+      createProjectTool = tools.singleWhere(
+        (t) => t.name == DashCliSupport.createProjectTool.name,
+      );
     });
 
     test('dart fix', () async {
+      testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
       final request = CallToolRequest(
         name: dartFixTool.name,
         arguments: {
@@ -72,13 +76,14 @@
       expect(result.isError, isNot(true));
       expect(testProcessManager.commandsRan, [
         equalsCommand((
-          command: ['dart', 'fix', '--apply'],
+          command: [endsWith('dart'), 'fix', '--apply'],
           workingDirectory: exampleFlutterAppRoot.path,
         )),
       ]);
     });
 
     test('dart format', () async {
+      testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
       final request = CallToolRequest(
         name: dartFormatTool.name,
         arguments: {
@@ -93,13 +98,14 @@
       expect(result.isError, isNot(true));
       expect(testProcessManager.commandsRan, [
         equalsCommand((
-          command: ['dart', 'format', '.'],
+          command: [endsWith('dart'), 'format', '.'],
           workingDirectory: exampleFlutterAppRoot.path,
         )),
       ]);
     });
 
     test('dart format with paths', () async {
+      testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
       final request = CallToolRequest(
         name: dartFormatTool.name,
         arguments: {
@@ -117,7 +123,7 @@
       expect(result.isError, isNot(true));
       expect(testProcessManager.commandsRan, [
         equalsCommand((
-          command: ['dart', 'format', 'foo.dart', 'bar.dart'],
+          command: [endsWith('dart'), 'format', 'foo.dart', 'bar.dart'],
           workingDirectory: exampleFlutterAppRoot.path,
         )),
       ]);
@@ -125,6 +131,7 @@
 
     test('flutter and dart package tests with paths', () async {
       testHarness.mcpClient.addRoot(dartCliAppRoot);
+      testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
       await pumpEventQueue();
       final request = CallToolRequest(
         name: DashCliSupport.runTestsTool.name,
@@ -147,14 +154,227 @@
       expect(result.isError, isNot(true));
       expect(testProcessManager.commandsRan, [
         equalsCommand((
-          command: ['flutter', 'test', 'foo_test.dart', 'bar_test.dart'],
+          command: [
+            endsWith('flutter'),
+            'test',
+            'foo_test.dart',
+            'bar_test.dart',
+          ],
           workingDirectory: exampleFlutterAppRoot.path,
         )),
         equalsCommand((
-          command: ['dart', 'test', 'zip_test.dart'],
+          command: [endsWith('dart'), 'test', 'zip_test.dart'],
           workingDirectory: dartCliAppRoot.path,
         )),
       ]);
     });
+
+    group('create', () {
+      test('creates a Dart project', () async {
+        testHarness.mcpClient.addRoot(dartCliAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: dartCliAppRoot.uri,
+            ParameterNames.directory: 'new_app',
+            ParameterNames.projectType: 'dart',
+            ParameterNames.template: 'cli',
+          },
+        );
+        await testHarness.callToolWithRetry(request);
+
+        expect(testProcessManager.commandsRan, [
+          equalsCommand((
+            command: [
+              endsWith('dart'),
+              'create',
+              '--template',
+              'cli',
+              'new_app',
+            ],
+            workingDirectory: dartCliAppRoot.path,
+          )),
+        ]);
+      });
+
+      test('creates a Flutter project', () async {
+        testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: exampleFlutterAppRoot.uri,
+            ParameterNames.directory: 'new_app',
+            ParameterNames.projectType: 'flutter',
+            ParameterNames.template: 'app',
+          },
+        );
+        await testHarness.callToolWithRetry(request);
+
+        expect(testProcessManager.commandsRan, [
+          equalsCommand((
+            command: [
+              endsWith('flutter'),
+              'create',
+              '--template',
+              'app',
+              '--empty',
+              'new_app',
+            ],
+            workingDirectory: exampleFlutterAppRoot.path,
+          )),
+        ]);
+      });
+
+      test('creates a non-empty Flutter project', () async {
+        testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: exampleFlutterAppRoot.uri,
+            ParameterNames.directory: 'new_full_app',
+            ParameterNames.projectType: 'flutter',
+            ParameterNames.template: 'app',
+            ParameterNames.empty:
+                false, // Explicitly create a non-empty project
+          },
+        );
+        await testHarness.callToolWithRetry(request);
+
+        expect(testProcessManager.commandsRan, [
+          equalsCommand((
+            command: [
+              endsWith('flutter'),
+              'create',
+              '--template',
+              'app',
+              // Note: --empty is NOT present
+              'new_full_app',
+            ],
+            workingDirectory: exampleFlutterAppRoot.path,
+          )),
+        ]);
+      });
+
+      test('fails with invalid platform for Flutter project', () async {
+        testHarness.mcpClient.addRoot(exampleFlutterAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: exampleFlutterAppRoot.uri,
+            ParameterNames.directory: 'my_app_invalid_platform',
+            ParameterNames.projectType: 'flutter',
+            ParameterNames.platform: ['atari_jaguar', 'web'], // One invalid
+          },
+        );
+        final result = await testHarness.callToolWithRetry(
+          request,
+          expectError: true,
+        );
+
+        expect(result.isError, isTrue);
+        expect(
+          (result.content.first as TextContent).text,
+          allOf(
+            contains('atari_jaguar is not a valid platform.'),
+            contains(
+              'Platforms `web`, `linux`, `macos`, `windows`, `android`, `ios` '
+              'are the only allowed values',
+            ),
+          ),
+        );
+        expect(testProcessManager.commandsRan, isEmpty);
+      });
+
+      test('fails if projectType is missing', () async {
+        testHarness.mcpClient.addRoot(dartCliAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: dartCliAppRoot.uri,
+            ParameterNames.directory: 'my_app_no_type',
+          },
+        );
+        final result = await testHarness.callToolWithRetry(
+          request,
+          expectError: true,
+        );
+
+        expect(result.isError, isTrue);
+        expect(
+          (result.content.first as TextContent).text,
+          contains('Required property "projectType" is missing'),
+        );
+        expect(testProcessManager.commandsRan, isEmpty);
+      });
+
+      test('fails with invalid projectType', () async {
+        testHarness.mcpClient.addRoot(dartCliAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: dartCliAppRoot.uri,
+            ParameterNames.directory: 'my_app_invalid_type',
+            ParameterNames.projectType: 'java', // Invalid type
+          },
+        );
+        final result = await testHarness.callToolWithRetry(
+          request,
+          expectError: true,
+        );
+
+        expect(result.isError, isTrue);
+        expect(
+          (result.content.first as TextContent).text,
+          contains('Only `dart` and `flutter` are allowed values.'),
+        );
+        expect(testProcessManager.commandsRan, isEmpty);
+      });
+
+      test('fails if directory (project name) is an absolute path', () async {
+        testHarness.mcpClient.addRoot(dartCliAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.root: dartCliAppRoot.uri,
+            ParameterNames.directory: '/an/absolute/path/project',
+            ParameterNames.projectType: 'dart',
+          },
+        );
+        final result = await testHarness.callToolWithRetry(
+          request,
+          expectError: true,
+        );
+
+        expect(result.isError, isTrue);
+        expect(
+          (result.content.first as TextContent).text,
+          contains('Directory must be a relative path'),
+        );
+        expect(testProcessManager.commandsRan, isEmpty);
+      });
+
+      test('requires a root to be passed', () async {
+        testHarness.mcpClient.addRoot(dartCliAppRoot);
+        final request = CallToolRequest(
+          name: createProjectTool.name,
+          arguments: {
+            ParameterNames.directory: 'new_app',
+            ParameterNames.projectType: 'dart',
+            ParameterNames.template: 'cli',
+          },
+        );
+        final result = await testHarness.callToolWithRetry(
+          request,
+          expectError: true,
+        );
+
+        expect(result.isError, true);
+        expect(
+          (result.content.first as TextContent).text,
+          contains('missing `root` key'),
+        );
+        expect(testProcessManager.commandsRan, isEmpty);
+      });
+    });
   });
 }
diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart
index 83849e3..349a7fe 100644
--- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart
@@ -576,6 +576,64 @@
       });
     });
   });
+
+  group('ErrorLog', () {
+    test('adds errors and respects max size', () {
+      final log = ErrorLog(maxSize: 10);
+      log.add('abc');
+      expect(log.errors, ['abc']);
+      expect(log.characters, 3);
+
+      log.add('defg');
+      expect(log.errors, ['abc', 'defg']);
+      expect(log.characters, 7);
+
+      log.add('hijkl');
+      expect(log.errors, ['defg', 'hijkl']);
+      expect(log.characters, 9);
+
+      log.add('mnopq');
+      expect(log.errors, ['hijkl', 'mnopq']);
+      expect(log.characters, 10);
+    });
+
+    test('handles single error larger than max size', () {
+      final log = ErrorLog(maxSize: 10);
+      log.add('abcdefghijkl');
+      expect(log.errors, ['abcdefghij']);
+      expect(log.characters, 10);
+
+      log.add('mnopqrstuvwxyz');
+      expect(log.errors, ['mnopqrstuv']);
+      expect(log.characters, 10);
+    });
+
+    test('clear removes all errors', () {
+      final log = ErrorLog(maxSize: 10);
+      log
+        ..add('abc')
+        ..add('def');
+      log.clear();
+      expect(log.errors, isEmpty);
+      expect(log.characters, 0);
+    });
+
+    test('add, clear,clear and then add again', () {
+      final log = ErrorLog(maxSize: 10);
+      log
+        ..add('abc')
+        ..add('def');
+      log.clear();
+      expect(log.errors, isEmpty);
+      expect(log.characters, 0);
+      log.add('ghi');
+      expect(log.errors, ['ghi']);
+      expect(log.characters, 3);
+      log.add('jklmnopqrstuv');
+      expect(log.errors, ['jklmnopqrs']);
+      expect(log.characters, 10);
+    });
+  });
 }
 
 extension on Iterable<Resource> {
diff --git a/pkgs/dart_mcp_server/test/tools/pub_dev_search_test.dart b/pkgs/dart_mcp_server/test/tools/pub_dev_search_test.dart
index eedf697..eca24f7 100644
--- a/pkgs/dart_mcp_server/test/tools/pub_dev_search_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/pub_dev_search_test.dart
@@ -82,9 +82,6 @@
             'license:osi-approved',
           ],
           'publisher': 'publisher:google.dev',
-          'api': {
-            'qualifiedNames': containsAll(['retry', 'retry.RetryOptions']),
-          },
         });
       });
     }, _GoldenResponseClient.new);
@@ -161,7 +158,7 @@
           expect(result.isError, isTrue);
           expect(
             (result.content[0] as TextContent).text,
-            contains('No packages mached the query, consider simplifying it'),
+            contains('No packages matched the query, consider simplifying it'),
           );
         });
       },
diff --git a/pkgs/dart_mcp_server/test/tools/pub_test.dart b/pkgs/dart_mcp_server/test/tools/pub_test.dart
index 8f19130..693b99e 100644
--- a/pkgs/dart_mcp_server/test/tools/pub_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/pub_test.dart
@@ -68,7 +68,7 @@
           expect(result.isError, isNot(true));
           expect(testProcessManager.commandsRan, [
             equalsCommand((
-              command: [appKind, 'pub', 'add', 'foo'],
+              command: [endsWith(appKind), 'pub', 'add', 'foo'],
               workingDirectory: fakeAppPath,
             )),
           ]);
@@ -91,7 +91,7 @@
           expect(result.isError, isNot(true));
           expect(testProcessManager.commandsRan, [
             equalsCommand((
-              command: [appKind, 'pub', 'remove', 'foo'],
+              command: [endsWith(appKind), 'pub', 'remove', 'foo'],
               workingDirectory: fakeAppPath,
             )),
           ]);
@@ -113,7 +113,7 @@
           expect(result.isError, isNot(true));
           expect(testProcessManager.commandsRan, [
             equalsCommand((
-              command: [appKind, 'pub', 'get'],
+              command: [endsWith(appKind), 'pub', 'get'],
               workingDirectory: fakeAppPath,
             )),
           ]);
@@ -135,7 +135,7 @@
           expect(result.isError, isNot(true));
           expect(testProcessManager.commandsRan, [
             equalsCommand((
-              command: [appKind, 'pub', 'upgrade'],
+              command: [endsWith(appKind), 'pub', 'upgrade'],
               workingDirectory: fakeAppPath,
             )),
           ]);
diff --git a/pkgs/dart_mcp_server/test/utils/cli_utils_test.dart b/pkgs/dart_mcp_server/test/utils/cli_utils_test.dart
index 36b97e1..2392d30 100644
--- a/pkgs/dart_mcp_server/test/utils/cli_utils_test.dart
+++ b/pkgs/dart_mcp_server/test/utils/cli_utils_test.dart
@@ -5,6 +5,7 @@
 import 'package:dart_mcp/server.dart';
 import 'package:dart_mcp_server/src/utils/cli_utils.dart';
 import 'package:dart_mcp_server/src/utils/constants.dart';
+import 'package:dart_mcp_server/src/utils/sdk.dart';
 import 'package:file/memory.dart';
 import 'package:process/process.dart';
 import 'package:test/fake.dart';
@@ -35,12 +36,13 @@
             ],
           },
         ),
-        commandForRoot: (_, _) => 'testCommand',
+        commandForRoot: (_, _, _) => 'testCommand',
         arguments: ['a', 'b'],
         commandDescription: '',
         processManager: processManager,
         knownRoots: [Root(uri: 'file:///bar/')],
         fileSystem: fileSystem,
+        sdk: Sdk(),
       );
       expect(result.isError, isNot(true));
       expect(processManager.commandsRan, [
@@ -63,11 +65,12 @@
               ],
             },
           ),
-          commandForRoot: (_, _) => 'testCommand',
+          commandForRoot: (_, _, _) => 'testCommand',
           commandDescription: '',
           processManager: processManager,
           knownRoots: [Root(uri: 'file:///bar/')],
           fileSystem: fileSystem,
+          sdk: Sdk(),
         );
         expect(result.isError, isNot(true));
         expect(processManager.commandsRan, [
@@ -94,11 +97,12 @@
               ],
             },
           ),
-          commandForRoot: (_, _) => 'fake',
+          commandForRoot: (_, _, _) => 'fake',
           commandDescription: '',
           processManager: processManager,
           knownRoots: [Root(uri: 'file:///foo/')],
           fileSystem: fileSystem,
+          sdk: Sdk(),
         );
         expect(result.isError, isTrue);
         expect(
@@ -130,11 +134,12 @@
             ],
           },
         ),
-        commandForRoot: (_, _) => 'fake',
+        commandForRoot: (_, _, _) => 'fake',
         commandDescription: '',
         processManager: processManager,
         knownRoots: [Root(uri: 'file:///foo/')],
         fileSystem: fileSystem,
+        sdk: Sdk(),
       );
       expect(result.isError, isTrue);
       expect(