66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9+ import { readdir } from 'node:fs/promises' ;
910import path from 'node:path' ;
1011import z from 'zod' ;
11- import { McpToolContext , declareTool } from './tool-registry' ;
12+ import { AngularWorkspace } from '../../../utilities/config' ;
13+ import { assertIsError } from '../../../utilities/error' ;
14+ import { declareTool } from './tool-registry' ;
1215
1316export const LIST_PROJECTS_TOOL = declareTool ( {
1417 name : 'list_projects' ,
1518 title : 'List Angular Projects' ,
16- description :
17- 'Lists the names of all applications and libraries defined within an Angular workspace. ' +
18- 'It reads the `angular.json` configuration file to identify the projects. ' ,
19+ description : `
20+ <Purpose>
21+ Provides a comprehensive overview of all Angular workspaces and projects within a monorepo.
22+ It is essential to use this tool as a first step before performing any project-specific actions to understand the available projects,
23+ their types, and their locations.
24+ </Purpose>
25+ <Use Cases>
26+ * Finding the correct project name to use in other commands (e.g., \`ng generate component my-comp --project=my-app\`).
27+ * Identifying the \`root\` and \`sourceRoot\` of a project to read, analyze, or modify its files.
28+ * Determining if a project is an \`application\` or a \`library\`.
29+ * Getting the \`selectorPrefix\` for a project before generating a new component to ensure it follows conventions.
30+ </Use Cases>
31+ <Operational Notes>
32+ * **Working Directory:** Shell commands for a project (like \`ng generate\`) **MUST**
33+ be executed from the parent directory of the \`path\` field for the relevant workspace.
34+ * **Disambiguation:** A monorepo may contain multiple workspaces (e.g., for different applications or even in output directories).
35+ Use the \`path\` of each workspace to understand its context and choose the correct project.
36+ </Operational Notes>` ,
1937 outputSchema : {
20- projects : z . array (
38+ workspaces : z . array (
2139 z . object ( {
22- name : z
23- . string ( )
24- . describe ( 'The name of the project, as defined in the `angular.json` file.' ) ,
25- type : z
26- . enum ( [ 'application' , 'library' ] )
27- . optional ( )
28- . describe ( `The type of the project, either 'application' or 'library'.` ) ,
29- root : z
30- . string ( )
31- . describe ( 'The root directory of the project, relative to the workspace root.' ) ,
32- sourceRoot : z
33- . string ( )
34- . describe (
35- `The root directory of the project's source files, relative to the workspace root.` ,
36- ) ,
37- selectorPrefix : z
38- . string ( )
39- . optional ( )
40- . describe (
41- 'The prefix to use for component selectors.' +
42- ` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.` ,
43- ) ,
40+ path : z . string ( ) . describe ( 'The path to the `angular.json` file for this workspace.' ) ,
41+ projects : z . array (
42+ z . object ( {
43+ name : z
44+ . string ( )
45+ . describe ( 'The name of the project, as defined in the `angular.json` file.' ) ,
46+ type : z
47+ . enum ( [ 'application' , 'library' ] )
48+ . optional ( )
49+ . describe ( `The type of the project, either 'application' or 'library'.` ) ,
50+ root : z
51+ . string ( )
52+ . describe ( 'The root directory of the project, relative to the workspace root.' ) ,
53+ sourceRoot : z
54+ . string ( )
55+ . describe (
56+ `The root directory of the project's source files, relative to the workspace root.` ,
57+ ) ,
58+ selectorPrefix : z
59+ . string ( )
60+ . optional ( )
61+ . describe (
62+ 'The prefix to use for component selectors.' +
63+ ` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.` ,
64+ ) ,
65+ } ) ,
66+ ) ,
4467 } ) ,
4568 ) ,
4669 } ,
4770 isReadOnly : true ,
4871 isLocalOnly : true ,
49- shouldRegister : ( context ) => ! ! context . workspace ,
5072 factory : createListProjectsHandler ,
5173} ) ;
5274
53- function createListProjectsHandler ( { workspace } : McpToolContext ) {
75+ /**
76+ * Recursively finds all 'angular.json' files in a directory, skipping 'node_modules'.
77+ * @param dir The directory to start the search from.
78+ * @returns An async generator that yields the full path of each found 'angular.json' file.
79+ */
80+ async function * findAngularJsonFiles ( dir : string ) : AsyncGenerator < string > {
81+ try {
82+ const entries = await readdir ( dir , { withFileTypes : true } ) ;
83+ for ( const entry of entries ) {
84+ const fullPath = path . join ( dir , entry . name ) ;
85+ if ( entry . isDirectory ( ) ) {
86+ if ( entry . name === 'node_modules' ) {
87+ continue ;
88+ }
89+ yield * findAngularJsonFiles ( fullPath ) ;
90+ } else if ( entry . name === 'angular.json' ) {
91+ yield fullPath ;
92+ }
93+ }
94+ } catch ( error ) {
95+ assertIsError ( error ) ;
96+ // Silently ignore errors for directories that cannot be read
97+ if ( error . code === 'EACCES' || error . code === 'EPERM' ) {
98+ return ;
99+ }
100+ throw error ;
101+ }
102+ }
103+
104+ async function createListProjectsHandler ( ) {
54105 return async ( ) => {
55- if ( ! workspace ) {
106+ const workspaces = [ ] ;
107+ const seenPaths = new Set < string > ( ) ;
108+ for await ( const configFile of findAngularJsonFiles ( process . cwd ( ) ) ) {
109+ // A workspace may be found multiple times in a monorepo
110+ const resolvedPath = path . resolve ( configFile ) ;
111+ if ( seenPaths . has ( resolvedPath ) ) {
112+ continue ;
113+ }
114+ seenPaths . add ( resolvedPath ) ;
115+
116+ const ws = await AngularWorkspace . load ( configFile ) ;
117+
118+ const projects = [ ] ;
119+ for ( const [ name , project ] of ws . projects . entries ( ) ) {
120+ projects . push ( {
121+ name,
122+ type : project . extensions [ 'projectType' ] as 'application' | 'library' | undefined ,
123+ root : project . root ,
124+ sourceRoot : project . sourceRoot ?? path . posix . join ( project . root , 'src' ) ,
125+ selectorPrefix : project . extensions [ 'prefix' ] as string ,
126+ } ) ;
127+ }
128+
129+ workspaces . push ( {
130+ path : configFile ,
131+ projects,
132+ } ) ;
133+ }
134+
135+ if ( workspaces . length === 0 ) {
56136 return {
57137 content : [
58138 {
@@ -63,32 +143,20 @@ function createListProjectsHandler({ workspace }: McpToolContext) {
63143 ' could not be located in the current directory or any of its parent directories.' ,
64144 } ,
65145 ] ,
66- structuredContent : { projects : [ ] } ,
146+ structuredContent : { workspaces : [ ] } ,
67147 } ;
68148 }
69149
70- const projects = [ ] ;
71- // Convert to output format
72- for ( const [ name , project ] of workspace . projects . entries ( ) ) {
73- projects . push ( {
74- name,
75- type : project . extensions [ 'projectType' ] as 'application' | 'library' | undefined ,
76- root : project . root ,
77- sourceRoot : project . sourceRoot ?? path . posix . join ( project . root , 'src' ) ,
78- selectorPrefix : project . extensions [ 'prefix' ] as string ,
79- } ) ;
80- }
81-
82150 // The structuredContent field is newer and may not be supported by all hosts.
83151 // A text representation of the content is also provided for compatibility.
84152 return {
85153 content : [
86154 {
87155 type : 'text' as const ,
88- text : `Projects in the Angular workspace:\n${ JSON . stringify ( projects ) } ` ,
156+ text : `Projects in the Angular workspace:\n${ JSON . stringify ( { workspaces } ) } ` ,
89157 } ,
90158 ] ,
91- structuredContent : { projects } ,
159+ structuredContent : { workspaces } ,
92160 } ;
93161 } ;
94162}
0 commit comments