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' ;
915import { z } from 'zod' ;
10- import { declareTool } from './tool-registry' ;
16+ import { McpToolDeclaration , declareTool } from './tool-registry' ;
1117
1218interface Transformation {
1319 name : string ;
@@ -18,13 +24,13 @@ interface Transformation {
1824
1925const 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
6975const 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
8092export 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