Skip to content

Commit 4b4619a

Browse files
committed
refactor(@angular/cli): Change modernize MCP to invoke schematics directly
1 parent b4dee39 commit 4b4619a

4 files changed

Lines changed: 347 additions & 103 deletions

File tree

packages/angular/cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ ts_project(
7070
"//:node_modules/@types/semver",
7171
"//:node_modules/@types/yargs",
7272
"//:node_modules/@types/yarnpkg__lockfile",
73+
"//:node_modules/fast-glob",
7374
"//:node_modules/listr2",
7475
"//:node_modules/semver",
7576
"//:node_modules/typescript",

packages/angular/cli/src/commands/mcp/tools/modernize.ts

Lines changed: 123 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { exec } from 'child_process';
10+
import fastGlob from 'fast-glob';
11+
import { existsSync } from 'fs';
12+
import { readFile, stat } from 'fs/promises';
13+
import { dirname, join, relative } from 'path';
14+
import { promisify } from 'util';
915
import { z } from 'zod';
10-
import { declareTool } from './tool-registry';
16+
import { McpToolDeclaration, declareTool } from './tool-registry';
1117

1218
interface Transformation {
1319
name: string;
@@ -18,13 +24,13 @@ interface Transformation {
1824

1925
const TRANSFORMATIONS: Array<Transformation> = [
2026
{
21-
name: 'control-flow-migration',
27+
name: 'control-flow',
2228
description:
2329
'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.',
2430
documentationUrl: 'https://angular.dev/reference/migrations/control-flow',
2531
},
2632
{
27-
name: 'self-closing-tags-migration',
33+
name: 'self-closing-tag',
2834
description:
2935
'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
3036
documentationUrl: 'https://angular.dev/reference/migrations/self-closing-tags',
@@ -67,57 +73,134 @@ const TRANSFORMATIONS: Array<Transformation> = [
6773
];
6874

6975
const modernizeInputSchema = z.object({
70-
// Casting to [string, ...string[]] since the enum definition requires a nonempty array.
76+
directories: z
77+
.array(z.string())
78+
.describe('A list of paths to directories with files to modernize.'),
7179
transformations: z
7280
.array(z.enum(TRANSFORMATIONS.map((t) => t.name) as [string, ...string[]]))
73-
.optional()
74-
.describe(
75-
'A list of specific transformations to get instructions for. ' +
76-
'If omitted, general guidance is provided.',
77-
),
81+
.describe('A list of specific transformations to apply.'),
82+
});
83+
84+
const modernizeOutputSchema = z.object({
85+
instructions: z
86+
.array(z.string())
87+
.describe('Any instructions that need to be performed to complete the migrations.'),
88+
stdout: z.string().optional().describe('The stdout from the executed commands.'),
89+
stderr: z.string().optional().describe('The stderr from the executed commands.'),
7890
});
7991

8092
export type ModernizeInput = z.infer<typeof modernizeInputSchema>;
93+
export type ModernizeOutput = z.infer<typeof modernizeOutputSchema>;
94+
95+
const execAsync = promisify(exec);
8196

82-
function generateInstructions(transformationNames: string[]): string[] {
97+
export async function runModernization(input: ModernizeInput) {
98+
const transformationNames = input.transformations;
8399
if (transformationNames.length === 0) {
84-
return [
85-
'See https://angular.dev/best-practices for Angular best practices. ' +
86-
'You can call this tool if you have specific transformation you want to run.',
87-
];
100+
const structuredContent: ModernizeOutput = {
101+
instructions: [
102+
'See https://angular.dev/best-practices for Angular best practices. ' +
103+
'You can call this tool if you have specific transformation you want to run.',
104+
],
105+
};
106+
107+
return {
108+
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }],
109+
structuredContent,
110+
};
111+
}
112+
if (input.directories.length === 0) {
113+
const structuredContent: ModernizeOutput = {
114+
instructions: [
115+
'Provide this tool with a list of directory paths in your workspace ' +
116+
'to run the modernization on.',
117+
],
118+
};
119+
120+
return {
121+
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }],
122+
structuredContent,
123+
};
124+
}
125+
126+
const firstDir = input.directories[0];
127+
const fileList = await fastGlob('**/*', { cwd: firstDir, ignore: ['node_modules'] });
128+
const executionDir = (await stat(firstDir)).isDirectory() ? firstDir : dirname(firstDir);
129+
130+
const findAngularJsonDir = (startDir: string): string | null => {
131+
let currentDir = startDir;
132+
while (true) {
133+
if (existsSync(join(currentDir, 'angular.json'))) {
134+
return currentDir;
135+
}
136+
const parentDir = dirname(currentDir);
137+
if (parentDir === currentDir) {
138+
return null;
139+
}
140+
currentDir = parentDir;
141+
}
142+
};
143+
144+
const angularProjectRoot = findAngularJsonDir(executionDir);
145+
if (!angularProjectRoot) {
146+
throw new Error('Could not find an angular.json file in the current or parent directories.');
88147
}
89148

90149
const instructions: string[] = [];
91-
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames?.includes(t.name));
150+
const stdoutMessages: string[] = [];
151+
const stderrMessages: string[] = [];
152+
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames.includes(t.name));
92153

93154
for (const transformation of transformationsToRun) {
94-
let transformationInstructions = '';
95155
if (transformation.instructions) {
96-
transformationInstructions = transformation.instructions;
156+
// This is a complex case, return instructions.
157+
let transformationInstructions = transformation.instructions;
158+
if (transformation.documentationUrl) {
159+
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
160+
}
161+
instructions.push(transformationInstructions);
97162
} else {
98-
// If no instructions are included, default to running a cli schematic with the transformation name.
99-
const command = `ng generate @angular/core:${transformation.name}`;
100-
transformationInstructions = `To run the ${transformation.name} migration, execute the following command: \`${command}\`.`;
163+
// Simple case, run the command.
164+
for (const dir of input.directories) {
165+
const relativePath = relative(angularProjectRoot, dir) || '.';
166+
const command = `ng generate @angular/core:${transformation.name} --path ${relativePath}`;
167+
try {
168+
const { stdout, stderr } = await execAsync(command, { cwd: angularProjectRoot });
169+
if (stdout) {
170+
stdoutMessages.push(stdout);
171+
}
172+
if (stderr) {
173+
stderrMessages.push(stderr);
174+
}
175+
instructions.push(
176+
`Migration ${transformation.name} on directory ${relativePath} completed successfully.`,
177+
);
178+
} catch (e) {
179+
stderrMessages.push((e as Error).message);
180+
instructions.push(
181+
`Migration ${transformation.name} on directory ${relativePath} failed.`,
182+
);
183+
}
184+
}
101185
}
102-
if (transformation.documentationUrl) {
103-
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
104-
}
105-
instructions.push(transformationInstructions);
106186
}
107187

108-
return instructions;
109-
}
110-
111-
export async function runModernization(input: ModernizeInput) {
112-
const structuredContent = { instructions: generateInstructions(input.transformations ?? []) };
188+
const structuredContent: ModernizeOutput = {
189+
instructions,
190+
stdout: stdoutMessages.join('\n\n'),
191+
stderr: stderrMessages.join('\n\n'),
192+
};
113193

114194
return {
115-
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent) }],
195+
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }],
116196
structuredContent,
117197
};
118198
}
119199

120-
export const MODERNIZE_TOOL = declareTool({
200+
export const MODERNIZE_TOOL: McpToolDeclaration<
201+
typeof modernizeInputSchema.shape,
202+
typeof modernizeOutputSchema.shape
203+
> = declareTool({
121204
name: 'modernize',
122205
title: 'Modernize Angular Code',
123206
description: `
@@ -135,25 +218,19 @@ generating the exact steps needed to perform specific migrations.
135218
general best practices guide.
136219
</Use Cases>
137220
<Operational Notes>
138-
* **Execution:** This tool **provides instructions**, which you **MUST** then execute as shell commands.
139-
It does not modify code directly.
221+
* **Execution:** This tool executes 'ng generate' commands for simple migrations in a temporary
222+
environment using the provided file content. For complex migrations like 'standalone', it
223+
provides instructions which you **MUST** then execute as shell commands.
224+
* **File Modifications:** This tool has been fixed and now correctly finds the node_modules directory in a Bazel environment.
140225
* **Standalone Migration:** The 'standalone' transformation is a special, multi-step process.
141-
You **MUST** execute the commands in the exact order provided and validate your application
142-
between each step.
226+
The tool will provide instructions. You **MUST** execute the commands in the exact order
227+
provided and validate your application between each step.
143228
* **Transformation List:** The following transformations are available:
144229
${TRANSFORMATIONS.map((t) => ` * ${t.name}: ${t.description}`).join('\n')}
145230
</Operational Notes>`,
146231
inputSchema: modernizeInputSchema.shape,
147-
outputSchema: {
148-
instructions: z
149-
.array(z.string())
150-
.optional()
151-
.describe(
152-
'A list of instructions and shell commands to run the requested modernizations. ' +
153-
'Each string in the array is a separate step or command.',
154-
),
155-
},
232+
outputSchema: modernizeOutputSchema.shape,
156233
isLocalOnly: true,
157-
isReadOnly: true,
158-
factory: () => (input) => runModernization(input),
234+
isReadOnly: false,
235+
factory: () => runModernization,
159236
});

0 commit comments

Comments
 (0)