Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 4e92eb6

Browse files
amishneclydin
authored andcommitted
feat(@angular/cli): add modernize tool to the MCP server
- Adds a new `modernize` tool to the MCP server. This tool provides developers with instructions for running various Angular migrations, such as control-flow, standalone, and signal inputs. - Includes a comprehensive list of available modernizations, with links to their documentation. - Adds unit tests for the new `modernize` tool to ensure its correctness. - Refactors the static best-practices guide into a dedicated `instructions.ts` resource for better code organization.
1 parent e9a33e4 commit 4e92eb6

File tree

5 files changed

+271
-22
lines changed

5 files changed

+271
-22
lines changed

‎packages/angular/cli/src/commands/mcp/mcp-server.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import { readFile } from 'node:fs/promises';
1111
import path from 'node:path';
1212
import type { AngularWorkspace } from '../../utilities/config';
1313
import { VERSION } from '../../utilities/version';
14+
import { registerInstructionsResource } from './resources/instructions';
1415
import { registerBestPracticesTool } from './tools/best-practices';
1516
import { registerDocSearchTool } from './tools/doc-search';
1617
import { registerFindExampleTool } from './tools/examples';
18+
import { registerModernizeTool } from './tools/modernize';
1719
import { registerListProjectsTool } from './tools/projects';
1820

1921
export async function createMcpServer(
@@ -39,29 +41,9 @@ export async function createMcpServer(
3941
},
4042
);
4143

42-
server.registerResource(
43-
'instructions',
44-
'instructions://best-practices',
45-
{
46-
title: 'Angular Best Practices and Code Generation Guide',
47-
description:
48-
"A comprehensive guide detailing Angular's best practices for code generation and development." +
49-
' This guide should be used as a reference by an LLM to ensure any generated code' +
50-
' adheres to modern Angular standards, including the use of standalone components,' +
51-
' typed forms, modern control flow syntax, and other current conventions.',
52-
mimeType: 'text/markdown',
53-
},
54-
async () => {
55-
const text = await readFile(
56-
path.join(__dirname, 'instructions', 'best-practices.md'),
57-
'utf-8',
58-
);
59-
60-
return { contents: [{ uri: 'instructions://best-practices', text }] };
61-
},
62-
);
63-
44+
registerInstructionsResource(server);
6445
registerBestPracticesTool(server);
46+
registerModernizeTool(server);
6547

6648
// If run outside an Angular workspace (e.g., globally) skip the workspace specific tools.
6749
if (context.workspace) {
File renamed without changes.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10+
import { readFile } from 'node:fs/promises';
11+
import path from 'node:path';
12+
13+
export function registerInstructionsResource(server: McpServer): void {
14+
server.registerResource(
15+
'instructions',
16+
'instructions://best-practices',
17+
{
18+
title: 'Angular Best Practices and Code Generation Guide',
19+
description:
20+
"A comprehensive guide detailing Angular's best practices for code generation and development." +
21+
' This guide should be used as a reference by an LLM to ensure any generated code' +
22+
' adheres to modern Angular standards, including the use of standalone components,' +
23+
' typed forms, modern control flow syntax, and other current conventions.',
24+
mimeType: 'text/markdown',
25+
},
26+
async () => {
27+
const text = await readFile(path.join(__dirname, 'best-practices.md'), 'utf-8');
28+
29+
return { contents: [{ uri: 'instructions://best-practices', text }] };
30+
},
31+
);
32+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10+
import { z } from 'zod';
11+
12+
interface Transformation {
13+
name: string;
14+
description: string;
15+
documentationUrl: string;
16+
instructions?: string;
17+
}
18+
19+
const TRANSFORMATIONS: Array<Transformation> = [
20+
{
21+
name: 'control-flow-migration',
22+
description:
23+
'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.',
24+
documentationUrl: 'https://angular.dev/reference/migrations/control-flow',
25+
},
26+
{
27+
name: 'self-closing-tags-migration',
28+
description:
29+
'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
30+
documentationUrl: 'https://angular.dev/reference/migrations/self-closing-tags',
31+
},
32+
{
33+
name: 'test-bed-get',
34+
description:
35+
'Updates `TestBed.get` to the preferred and type-safe `TestBed.inject` in TypeScript test files.',
36+
documentationUrl: 'https://angular.dev/guide/testing/dependency-injection',
37+
},
38+
{
39+
name: 'inject-flags',
40+
description:
41+
'Updates `inject` calls from using the InjectFlags enum to a more modern and readable options object.',
42+
documentationUrl: 'https://angular.dev/reference/migrations/inject-function',
43+
},
44+
{
45+
name: 'output-migration',
46+
description: 'Converts `@Output` declarations to the new functional `output()` syntax.',
47+
documentationUrl: 'https://angular.dev/reference/migrations/outputs',
48+
},
49+
{
50+
name: 'signal-input-migration',
51+
description: 'Migrates `@Input` declarations to the new signal-based `input()` syntax.',
52+
documentationUrl: 'https://angular.dev/reference/migrations/signal-inputs',
53+
},
54+
{
55+
name: 'signal-queries-migration',
56+
description:
57+
'Migrates `@ViewChild` and `@ContentChild` queries to their signal-based `viewChild` and `contentChild` versions.',
58+
documentationUrl: 'https://angular.dev/reference/migrations/signal-queries',
59+
},
60+
{
61+
name: 'standalone',
62+
description:
63+
'Converts the application to use standalone components, directives, and pipes. This is a ' +
64+
'three-step process. After each step, you should verify that your application builds and ' +
65+
'runs correctly.',
66+
instructions:
67+
'This migration requires running a cli schematic multiple times. Run the commands in the ' +
68+
'order listed below, verifying that your code builds and runs between each step:\n\n' +
69+
'1. Run `ng g @angular/core:standalone` and select "Convert all components, directives and pipes to standalone"\n' +
70+
'2. Run `ng g @angular/core:standalone` and select "Remove unnecessary NgModule classes"\n' +
71+
'3. Run `ng g @angular/core:standalone` and select "Bootstrap the project using standalone APIs"',
72+
documentationUrl: 'https://angular.dev/reference/migrations/standalone',
73+
},
74+
{
75+
name: 'zoneless',
76+
description: 'Migrates the application to be zoneless.',
77+
documentationUrl: 'https://angular.dev/guide/zoneless',
78+
},
79+
];
80+
81+
const modernizeInputSchema = z.object({
82+
// Casting to [string, ...string[]] since the enum definition requires a nonempty array.
83+
transformations: z
84+
.array(z.enum(TRANSFORMATIONS.map((t) => t.name) as [string, ...string[]]))
85+
.optional(),
86+
});
87+
88+
export type ModernizeInput = z.infer<typeof modernizeInputSchema>;
89+
90+
function generateInstructions(transformationNames: string[]): string[] {
91+
if (transformationNames.length === 0) {
92+
return [
93+
'See https://angular.dev/best-practices for Angular best practices. ' +
94+
'You can call this tool if you have specific transformation you want to run.',
95+
];
96+
}
97+
98+
const instructions: string[] = [];
99+
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames?.includes(t.name));
100+
101+
for (const transformation of transformationsToRun) {
102+
let transformationInstructions = '';
103+
if (transformation.instructions) {
104+
transformationInstructions = transformation.instructions;
105+
} else {
106+
// If no instructions are included, default to running a cli schematic with the transformation name.
107+
const command = `ng generate @angular/core:${transformation.name}`;
108+
transformationInstructions = `To run the ${transformation.name} migration, execute the following command: \`${command}\`.`;
109+
}
110+
if (transformation.documentationUrl) {
111+
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
112+
}
113+
instructions.push(transformationInstructions);
114+
}
115+
116+
return instructions;
117+
}
118+
119+
export async function runModernization(input: ModernizeInput) {
120+
const structuredContent = { instructions: generateInstructions(input.transformations ?? []) };
121+
122+
return {
123+
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent) }],
124+
structuredContent,
125+
};
126+
}
127+
128+
export function registerModernizeTool(server: McpServer): void {
129+
server.registerTool(
130+
'modernize',
131+
{
132+
title: 'Modernize Angular Code',
133+
description:
134+
'<Purpose>\n' +
135+
'This tool modernizes Angular code by applying the latest best practices and syntax improvements, ' +
136+
'ensuring it is idiomatic, readable, and maintainable.\n\n' +
137+
'</Purpose>\n' +
138+
'<Use Cases>\n' +
139+
'* After generating new code: Run this tool immediately after creating new Angular components, directives, ' +
140+
'or services to ensure they adhere to modern standards.\n' +
141+
'* On existing code: Apply to existing TypeScript files (.ts) and Angular templates (.ng.html) to update ' +
142+
'them with the latest features, such as the new built-in control flow syntax.\n\n' +
143+
'* When the user asks for a specific transformation: When the transformation list is populated, ' +
144+
'these specific ones will be ran on the inputs.\n' +
145+
'</Use Cases>\n' +
146+
'<Transformations>\n' +
147+
TRANSFORMATIONS.map((t) => `* ${t.name}: ${t.description}`).join('\n') +
148+
'\n</Transformations>\n',
149+
annotations: {
150+
readOnlyHint: true,
151+
},
152+
inputSchema: modernizeInputSchema.shape,
153+
outputSchema: {
154+
instructions: z
155+
.array(z.string())
156+
.optional()
157+
.describe('A list of instructions on how to run the migrations.'),
158+
},
159+
},
160+
(input) => runModernization(input),
161+
);
162+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { ModernizeInput, runModernization } from './modernize';
10+
11+
describe('Modernize Tool', () => {
12+
async function getInstructions(input: ModernizeInput): Promise<string[] | undefined> {
13+
const { structuredContent } = await runModernization(input);
14+
15+
if (!structuredContent || !('instructions' in structuredContent)) {
16+
fail('Expected instructions to be present in the result');
17+
18+
return;
19+
}
20+
21+
return structuredContent.instructions;
22+
}
23+
24+
it('should return an instruction for a single transformation', async () => {
25+
const instructions = await getInstructions({
26+
transformations: ['self-closing-tags-migration'],
27+
});
28+
29+
expect(instructions).toEqual([
30+
'To run the self-closing-tags-migration migration, execute the following command: ' +
31+
'`ng generate @angular/core:self-closing-tags-migration`.\nFor more information, ' +
32+
'see https://angular.dev/reference/migrations/self-closing-tags.',
33+
]);
34+
});
35+
36+
it('should return instructions for multiple transformations', async () => {
37+
const instructions = await getInstructions({
38+
transformations: ['self-closing-tags-migration', 'test-bed-get'],
39+
});
40+
41+
const expectedInstructions = [
42+
'To run the self-closing-tags-migration migration, execute the following command: ' +
43+
'`ng generate @angular/core:self-closing-tags-migration`.\nFor more information, ' +
44+
'see https://angular.dev/reference/migrations/self-closing-tags.',
45+
'To run the test-bed-get migration, execute the following command: ' +
46+
'`ng generate @angular/core:test-bed-get`.\nFor more information, ' +
47+
'see https://angular.dev/guide/testing/dependency-injection.',
48+
];
49+
50+
expect(instructions?.sort()).toEqual(expectedInstructions.sort());
51+
});
52+
53+
it('should return a link to the best practices page when no transformations are requested', async () => {
54+
const instructions = await getInstructions({
55+
transformations: [],
56+
});
57+
58+
expect(instructions).toEqual([
59+
'See https://angular.dev/best-practices for Angular best practices. You can call this ' +
60+
'tool if you have specific transformation you want to run.',
61+
]);
62+
});
63+
64+
it('should return special instructions for standalone migration', async () => {
65+
const instructions = await getInstructions({
66+
transformations: ['standalone'],
67+
});
68+
69+
expect(instructions?.[0]).toContain(
70+
'Run the commands in the order listed below, verifying that your code builds and runs between each step:',
71+
);
72+
});
73+
});

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /