Skip to content

Commit 17889c2

Browse files
committed
refactor(@angular/cli): Change modernize MCP to invoke schematics directly
1 parent 512ad28 commit 17889c2

File tree

2 files changed

+314
-93
lines changed

2 files changed

+314
-93
lines changed

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

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

9+
import { exec } from 'child_process';
10+
import { existsSync } from 'fs';
11+
import { stat } from 'fs/promises';
12+
import { dirname, join, relative } from 'path';
13+
import { promisify } from 'util';
914
import { z } from 'zod';
10-
import { declareTool } from './tool-registry';
15+
import { McpToolDeclaration, declareTool } from './tool-registry';
1116

1217
interface Transformation {
1318
name: string;
@@ -18,13 +23,13 @@ interface Transformation {
1823

1924
const TRANSFORMATIONS: Array<Transformation> = [
2025
{
21-
name: 'control-flow-migration',
26+
name: 'control-flow',
2227
description:
2328
'Migrates from `*ngIf`, `*ngFor`, and `*ngSwitch` to the new `@if`, `@for`, and `@switch` block syntax in templates.',
2429
documentationUrl: 'https://angular.dev/reference/migrations/control-flow',
2530
},
2631
{
27-
name: 'self-closing-tags-migration',
32+
name: 'self-closing-tag',
2833
description:
2934
'Converts tags for elements with no content to be self-closing (e.g., `<app-foo></app-foo>` becomes `<app-foo />`).',
3035
documentationUrl: 'https://angular.dev/reference/migrations/self-closing-tags',
@@ -67,57 +72,134 @@ const TRANSFORMATIONS: Array<Transformation> = [
6772
];
6873

6974
const modernizeInputSchema = z.object({
70-
// Casting to [string, ...string[]] since the enum definition requires a nonempty array.
75+
directories: z
76+
.array(z.string())
77+
.optional()
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[]]))
7381
.optional()
82+
.describe('A list of specific transformations to apply.'),
83+
});
84+
85+
const modernizeOutputSchema = z.object({
86+
instructions: z
87+
.array(z.string())
88+
.optional()
7489
.describe(
75-
'A list of specific transformations to get instructions for. ' +
76-
'If omitted, general guidance is provided.',
90+
'Migration summary, as well as any instructions that need to be performed to complete the migrations.',
7791
),
92+
stdout: z.string().optional().describe('The stdout from the executed commands.'),
93+
stderr: z.string().optional().describe('The stderr from the executed commands.'),
7894
});
7995

8096
export type ModernizeInput = z.infer<typeof modernizeInputSchema>;
97+
export type ModernizeOutput = z.infer<typeof modernizeOutputSchema>;
98+
99+
const execAsync = promisify(exec);
100+
101+
function createToolOutput(structuredContent: ModernizeOutput) {
102+
return {
103+
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent, null, 2) }],
104+
structuredContent,
105+
};
106+
}
107+
108+
function findAngularJsonDir(startDir: string): string | null {
109+
let currentDir = startDir;
110+
while (true) {
111+
if (existsSync(join(currentDir, 'angular.json'))) {
112+
return currentDir;
113+
}
114+
const parentDir = dirname(currentDir);
115+
if (parentDir === currentDir) {
116+
return null;
117+
}
118+
currentDir = parentDir;
119+
}
120+
}
121+
122+
export async function runModernization(input: ModernizeInput) {
123+
const transformationNames = input.transformations ?? [];
124+
const directories = input.directories ?? [];
81125

82-
function generateInstructions(transformationNames: string[]): string[] {
83126
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-
];
127+
return createToolOutput({
128+
instructions: [
129+
'See https://angular.dev/best-practices for Angular best practices. ' +
130+
'You can call this tool if you have specific transformation you want to run.',
131+
],
132+
});
133+
}
134+
if (directories.length === 0) {
135+
return createToolOutput({
136+
instructions: [
137+
'Provide this tool with a list of directory paths in your workspace ' +
138+
'to run the modernization on.',
139+
],
140+
});
141+
}
142+
143+
const firstDir = directories[0];
144+
const executionDir = (await stat(firstDir)).isDirectory() ? firstDir : dirname(firstDir);
145+
146+
const angularProjectRoot = findAngularJsonDir(executionDir);
147+
if (!angularProjectRoot) {
148+
return createToolOutput({
149+
instructions: ['Could not find an angular.json file in the current or parent directories.'],
150+
});
88151
}
89152

90153
const instructions: string[] = [];
91-
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames?.includes(t.name));
154+
const stdoutMessages: string[] = [];
155+
const stderrMessages: string[] = [];
156+
const transformationsToRun = TRANSFORMATIONS.filter((t) => transformationNames.includes(t.name));
92157

93158
for (const transformation of transformationsToRun) {
94-
let transformationInstructions = '';
95159
if (transformation.instructions) {
96-
transformationInstructions = transformation.instructions;
160+
// This is a complex case, return instructions.
161+
let transformationInstructions = transformation.instructions;
162+
if (transformation.documentationUrl) {
163+
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
164+
}
165+
instructions.push(transformationInstructions);
97166
} 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}\`.`;
167+
// Simple case, run the command.
168+
for (const dir of directories) {
169+
const relativePath = relative(angularProjectRoot, dir) || '.';
170+
const command = `ng generate @angular/core:${transformation.name} --path ${relativePath}`;
171+
try {
172+
const { stdout, stderr } = await execAsync(command, { cwd: angularProjectRoot });
173+
if (stdout) {
174+
stdoutMessages.push(stdout);
175+
}
176+
if (stderr) {
177+
stderrMessages.push(stderr);
178+
}
179+
instructions.push(
180+
`Migration ${transformation.name} on directory ${relativePath} completed successfully.`,
181+
);
182+
} catch (e) {
183+
stderrMessages.push((e as Error).message);
184+
instructions.push(
185+
`Migration ${transformation.name} on directory ${relativePath} failed.`,
186+
);
187+
}
188+
}
101189
}
102-
if (transformation.documentationUrl) {
103-
transformationInstructions += `\nFor more information, see ${transformation.documentationUrl}.`;
104-
}
105-
instructions.push(transformationInstructions);
106190
}
107191

108-
return instructions;
192+
return createToolOutput({
193+
instructions: instructions.length > 0 ? instructions : undefined,
194+
stdout: stdoutMessages?.join('\n\n') ?? undefined,
195+
stderr: stderrMessages?.join('\n\n') ?? undefined,
196+
});
109197
}
110198

111-
export async function runModernization(input: ModernizeInput) {
112-
const structuredContent = { instructions: generateInstructions(input.transformations ?? []) };
113-
114-
return {
115-
content: [{ type: 'text' as const, text: JSON.stringify(structuredContent) }],
116-
structuredContent,
117-
};
118-
}
119-
120-
export const MODERNIZE_TOOL = declareTool({
199+
export const MODERNIZE_TOOL: McpToolDeclaration<
200+
typeof modernizeInputSchema.shape,
201+
typeof modernizeOutputSchema.shape
202+
> = declareTool({
121203
name: 'modernize',
122204
title: 'Modernize Angular Code',
123205
description: `
@@ -135,25 +217,19 @@ generating the exact steps needed to perform specific migrations.
135217
general best practices guide.
136218
</Use Cases>
137219
<Operational Notes>
138-
* **Execution:** This tool **provides instructions**, which you **MUST** then execute as shell commands.
139-
It does not modify code directly.
220+
* **Execution:** This tool executes 'ng generate' commands for simple migrations in a temporary
221+
environment using the provided file content. For complex migrations like 'standalone', it
222+
provides instructions which you **MUST** then execute as shell commands.
223+
* **File Modifications:** This tool has been fixed and now correctly finds the node_modules directory in a Bazel environment.
140224
* **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.
225+
The tool will provide instructions. You **MUST** execute the commands in the exact order
226+
provided and validate your application between each step.
143227
* **Transformation List:** The following transformations are available:
144228
${TRANSFORMATIONS.map((t) => ` * ${t.name}: ${t.description}`).join('\n')}
145229
</Operational Notes>`,
146230
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-
},
231+
outputSchema: modernizeOutputSchema.shape,
156232
isLocalOnly: true,
157-
isReadOnly: true,
158-
factory: () => (input) => runModernization(input),
233+
isReadOnly: false,
234+
factory: () => runModernization,
159235
});

0 commit comments

Comments
 (0)