Skip to content

Commit fce43e8

Browse files
committed
Incorporate validateRootConfig
1 parent d32011e commit fce43e8

File tree

2 files changed

+115
-7
lines changed

2 files changed

+115
-7
lines changed

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,8 @@ base mixin DartAnalyzerSupport
256256
final allRoots = await roots;
257257

258258
if (rootConfigs != null && rootConfigs.isEmpty) {
259-
// Empty list of roots means do nothing.
260-
return CallToolResult(content: [TextContent(text: 'No errors')]);
259+
// Have to have at least one root set.
260+
return noRootsSetResponse;
261261
}
262262

263263
// Default to use the known roots if none were specified.
@@ -267,12 +267,20 @@ base mixin DartAnalyzerSupport
267267

268268
final requestedUris = <Uri>{};
269269
for (final rootConfig in rootConfigs) {
270-
final rootUriString = rootConfig[ParameterNames.root] as String;
271-
final rootUri = Uri.parse(rootUriString);
272-
final paths = (rootConfig[ParameterNames.paths] as List?)?.cast<String>();
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);
273281

274-
if (paths != null && paths.isNotEmpty) {
275-
for (final path in paths) {
282+
if (validated.paths != null && validated.paths!.isNotEmpty) {
283+
for (final path in validated.paths!) {
276284
requestedUris.add(rootUri.resolve(path));
277285
}
278286
} else {

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,106 @@ 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+
289+
final knownRoot = knownRoots.firstWhereOrNull(
290+
(root) => isUnderRoot(root, rootUriString, fileSystem),
291+
);
292+
if (knownRoot == null) {
293+
return (
294+
root: null,
295+
paths: null,
296+
errorResult: CallToolResult(
297+
content: [
298+
TextContent(
299+
text:
300+
'Invalid root $rootUriString, must be under one of the '
301+
'registered project roots:\n\n${knownRoots.join('\n')}',
302+
),
303+
],
304+
isError: true,
305+
)..failureReason ??= CallToolFailureReason.invalidRootPath,
306+
);
307+
}
308+
final root = Root(uri: rootUriString);
309+
310+
final rootUri = Uri.parse(rootUriString);
311+
if (rootUri.scheme != 'file') {
312+
return (
313+
root: null,
314+
paths: null,
315+
errorResult: CallToolResult(
316+
content: [
317+
TextContent(
318+
text:
319+
'Only file scheme uris are allowed for roots, but got '
320+
'$rootUri',
321+
),
322+
],
323+
isError: true,
324+
)..failureReason ??= CallToolFailureReason.invalidRootScheme,
325+
);
326+
}
327+
328+
final paths =
329+
(rootConfig?[ParameterNames.paths] as List?)?.cast<String>() ??
330+
defaultPaths;
331+
if (paths != null) {
332+
final invalidPaths = paths.where(
333+
(path) => !isUnderRoot(root, path, fileSystem),
334+
);
335+
if (invalidPaths.isNotEmpty) {
336+
return (
337+
root: null,
338+
paths: null,
339+
errorResult: CallToolResult(
340+
content: [
341+
TextContent(
342+
text:
343+
'Paths are not allowed to escape their project root:\n'
344+
'${invalidPaths.join('\n')}',
345+
),
346+
],
347+
isError: true,
348+
)..failureReason ??= CallToolFailureReason.invalidPath,
349+
);
350+
}
351+
}
352+
return (root: root, paths: paths, errorResult: null);
353+
}
354+
255355
/// Returns 'dart' or 'flutter' based on the pubspec contents.
256356
///
257357
/// Throws an [ArgumentError] if there is no pubspec.

0 commit comments

Comments
 (0)