Skip to content

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) {
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)