From c2c17dced8f0fc896412fbea452036597029c7df Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:35:26 -0400 Subject: [PATCH 1/2] refactor(@angular/cli): modularize `list_projects` tool logic The main handler for the `list_projects` tool was becoming complex, with the main loop handling file parsing, version discovery, and multiple error paths. This change extracts the logic for processing a single `angular.json` file into a new `processConfigFile` helper function. This refactoring improves the codes structure by separating the orchestration logic in the main handler from the implementation details of processing a file. This leads to a more modular, readable, and maintainable codebase with no change to the tools external behavior or output. --- .../cli/src/commands/mcp/tools/projects.ts | 79 +++++++++++++++---- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index bba995a4fd2a..d27a015eb38e 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -272,6 +272,56 @@ async function loadAndParseWorkspace( } } +// Types for the structured output of the helper function. +type VersioningError = z.infer[number]; + +/** + * Processes a single `angular.json` file to extract workspace and framework version information. + * @param configFile The path to the `angular.json` file. + * @param searchRoot The directory at which to stop the upward search for `package.json`. + * @param seenPaths A Set of absolute paths that have already been processed to avoid duplicates. + * @param versionCache A Map to cache framework version lookups for performance. + * @returns A promise resolving to an object containing the processed data and any errors. + */ +async function processConfigFile( + configFile: string, + searchRoot: string, + seenPaths: Set, + versionCache: Map, +): Promise<{ + workspace?: WorkspaceData; + parsingError?: ParsingError; + versioningError?: VersioningError; +}> { + const { workspace, error } = await loadAndParseWorkspace(configFile, seenPaths); + if (error) { + return { parsingError: error }; + } + + if (!workspace) { + return {}; // Skipped as it was already seen. + } + + try { + const workspaceDir = path.dirname(configFile); + workspace.frameworkVersion = await findAngularCoreVersion( + workspaceDir, + versionCache, + searchRoot, + ); + + return { workspace }; + } catch (e) { + return { + workspace, + versioningError: { + filePath: workspace.path, + message: e instanceof Error ? e.message : 'An unknown error occurred.', + }, + }; + } +} + async function createListProjectsHandler({ server }: McpToolContext) { return async () => { const workspaces: WorkspaceData[] = []; @@ -292,27 +342,22 @@ async function createListProjectsHandler({ server }: McpToolContext) { for (const root of searchRoots) { for await (const configFile of findAngularJsonFiles(root)) { - const { workspace, error } = await loadAndParseWorkspace(configFile, seenPaths); - if (error) { - parsingErrors.push(error); - } + const { workspace, parsingError, versioningError } = await processConfigFile( + configFile, + root, + seenPaths, + versionCache, + ); if (workspace) { - try { - const workspaceDir = path.dirname(configFile); - workspace.frameworkVersion = await findAngularCoreVersion( - workspaceDir, - versionCache, - root, - ); - } catch (e) { - versioningErrors.push({ - filePath: workspace.path, - message: e instanceof Error ? e.message : 'An unknown error occurred.', - }); - } workspaces.push(workspace); } + if (parsingError) { + parsingErrors.push(parsingError); + } + if (versioningError) { + versioningErrors.push(versioningError); + } } } From 3a25abcfcceb07772aaf6c1b159c0df6193910f1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 6 Oct 2025 10:39:06 -0400 Subject: [PATCH 2/2] feat(@angular/cli): add builder info to `list_projects` MCP tool The `list_projects` tool is enhanced to include the primary builder for each project, providing more specific metadata about its function. The builder is extracted from the project's 'build' target and added as an optional 'builder' field to the output. This allows users and other tools to quickly determine a project's type (e.g., application, library) by inspecting its builder string. The tool's description has also been updated to reflect this new capability. --- packages/angular/cli/src/commands/mcp/tools/projects.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index d27a015eb38e..c6fb60417fa5 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -34,6 +34,10 @@ const listProjectsOutputSchema = { .enum(['application', 'library']) .optional() .describe(`The type of the project, either 'application' or 'library'.`), + builder: z + .string() + .optional() + .describe('The primary builder for the project, typically from the "build" target.'), root: z .string() .describe('The root directory of the project, relative to the workspace root.'), @@ -90,6 +94,7 @@ their types, and their locations. * Determining if a project is an \`application\` or a \`library\`. * Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions. * Identifying the major version of the Angular framework for each workspace, which is crucial for monorepos. +* Determining a project's primary function by inspecting its builder (e.g., '@angular-devkit/build-angular:browser' for an application). * **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST** @@ -253,6 +258,7 @@ async function loadAndParseWorkspace( projects.push({ name, type: project.extensions['projectType'] as 'application' | 'library' | undefined, + builder: project.targets.get('build')?.builder, root: project.root, sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), selectorPrefix: project.extensions['prefix'] as string,