From eed64acded4bbf9e076dcb1573ddba55edc9b9d0 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Sat, 7 Jun 2025 00:35:46 +0000 Subject: [PATCH 1/2] Retain compatibility with 3.7 SDKs The code will only ship with newer SDKs but keep compatibility for easier testing from source with older SDKs. Remove use of null aware elements. --- .../lib/src/mixins/pub_dev_search.dart | 110 ++++++++++-------- pkgs/dart_mcp_server/pubspec.yaml | 2 +- 2 files changed, 64 insertions(+), 48 deletions(-) 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 8bb836f..5b16eb4 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 @@ -43,9 +43,11 @@ base mixin PubDevSupport on ToolsSupport { try { result = jsonDecode(await _client.read(searchUrl)); - final packageNames = dig(result, [ - 'packages', - ]).take(_resultsLimit).map((p) => dig(p, ['package'])).toList(); + final packageNames = + dig(result, ['packages']) + .take(_resultsLimit) + .map((p) => dig(p, ['package'])) + .toList(); if (packageNames.isEmpty) { return CallToolResult( @@ -69,17 +71,18 @@ base mixin PubDevSupport on ToolsSupport { } // Retrieve information about all the packages in parallel. - final subQueryFutures = packageNames - .map( - (packageName) => ( - versionListing: retrieve('api/packages/$packageName'), - score: retrieve('api/packages/$packageName/score'), - docIndex: retrieve( - 'documentation/$packageName/latest/index.json', - ), - ), - ) - .toList(); + final subQueryFutures = + packageNames + .map( + (packageName) => ( + versionListing: retrieve('api/packages/$packageName'), + score: retrieve('api/packages/$packageName/score'), + docIndex: retrieve( + 'documentation/$packageName/latest/index.json', + ), + ), + ) + .toList(); // Aggregate the retrieved information about each package into a // TextContent. @@ -94,10 +97,11 @@ base mixin PubDevSupport on ToolsSupport { ?.cast>() ?? >[]) if (!object.containsKey('enclosedBy')) - object['name'] as String: Uri.https( - 'pub.dev', - 'documentation/$packageName/latest/${object['href']}', - ).toString(), + object['name'] as String: + Uri.https( + 'pub.dev', + 'documentation/$packageName/latest/${object['href']}', + ).toString(), }; results.add( TextContent( @@ -108,26 +112,34 @@ base mixin PubDevSupport on ToolsSupport { 'latest', 'version', ]), - 'description': ?dig(versionListing, [ - 'latest', - 'pubspec', - 'description', - ]), - 'homepage': ?dig(versionListing, [ - 'latest', - 'pubspec', - 'homepage', - ]), - 'repository': ?dig(versionListing, [ - 'latest', - 'pubspec', - 'repository', - ]), - 'documentation': ?dig(versionListing, [ - 'latest', - 'pubspec', - 'documentation', - ]), + if (dig(versionListing, [ + 'latest', + 'pubspec', + 'description', + ]) + case final description?) + 'description': description, + if (dig(versionListing, [ + 'latest', + 'pubspec', + 'homepage', + ]) + case final homepage?) + 'homepage': homepage, + if (dig(versionListing, [ + 'latest', + 'pubspec', + 'repository', + ]) + case final repository?) + 'repository': repository, + if (dig(versionListing, [ + 'latest', + 'pubspec', + 'documentation', + ]) + case final documentation?) + 'documentation': documentation, }, if (libraryDocs.isNotEmpty) ...{'libraries': libraryDocs}, if (scoreResult != null) ...{ @@ -139,15 +151,19 @@ base mixin PubDevSupport on ToolsSupport { 'downloadCount30Days', ]), }, - 'topics': dig(scoreResult, [ - 'tags', - ]).where((t) => (t as String).startsWith('topic:')).toList(), - 'licenses': dig(scoreResult, [ - 'tags', - ]).where((t) => (t as String).startsWith('license')).toList(), - 'publisher': dig(scoreResult, ['tags']) - .where((t) => (t as String).startsWith('publisher:')) - .firstOrNull, + 'topics': + dig( + scoreResult, + ['tags'], + ).where((t) => (t as String).startsWith('topic:')).toList(), + 'licenses': + dig(scoreResult, ['tags']) + .where((t) => (t as String).startsWith('license')) + .toList(), + 'publisher': + dig(scoreResult, ['tags']) + .where((t) => (t as String).startsWith('publisher:')) + .firstOrNull, }, }), ), diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml index cea8e92..b560d11 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -6,7 +6,7 @@ description: >- publish_to: none environment: - sdk: ^3.8.0 + sdk: ^3.7.0 executables: dart_mcp_server: main From e080cc32a56cc1ab311d475818c51eee1ad94619 Mon Sep 17 00:00:00 2001 From: Nate Bosch Date: Sat, 7 Jun 2025 01:14:28 +0000 Subject: [PATCH 2/2] Format at 2.7 language version --- pkgs/dart_mcp_server/bin/main.dart | 51 ++++++++++--------- .../lib/src/mixins/analyzer.dart | 44 ++++++++-------- .../lib/src/mixins/dash_cli.dart | 22 ++++---- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 6 ++- .../src/mixins/roots_fallback_support.dart | 8 +-- .../lib/src/utils/cli_utils.dart | 18 ++++--- pkgs/dart_mcp_server/test/test_harness.dart | 11 ++-- pkgs/dart_mcp_server/test/tools/dtd_test.dart | 42 ++++++++------- .../test/tools/pub_dev_search_test.dart | 6 ++- pkgs/dart_mcp_server/test/tools/pub_test.dart | 11 ++-- 10 files changed, 115 insertions(+), 104 deletions(-) diff --git a/pkgs/dart_mcp_server/bin/main.dart b/pkgs/dart_mcp_server/bin/main.dart index 4fec4f0..351dc72 100644 --- a/pkgs/dart_mcp_server/bin/main.dart +++ b/pkgs/dart_mcp_server/bin/main.dart @@ -69,31 +69,32 @@ void main(List args) async { ); } -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.', - ) - ..addFlag(help, abbr: 'h', help: 'Show usage text'); +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.', + ) + ..addFlag(help, abbr: 'h', help: 'Show usage text'); const dartSdkOption = 'dart-sdk'; const flutterSdkOption = 'flutter-sdk'; diff --git a/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart b/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart index e39ae44..10ae558 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart @@ -109,18 +109,19 @@ base mixin DartAnalyzerSupport log(LoggingLevel.warning, line, logger: 'DartLanguageServer'); }); - _lspConnection = Peer(lspChannel(_lspServer.stdout, _lspServer.stdin)) - ..registerMethod( - lsp.Method.textDocument_publishDiagnostics.toString(), - _handleDiagnostics, - ) - ..registerMethod(r'$/analyzerStatus', _handleAnalyzerStatus) - ..registerFallback((Parameters params) { - log( - LoggingLevel.debug, - () => 'Unhandled LSP message: ${params.method} - ${params.asMap}', - ); - }); + _lspConnection = + Peer(lspChannel(_lspServer.stdout, _lspServer.stdin)) + ..registerMethod( + lsp.Method.textDocument_publishDiagnostics.toString(), + _handleDiagnostics, + ) + ..registerMethod(r'$/analyzerStatus', _handleAnalyzerStatus) + ..registerFallback((Parameters params) { + log( + LoggingLevel.debug, + () => 'Unhandled LSP message: ${params.method} - ${params.asMap}', + ); + }); unawaited(_lspConnection.listen()); @@ -354,9 +355,8 @@ base mixin DartAnalyzerSupport diagnostics[diagnosticParams.uri] = diagnosticParams.diagnostics; log(LoggingLevel.debug, { ParameterNames.uri: diagnosticParams.uri, - 'diagnostics': diagnosticParams.diagnostics - .map((d) => d.toJson()) - .toList(), + 'diagnostics': + diagnosticParams.diagnostics.map((d) => d.toJson()).toList(), }); } @@ -368,18 +368,16 @@ base mixin DartAnalyzerSupport final newRoots = await roots; final oldWorkspaceFolders = _currentWorkspaceFolders; - final newWorkspaceFolders = _currentWorkspaceFolders = - HashSet( + final newWorkspaceFolders = + _currentWorkspaceFolders = HashSet( equals: (a, b) => a.uri == b.uri, hashCode: (a) => a.uri.hashCode, )..addAll(newRoots.map((r) => r.asWorkspaceFolder)); - final added = newWorkspaceFolders - .difference(oldWorkspaceFolders) - .toList(); - final removed = oldWorkspaceFolders - .difference(newWorkspaceFolders) - .toList(); + final added = + newWorkspaceFolders.difference(oldWorkspaceFolders).toList(); + final removed = + oldWorkspaceFolders.difference(newWorkspaceFolders).toList(); // This can happen in the case of multiple notifications in quick // succession, the `roots` future will complete only after the state has 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 a4337d9..a6f8bfc 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart @@ -109,9 +109,10 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport // 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'; + final plural = + invalidPlatforms.length > 1 + ? 'are not valid platforms' + : 'is not a valid platform'; errors.add( ValidationError( ValidationErrorType.itemInvalid, @@ -152,13 +153,14 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport return runCommandInRoot( request, arguments: commandArgs, - commandForRoot: (_, _, sdk) => - switch (projectType) { - 'dart' => sdk.dartExecutablePath, - 'flutter' => sdk.flutterExecutablePath, - _ => StateError('Unknown project type: $projectType'), - } - as String, + commandForRoot: + (_, _, sdk) => + switch (projectType) { + 'dart' => sdk.dartExecutablePath, + 'flutter' => sdk.flutterExecutablePath, + _ => StateError('Unknown project type: $projectType'), + } + as String, commandDescription: '$projectType create', fileSystem: fileSystem, processManager: processManager, diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index d94f25d..1dfffe4 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -96,8 +96,10 @@ base mixin DartToolingDaemonSupport continue; } if (debugSession.vmServiceUri case final vmServiceUri?) { - final vmServiceFuture = activeVmServices[debugSession.id] = - vmServiceConnectUri(vmServiceUri); + final vmServiceFuture = + activeVmServices[debugSession.id] = vmServiceConnectUri( + vmServiceUri, + ); final vmService = await vmServiceFuture; // Start listening for and collecting errors immediately. final errorService = await _AppErrorsListener.forVmService( diff --git a/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart b/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart index 1c2562a..a47f1b9 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/roots_fallback_support.dart @@ -50,8 +50,8 @@ base mixin RootsFallbackSupport on ToolsSupport, RootsTrackingSupport { // If the client supports roots, just use their stream (or lack thereof). // If they don't, use our own stream. _fallbackEnabled - ? _rootsListChangedFallbackController?.stream - : super.rootsListChanged; + ? _rootsListChangedFallbackController?.stream + : super.rootsListChanged; StreamController? _rootsListChangedFallbackController; @@ -76,8 +76,8 @@ base mixin RootsFallbackSupport on ToolsSupport, RootsTrackingSupport { @override Future listRoots(ListRootsRequest request) async => _fallbackEnabled - ? ListRootsResult(roots: _customRoots.toList()) - : super.listRoots(request); + ? ListRootsResult(roots: _customRoots.toList()) + : super.listRoots(request); /// Adds the roots in [request] the custom roots and calls [updateRoots]. /// 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 593627d..392fc4e 100644 --- a/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart +++ b/pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart @@ -85,8 +85,9 @@ Future runCommandInRoots( List defaultPaths = const [], required Sdk sdk, }) async { - var rootConfigs = (request.arguments?[ParameterNames.roots] as List?) - ?.cast>(); + var rootConfigs = + (request.arguments?[ParameterNames.roots] as List?) + ?.cast>(); // Default to use the known roots if none were specified. if (rootConfigs == null || rootConfigs.isEmpty) { @@ -256,12 +257,13 @@ Future defaultCommandForRoot( ) async => switch (await inferProjectKind(rootUri, fileSystem)) { ProjectKind.dart => sdk.dartExecutablePath, ProjectKind.flutter => sdk.flutterExecutablePath, - ProjectKind.unknown => throw ArgumentError.value( - rootUri, - 'rootUri', - 'Unknown project kind at root $rootUri. All projects must have a ' - 'pubspec.', - ), + ProjectKind.unknown => + throw ArgumentError.value( + rootUri, + 'rootUri', + 'Unknown project kind at root $rootUri. All projects must have a ' + 'pubspec.', + ), }; /// Returns whether [uri] is under or exactly equal to [root]. diff --git a/pkgs/dart_mcp_server/test/test_harness.dart b/pkgs/dart_mcp_server/test/test_harness.dart index e5901c8..6e9f577 100644 --- a/pkgs/dart_mcp_server/test/test_harness.dart +++ b/pkgs/dart_mcp_server/test/test_harness.dart @@ -226,9 +226,8 @@ final class AppDebugSession { final stdout = StreamQueue(process.stdoutStream()); while (vmServiceUri == null && await stdout.hasNext) { final line = await stdout.next; - final serviceString = isFlutter - ? 'A Dart VM Service' - : 'The Dart VM service'; + final serviceString = + isFlutter ? 'A Dart VM Service' : 'The Dart VM service'; if (line.contains(serviceString)) { vmServiceUri = line .substring(line.indexOf('http:')) @@ -377,10 +376,8 @@ Future _getDTDUri(TestProcess dtdProcess) async { return dtdUri; } -typedef ServerConnectionPair = ({ - ServerConnection serverConnection, - DartMCPServer? server, -}); +typedef ServerConnectionPair = + ({ServerConnection serverConnection, DartMCPServer? server}); /// Starts up the [DartMCPServer] and connects [client] to it. /// diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 0961ee9..ba07aa8 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -405,14 +405,16 @@ void main() { final stdin = debugSession.appProcess.stdin; stdin.writeln(''); - var resources = (await serverConnection.listResources( - ListResourcesRequest(), - )).resources; + var resources = + (await serverConnection.listResources( + ListResourcesRequest(), + )).resources; if (resources.runtimeErrors.isEmpty) { await onResourceListChanged; - resources = (await serverConnection.listResources( - ListResourcesRequest(), - )).resources; + resources = + (await serverConnection.listResources( + ListResourcesRequest(), + )).resources; } final resource = resources.runtimeErrors.single; @@ -422,9 +424,10 @@ void main() { await serverConnection.subscribeResource( SubscribeRequest(uri: resource.uri), ); - var originalContents = (await serverConnection.readResource( - ReadResourceRequest(uri: resource.uri), - )).contents; + var originalContents = + (await serverConnection.readResource( + ReadResourceRequest(uri: resource.uri), + )).contents; final errorMatcher = isA().having( (c) => c.text, 'text', @@ -434,9 +437,10 @@ void main() { // re-read the resource. if (originalContents.isEmpty) { await resourceUpdatedQueue.next; - originalContents = (await serverConnection.readResource( - ReadResourceRequest(uri: resource.uri), - )).contents; + originalContents = + (await serverConnection.readResource( + ReadResourceRequest(uri: resource.uri), + )).contents; } expect( originalContents.length, @@ -456,9 +460,10 @@ void main() { ); // Should now have another error. - final newContents = (await serverConnection.readResource( - ReadResourceRequest(uri: resource.uri), - )).contents; + final newContents = + (await serverConnection.readResource( + ReadResourceRequest(uri: resource.uri), + )).contents; expect(newContents.length, 2); expect(newContents.last, errorMatcher); @@ -470,9 +475,10 @@ void main() { ), ); - final finalContents = (await serverConnection.readResource( - ReadResourceRequest(uri: resource.uri), - )).contents; + final finalContents = + (await serverConnection.readResource( + ReadResourceRequest(uri: resource.uri), + )).contents; expect(finalContents, isEmpty); }, onPlatform: { 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 436ac6c..11feb5e 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 @@ -213,8 +213,10 @@ class _FixedResponseClient implements Client { _FixedResponseClient(this.handler); _FixedResponseClient.withMappedResponses(Map responses) - : handler = ((url) => - responses[url.toString()] ?? (throw ClientException('No internet'))); + : handler = + ((url) => + responses[url.toString()] ?? + (throw ClientException('No internet'))); @override Future read(Uri url, {Map? headers}) async { diff --git a/pkgs/dart_mcp_server/test/tools/pub_test.dart b/pkgs/dart_mcp_server/test/tools/pub_test.dart index eea837a..841a60d 100644 --- a/pkgs/dart_mcp_server/test/tools/pub_test.dart +++ b/pkgs/dart_mcp_server/test/tools/pub_test.dart @@ -28,17 +28,18 @@ void main() { final executableName = '$appKind${Platform.isWindows ? appKind == 'dart' - ? '.exe' - : '.bat' + ? '.exe' + : '.bat' : ''}'; group('$appKind app', () { // TODO: Use setUpAll, currently this fails due to an apparent TestProcess // issue. setUp(() async { fileSystem = MemoryFileSystem( - style: Platform.isWindows - ? FileSystemStyle.windows - : FileSystemStyle.posix, + style: + Platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, ); fileSystem.file(p.join(fakeAppPath, 'pubspec.yaml')) ..createSync(recursive: true)