Skip to content

refactor(@angular/cli): move MCP list projects tool to separate file #30756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 2 additions & 83 deletions packages/angular/cli/src/commands/mcp/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 '<app-my-component>'.`,
),
}),
),
},
},
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);

Expand Down
103 changes: 103 additions & 0 deletions packages/angular/cli/src/commands/mcp/tools/projects.ts
Original file line number Diff line number Diff line change
@@ -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 '<app-my-component>'.`,
),
}),
),
},
},
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 },
};
},
);
}