Skip to content

Commit 6b4b2bc

Browse files
authored
Add ability to filter analyzer results on a set of paths (#260)
This adds the ability to filter the results of analyze_files with a set of paths.
1 parent 72a9283 commit 6b4b2bc

File tree

10 files changed

+593
-18
lines changed

10 files changed

+593
-18
lines changed

.github/workflows/dart_mcp_server.yaml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ on:
55
# Run CI on all PRs (against any branch) and on pushes to the main branch.
66
pull_request:
77
paths:
8-
- '.github/workflows/dart_mcp_server.yaml'
9-
- 'pkgs/dart_mcp_server/**'
10-
- 'pkgs/dart_mcp/**'
8+
- ".github/workflows/dart_mcp_server.yaml"
9+
- "pkgs/dart_mcp_server/**"
10+
- "pkgs/dart_mcp/**"
1111
push:
12-
branches: [ main ]
12+
branches: [main]
1313
paths:
14-
- '.github/workflows/dart_mcp_server.yaml'
15-
- 'pkgs/dart_mcp_server/**'
16-
- 'pkgs/dart_mcp/**'
14+
- ".github/workflows/dart_mcp_server.yaml"
15+
- "pkgs/dart_mcp_server/**"
16+
- "pkgs/dart_mcp/**"
1717
schedule:
18-
- cron: '0 0 * * 0' # weekly
18+
- cron: "0 0 * * 0" # weekly
1919

2020
defaults:
2121
run:
@@ -64,6 +64,6 @@ jobs:
6464

6565
# If this fails, you need to run 'dart tool/update_readme.dart'.
6666
- run: dart tool/update_readme.dart
67-
- run: git diff --exit-code README.md
67+
- run: git diff --exit-code README.md || (echo 'README.md needs to be updated. Run "dart tool/update_readme.dart"' && false)
6868

6969
- run: dart test

pkgs/dart_mcp_server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
tends to hide nested text widgets which makes it difficult to find widgets
1313
based on their text values.
1414
* Add an `--exclude-tool` command line flag to exclude tools by name.
15+
* Add the abillity to limit the output of `analyze_files` to a set of paths.
1516

1617
# 0.1.0 (Dart SDK 3.9.0)
1718

pkgs/dart_mcp_server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ For more information, see the official VS Code documentation for
151151
| `run_tests` | Run tests | Run Dart or Flutter tests with an agent centric UX. ALWAYS use instead of `dart test` or `flutter test` shell commands. |
152152
| `create_project` | Create project | Creates a new Dart or Flutter project. |
153153
| `pub` | pub | Runs a pub command for the given project roots, like `dart pub get` or `flutter pub add`. |
154-
| `analyze_files` | Analyze projects | Analyzes the entire project for errors. |
154+
| `analyze_files` | Analyze projects | Analyzes specific paths, or the entire project, for errors. |
155155
| `resolve_workspace_symbol` | Project search | Look up a symbol or symbols in all workspaces by name. Can be used to validate that a symbol exists or discover small spelling mistakes, since the search is fuzzy. |
156156
| `signature_help` | Signature help | Get signature help for an API being used at a given cursor position in a file. |
157157
| `hover` | Hover information | Get hover information at a given cursor position in a file. This can include documentation, type information, etc for the text at that position. |

pkgs/dart_mcp_server/lib/src/mixins/analyzer.dart

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@ import 'package:meta/meta.dart';
1414

1515
import '../lsp/wire_format.dart';
1616
import '../utils/analytics.dart';
17+
import '../utils/cli_utils.dart';
1718
import '../utils/constants.dart';
19+
import '../utils/file_system.dart';
1820
import '../utils/sdk.dart';
1921

2022
/// Mix this in to any MCPServer to add support for analyzing Dart projects.
2123
///
2224
/// The MCPServer must already have the [ToolsSupport] and [LoggingSupport]
2325
/// mixins applied.
2426
base mixin DartAnalyzerSupport
25-
on ToolsSupport, LoggingSupport, RootsTrackingSupport
27+
on ToolsSupport, LoggingSupport, RootsTrackingSupport, FileSystemSupport
2628
implements SdkSupport {
2729
/// The LSP server connection for the analysis server.
2830
Peer? _lspConnection;
@@ -249,8 +251,54 @@ base mixin DartAnalyzerSupport
249251
final errorResult = await _ensurePrerequisites(request);
250252
if (errorResult != null) return errorResult;
251253

254+
var rootConfigs = (request.arguments?[ParameterNames.roots] as List?)
255+
?.cast<Map<String, Object?>>();
256+
final allRoots = await roots;
257+
258+
if (rootConfigs != null && rootConfigs.isEmpty) {
259+
// Have to have at least one root set.
260+
return noRootsSetResponse;
261+
}
262+
263+
// Default to use the known roots if none were specified.
264+
rootConfigs ??= [
265+
for (final root in allRoots) {ParameterNames.root: root.uri},
266+
];
267+
268+
final requestedUris = <Uri>{};
269+
for (final rootConfig in rootConfigs) {
270+
final validated = validateRootConfig(
271+
rootConfig,
272+
knownRoots: allRoots,
273+
fileSystem: fileSystem,
274+
);
275+
276+
if (validated.errorResult != null) {
277+
return errorResult!;
278+
}
279+
280+
final rootUri = Uri.parse(validated.root!.uri);
281+
282+
if (validated.paths != null && validated.paths!.isNotEmpty) {
283+
for (final path in validated.paths!) {
284+
requestedUris.add(rootUri.resolve(path));
285+
}
286+
} else {
287+
requestedUris.add(rootUri);
288+
}
289+
}
290+
291+
final entries = diagnostics.entries.where((entry) {
292+
final entryPath = entry.key.toFilePath();
293+
return requestedUris.any((uri) {
294+
final requestedPath = uri.toFilePath();
295+
return fileSystem.path.equals(requestedPath, entryPath) ||
296+
fileSystem.path.isWithin(requestedPath, entryPath);
297+
});
298+
});
299+
252300
final messages = <Content>[];
253-
for (var entry in diagnostics.entries) {
301+
for (var entry in entries) {
254302
for (var diagnostic in entry.value) {
255303
final diagnosticJson = diagnostic.toJson();
256304
diagnosticJson[ParameterNames.uri] = entry.key.toString();
@@ -411,8 +459,10 @@ base mixin DartAnalyzerSupport
411459
@visibleForTesting
412460
static final analyzeFilesTool = Tool(
413461
name: 'analyze_files',
414-
description: 'Analyzes the entire project for errors.',
415-
inputSchema: Schema.object(),
462+
description: 'Analyzes specific paths, or the entire project, for errors.',
463+
inputSchema: Schema.object(
464+
properties: {ParameterNames.roots: rootsSchema(supportsPaths: true)},
465+
),
416466
annotations: ToolAnnotations(title: 'Analyze projects', readOnlyHint: true),
417467
);
418468

pkgs/dart_mcp_server/lib/src/server.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ final class DartMCPServer extends MCPServer
3636
ResourcesSupport,
3737
RootsTrackingSupport,
3838
RootsFallbackSupport,
39-
DartAnalyzerSupport,
4039
DashCliSupport,
40+
DartAnalyzerSupport,
4141
PubSupport,
4242
PubDevSupport,
4343
DartToolingDaemonSupport,

pkgs/dart_mcp_server/lib/src/utils/cli_utils.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,105 @@ Future<CallToolResult> runCommandInRoot(
252252
);
253253
}
254254

255+
/// Validates a root argument given via [rootConfig], ensuring that it falls
256+
/// under one of the [knownRoots], and that all `paths` arguments are also under
257+
/// the given root.
258+
///
259+
/// Returns a root on success, equal to the given root (but this could be a
260+
/// subdirectory of one of the [knownRoots]), as well as any paths that were
261+
/// validated.
262+
///
263+
/// If no [ParameterNames.paths] are provided, then the [defaultPaths] will be
264+
/// used, if present. Otherwise no paths are validated or will be returned.
265+
///
266+
/// On failure, returns a [CallToolResult].
267+
({Root? root, List<String>? paths, CallToolResult? errorResult})
268+
validateRootConfig(
269+
Map<String, Object?>? rootConfig, {
270+
List<String>? defaultPaths,
271+
required FileSystem fileSystem,
272+
required List<Root> knownRoots,
273+
}) {
274+
final rootUriString = rootConfig?[ParameterNames.root] as String?;
275+
if (rootUriString == null) {
276+
// This shouldn't happen based on the schema, but handle defensively.
277+
return (
278+
root: null,
279+
paths: null,
280+
errorResult: CallToolResult(
281+
content: [
282+
TextContent(text: 'Invalid root configuration: missing `root` key.'),
283+
],
284+
isError: true,
285+
)..failureReason ??= CallToolFailureReason.noRootGiven,
286+
);
287+
}
288+
final rootUri = Uri.parse(rootUriString);
289+
if (rootUri.scheme != 'file') {
290+
return (
291+
root: null,
292+
paths: null,
293+
errorResult: CallToolResult(
294+
content: [
295+
TextContent(
296+
text:
297+
'Only file scheme uris are allowed for roots, but got '
298+
'$rootUri',
299+
),
300+
],
301+
isError: true,
302+
)..failureReason ??= CallToolFailureReason.invalidRootScheme,
303+
);
304+
}
305+
306+
final knownRoot = knownRoots.firstWhereOrNull(
307+
(root) => _isUnderRoot(root, rootUriString, fileSystem),
308+
);
309+
if (knownRoot == null) {
310+
return (
311+
root: null,
312+
paths: null,
313+
errorResult: CallToolResult(
314+
content: [
315+
TextContent(
316+
text:
317+
'Invalid root $rootUriString, must be under one of the '
318+
'registered project roots:\n\n${knownRoots.join('\n')}',
319+
),
320+
],
321+
isError: true,
322+
)..failureReason ??= CallToolFailureReason.invalidRootPath,
323+
);
324+
}
325+
final root = Root(uri: rootUriString);
326+
327+
final paths =
328+
(rootConfig?[ParameterNames.paths] as List?)?.cast<String>() ??
329+
defaultPaths;
330+
if (paths != null) {
331+
final invalidPaths = paths.where(
332+
(path) => !_isUnderRoot(root, path, fileSystem),
333+
);
334+
if (invalidPaths.isNotEmpty) {
335+
return (
336+
root: null,
337+
paths: null,
338+
errorResult: CallToolResult(
339+
content: [
340+
TextContent(
341+
text:
342+
'Paths are not allowed to escape their project root:\n'
343+
'${invalidPaths.join('\n')}',
344+
),
345+
],
346+
isError: true,
347+
)..failureReason ??= CallToolFailureReason.invalidPath,
348+
);
349+
}
350+
}
351+
return (root: root, paths: paths, errorResult: null);
352+
}
353+
255354
/// Returns 'dart' or 'flutter' based on the pubspec contents.
256355
///
257356
/// Throws an [ArgumentError] if there is no pubspec.

pkgs/dart_mcp_server/test/test_harness.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ class TestHarness {
173173
final result = await mcpServerConnection.callTool(request);
174174
expect(
175175
result.isError,
176-
expectError ? true : isNot(true),
176+
expectError ? true : isNot(isTrue),
177177
reason: result.content.join('\n'),
178178
);
179179
return result;
@@ -185,11 +185,12 @@ class TestHarness {
185185
Future<CallToolResult> callToolWithRetry(
186186
CallToolRequest request, {
187187
int maxTries = 5,
188+
bool expectError = false,
188189
}) async {
189190
var tryCount = 0;
190191
while (true) {
191192
try {
192-
return await callTool(request);
193+
return await callTool(request, expectError: expectError);
193194
} catch (_) {
194195
if (tryCount++ >= maxTries) rethrow;
195196
}

0 commit comments

Comments
 (0)