Skip to content

Commit d60f69c

Browse files
committed
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 31f2384 commit d60f69c

6 files changed

Lines changed: 265 additions & 22 deletions

File tree

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

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { registerBestPracticesTool } from './tools/best-practices';
1515
import { registerDocSearchTool } from './tools/doc-search';
1616
import { registerFindExampleTool } from './tools/examples';
1717
import { registerListProjectsTool } from './tools/projects';
18+
import { registerInstructionsResource } from './resources/instructions';
19+
import { registerModernizeTool } from './tools/modernize';
1820

1921
export async function createMcpServer(
2022
context: {
@@ -34,29 +36,9 @@ export async function createMcpServer(
3436
'When writing or modifying Angular code, use the MCP server and its tools instead of direct shell commands where possible.',
3537
});
3638

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

6143
// If run outside an Angular workspace (e.g., globally) skip the workspace specific tools.
6244
if (context.workspace) {

packages/angular/cli/src/commands/mcp/instructions/best-practices.md renamed to packages/angular/cli/src/commands/mcp/resources/best-practices.md

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

pnpm-lock.yaml

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)