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' ;
914import { z } from 'zod' ;
10- import { declareTool } from './tool-registry' ;
15+ import { McpToolDeclaration , declareTool } from './tool-registry' ;
1116
1217interface Transformation {
1318 name : string ;
@@ -18,13 +23,13 @@ interface Transformation {
1823
1924const 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
6974const 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
8096export 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