Skip to content
Open
Show file tree
Hide file tree
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
8 changes: 8 additions & 0 deletions packages/angular/cli/src/commands/mcp/tools/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ const listProjectsOutputSchema = {
'The default style language for the project (e.g., "scss"). ' +
'This determines the file extension for new component styles.',
),
targets: z
.array(z.string())
.describe(
'Available architect targets (e.g., ["build", "test", "lint", "e2e"]).',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid placing a comma immediately after abbreviations like 'e.g.' in user-facing messages.

Suggested change
'Available architect targets (e.g., ["build", "test", "lint", "e2e"]).',
'Available architect targets (e.g. ["build", "test", "lint", "e2e"]).',
References
  1. Avoid placing a comma immediately after abbreviations like 'e.g.' in user-facing messages.

),
}),
),
}),
Expand Down Expand Up @@ -131,6 +136,7 @@ their types, and their locations.
* 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).
* Identifying available architect targets (e.g., \`lint\`, \`e2e\`, \`serve\`, \`deploy\`) before attempting execution.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid placing a comma immediately after abbreviations like 'e.g.' in user-facing messages.

Suggested change
* Identifying available architect targets (e.g., \`lint\`, \`e2e\`, \`serve\`, \`deploy\`) before attempting execution.
* Identifying available architect targets (e.g. lint, e2e, serve, deploy) before attempting execution.
References
  1. Avoid placing a comma immediately after abbreviations like 'e.g.' in user-facing messages.

</Use Cases>
<Operational Notes>
* **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST**
Expand Down Expand Up @@ -471,6 +477,7 @@ async function loadAndParseWorkspace(
const fullSourceRoot = join(workspaceRoot, sourceRoot);
const unitTestFramework = getUnitTestFramework(project.targets.get('test'));
const styleLanguage = await getProjectStyleLanguage(project, ws, fullSourceRoot);
const targets = Array.from(project.targets.keys());

projects.push({
name,
Expand All @@ -481,6 +488,7 @@ async function loadAndParseWorkspace(
selectorPrefix: project.extensions['prefix'] as string,
unitTestFramework,
styleLanguage,
targets,
});
}

Expand Down
92 changes: 92 additions & 0 deletions packages/angular/cli/src/commands/mcp/tools/projects_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { workspaces } from '@angular-devkit/core';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { AngularWorkspace } from '../../../utilities/config';
import { createMockContext } from '../testing/test-utils';
import { LIST_PROJECTS_TOOL } from './projects';

describe('List Projects Tool', () => {
let mockWorkspace: AngularWorkspace;
let mockContext: ReturnType<typeof createMockContext>['context'];
let tempDir: string;
let allowedRoot: string;
let workspaceDir: string;

beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'mcp-projects-tool-'));
allowedRoot = join(tempDir, 'allowed-root');
workspaceDir = join(allowedRoot, 'workspace');
mkdirSync(workspaceDir, { recursive: true });
writeFileSync(join(workspaceDir, 'angular.json'), '{}');
writeFileSync(
join(workspaceDir, 'package.json'),
JSON.stringify({ dependencies: { '@angular/core': '18.0.0' } }),
);

const projects = new workspaces.ProjectDefinitionCollection();
const targets = new workspaces.TargetDefinitionCollection();
targets.set('build', { builder: '@angular-devkit/build-angular:application' });
targets.set('test', { builder: '@angular/build:unit-test', options: { runner: 'vitest' } });
targets.set('lint', { builder: '@angular-eslint/builder:lint' });
targets.set('e2e', { builder: '@cypress/schematic:cypress' });

projects.set('my-app', {
root: 'projects/my-app',
extensions: { projectType: 'application', prefix: 'app' },
targets,
});

mockWorkspace = {
projects,
extensions: {},
basePath: workspaceDir,
filePath: join(workspaceDir, 'angular.json'),
} as unknown as AngularWorkspace;

spyOn(AngularWorkspace, 'load').and.resolveTo(mockWorkspace);

const { context } = createMockContext();
mockContext = context;
mockContext.server = {
server: {
getClientCapabilities: jasmine.createSpy('getClientCapabilities').and.returnValue({
roots: { listChanged: false },
}),
listRoots: jasmine.createSpy('listRoots').and.resolveTo({
roots: [{ uri: pathToFileURL(allowedRoot).href, name: 'allowed-root' }],
}),
},
} as unknown as NonNullable<Parameters<typeof LIST_PROJECTS_TOOL.factory>[0]['server']>;
});

afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});

it('should list workspaces and extract available architect targets', async () => {
const handler = await LIST_PROJECTS_TOOL.factory(mockContext);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await (handler as any)({});

expect(result.structuredContent).toBeDefined();
const workspaces = result.structuredContent.workspaces;
expect(workspaces.length).toBe(1);
expect(workspaces[0].frameworkVersion).toBe('18');

const projects = workspaces[0].projects;
expect(projects.length).toBe(1);
expect(projects[0].name).toBe('my-app');
expect(projects[0].targets).toEqual(['build', 'test', 'lint', 'e2e']);
expect(projects[0].unitTestFramework).toBe('vitest');
});
});
Loading