diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index 1ca13f8dde3b..6a51515a7014 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -9,11 +9,11 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; -import { z } from 'zod'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; import { registerBestPracticesTool } from './tools/best-practices'; import { registerDocSearchTool } from './tools/doc-search'; +import { registerListProjectsTool } from './tools/projects'; export async function createMcpServer(context: { workspace?: AngularWorkspace; @@ -50,88 +50,7 @@ export async function createMcpServer(context: { ); registerBestPracticesTool(server); - - server.registerTool( - 'list_projects', - { - title: 'List Angular Projects', - description: - 'Lists the names of all applications and libraries defined within an Angular workspace. ' + - 'It reads the `angular.json` configuration file to identify the projects. ', - annotations: { - readOnlyHint: true, - }, - outputSchema: { - projects: z.array( - z.object({ - name: z - .string() - .describe('The name of the project, as defined in the `angular.json` file.'), - type: z - .enum(['application', 'library']) - .optional() - .describe(`The type of the project, either 'application' or 'library'.`), - root: z - .string() - .describe('The root directory of the project, relative to the workspace root.'), - sourceRoot: z - .string() - .describe( - `The root directory of the project's source files, relative to the workspace root.`, - ), - selectorPrefix: z - .string() - .optional() - .describe( - 'The prefix to use for component selectors.' + - ` For example, a prefix of 'app' would result in selectors like ''.`, - ), - }), - ), - }, - }, - async () => { - const { workspace } = context; - - if (!workspace) { - return { - content: [ - { - type: 'text' as const, - text: - 'No Angular workspace found.' + - ' An `angular.json` file, which marks the root of a workspace,' + - ' could not be located in the current directory or any of its parent directories.', - }, - ], - }; - } - - const projects = []; - // Convert to output format - for (const [name, project] of workspace.projects.entries()) { - projects.push({ - name, - type: project.extensions['projectType'] as 'application' | 'library' | undefined, - root: project.root, - sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), - selectorPrefix: project.extensions['prefix'] as string, - }); - } - - // The structuredContent field is newer and may not be supported by all hosts. - // A text representation of the content is also provided for compatibility. - return { - content: [ - { - type: 'text' as const, - text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`, - }, - ], - structuredContent: { projects }, - }; - }, - ); + registerListProjectsTool(server, context); await registerDocSearchTool(server); diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts new file mode 100644 index 000000000000..08ebdf46174b --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; +import path from 'node:path'; +import z from 'zod'; +import type { AngularWorkspace } from '../../../utilities/config'; + +export function registerListProjectsTool( + server: McpServer, + context: { + workspace?: AngularWorkspace; + }, +): void { + server.registerTool( + 'list_projects', + { + title: 'List Angular Projects', + description: + 'Lists the names of all applications and libraries defined within an Angular workspace. ' + + 'It reads the `angular.json` configuration file to identify the projects. ', + annotations: { + readOnlyHint: true, + openWorldHint: false, + }, + outputSchema: { + projects: z.array( + z.object({ + name: z + .string() + .describe('The name of the project, as defined in the `angular.json` file.'), + type: z + .enum(['application', 'library']) + .optional() + .describe(`The type of the project, either 'application' or 'library'.`), + root: z + .string() + .describe('The root directory of the project, relative to the workspace root.'), + sourceRoot: z + .string() + .describe( + `The root directory of the project's source files, relative to the workspace root.`, + ), + selectorPrefix: z + .string() + .optional() + .describe( + 'The prefix to use for component selectors.' + + ` For example, a prefix of 'app' would result in selectors like ''.`, + ), + }), + ), + }, + }, + async () => { + const { workspace } = context; + + if (!workspace) { + return { + content: [ + { + type: 'text' as const, + text: + 'No Angular workspace found.' + + ' An `angular.json` file, which marks the root of a workspace,' + + ' could not be located in the current directory or any of its parent directories.', + }, + ], + structuredContent: { projects: [] }, + }; + } + + const projects = []; + // Convert to output format + for (const [name, project] of workspace.projects.entries()) { + projects.push({ + name, + type: project.extensions['projectType'] as 'application' | 'library' | undefined, + root: project.root, + sourceRoot: project.sourceRoot ?? path.posix.join(project.root, 'src'), + selectorPrefix: project.extensions['prefix'] as string, + }); + } + + // The structuredContent field is newer and may not be supported by all hosts. + // A text representation of the content is also provided for compatibility. + return { + content: [ + { + type: 'text' as const, + text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`, + }, + ], + structuredContent: { projects }, + }; + }, + ); +}