diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index 8c6eb5d332f6..7a41581e8605 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -544,6 +544,31 @@ async function processConfigFile( } } +/** + * Deduplicates overlapping search roots (e.g., if one is a child of another). + * Sorting by length ensures parent directories are processed before children. + * @param roots A list of normalized absolute paths used as search roots. + * @returns A deduplicated list of search roots. + */ +function deduplicateSearchRoots(roots: string[]): string[] { + const sortedRoots = [...roots].sort((a, b) => a.length - b.length); + const deduplicated: string[] = []; + + for (const root of sortedRoots) { + const isSubdirectory = deduplicated.some((existing) => { + const rel = relative(existing, root); + + return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); + }); + + if (!isSubdirectory) { + deduplicated.push(root); + } + } + + return deduplicated; +} + async function createListProjectsHandler({ server }: McpToolContext) { return async () => { const workspaces: WorkspaceData[] = []; @@ -562,6 +587,8 @@ async function createListProjectsHandler({ server }: McpToolContext) { searchRoots = [process.cwd()]; } + searchRoots = deduplicateSearchRoots(searchRoots); + // Pre-resolve allowed roots to handle their own symlinks or normalizations. // We ignore failures here; if a root is broken, we simply won't match against it. const realAllowedRoots = searchRoots