@@ -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