Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 68 additions & 17 deletions packages/angular/cli/src/commands/mcp/tools/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
Expand Down Expand Up @@ -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).
</Use Cases>
<Operational Notes>
* **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST**
Expand Down Expand Up @@ -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,
Expand All @@ -272,6 +278,56 @@ async function loadAndParseWorkspace(
}
}

// Types for the structured output of the helper function.
type VersioningError = z.infer<typeof listProjectsOutputSchema.versioningErrors>[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<string>,
versionCache: Map<string, string | undefined>,
): 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[] = [];
Expand All @@ -292,27 +348,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);
}
}
}

Expand Down