Skip to content
Open
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
7 changes: 5 additions & 2 deletions packages/angular/cli/src/commands/mcp/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type CommandModuleImplementation,
} from '../../command-builder/command-module';
import { isTTY } from '../../utilities/tty';
import { EXPERIMENTAL_TOOLS, createMcpServer } from './mcp-server';
import { EXPERIMENTAL_TOOLS, EXPERIMENTAL_TOOL_GROUPS, createMcpServer } from './mcp-server';

const INTERACTIVE_MESSAGE = `
To start using the Angular CLI MCP Server, add this configuration to your host:
Expand Down Expand Up @@ -54,7 +54,10 @@ export default class McpCommandModule extends CommandModule implements CommandMo
alias: 'E',
array: true,
describe: 'Enable an experimental tool.',
choices: EXPERIMENTAL_TOOLS.map(({ name }) => name),
choices: [
...EXPERIMENTAL_TOOLS.map(({ name }) => name),
...Object.keys(EXPERIMENTAL_TOOL_GROUPS),
],
hidden: true,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type BuildStatus = 'success' | 'failure' | 'unknown';
/**
* An Angular development server managed by the MCP server.
*/
export interface DevServer {
export interface Devserver {
/**
* Launches the dev server and returns immediately.
*
Expand Down Expand Up @@ -64,19 +64,19 @@ export interface DevServer {
port: number;
}

export function devServerKey(project?: string) {
export function devserverKey(project?: string) {
return project ?? '<default>';
}

/**
* A local Angular development server managed by the MCP server.
*/
export class LocalDevServer implements DevServer {
export class LocalDevserver implements Devserver {
readonly host: Host;
readonly port: number;
readonly project?: string;

private devServerProcess: ChildProcess | null = null;
private devserverProcess: ChildProcess | null = null;
private serverLogs: string[] = [];
private buildInProgress = false;
private latestBuildLogStartIndex?: number = undefined;
Expand All @@ -89,7 +89,7 @@ export class LocalDevServer implements DevServer {
}

start() {
if (this.devServerProcess) {
if (this.devserverProcess) {
throw Error('Dev server already started.');
}

Expand All @@ -100,14 +100,14 @@ export class LocalDevServer implements DevServer {

args.push(`--port=${this.port}`);

this.devServerProcess = this.host.spawn('ng', args, { stdio: 'pipe' });
this.devServerProcess.stdout?.on('data', (data) => {
this.devserverProcess = this.host.spawn('ng', args, { stdio: 'pipe' });
this.devserverProcess.stdout?.on('data', (data) => {
this.addLog(data.toString());
});
this.devServerProcess.stderr?.on('data', (data) => {
this.devserverProcess.stderr?.on('data', (data) => {
this.addLog(data.toString());
});
this.devServerProcess.stderr?.on('close', () => {
this.devserverProcess.stderr?.on('close', () => {
this.stop();
});
this.buildInProgress = true;
Expand All @@ -127,8 +127,8 @@ export class LocalDevServer implements DevServer {
}

stop() {
this.devServerProcess?.kill();
this.devServerProcess = null;
this.devserverProcess?.kill();
this.devserverProcess = null;
}

getServerLogs(): string[] {
Expand Down
30 changes: 23 additions & 7 deletions packages/angular/cli/src/commands/mcp/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { join } from 'node:path';
import type { AngularWorkspace } from '../../utilities/config';
import { VERSION } from '../../utilities/version';
import type { DevServer } from './dev-server';
import type { Devserver } from './devserver';
import { registerInstructionsResource } from './resources/instructions';
import { AI_TUTOR_TOOL } from './tools/ai-tutor';
import { BEST_PRACTICES_TOOL } from './tools/best-practices';
import { BUILD_TOOL } from './tools/build';
import { START_DEVSERVER_TOOL } from './tools/devserver/start-devserver';
import { STOP_DEVSERVER_TOOL } from './tools/devserver/stop-devserver';
import { WAIT_FOR_DEVSERVER_BUILD_TOOL } from './tools/devserver/wait-for-devserver-build';
import { DEVSERVER_START_TOOL } from './tools/devserver/devserver-start';
import { DEVSERVER_STOP_TOOL } from './tools/devserver/devserver-stop';
import { DEVSERVER_WAIT_FOR_BUILD_TOOL } from './tools/devserver/devserver-wait-for-build';
import { DOC_SEARCH_TOOL } from './tools/doc-search';
import { FIND_EXAMPLE_TOOL } from './tools/examples';
import { MODERNIZE_TOOL } from './tools/modernize';
Expand All @@ -28,7 +28,16 @@ import { type AnyMcpToolDeclaration, registerTools } from './tools/tool-registry
/**
* Tools to manage devservers. Should be bundled together, then added to experimental or stable as a group.
*/
const SERVE_TOOLS = [START_DEVSERVER_TOOL, STOP_DEVSERVER_TOOL, WAIT_FOR_DEVSERVER_BUILD_TOOL];
const DEVSERVER_TOOLS = [DEVSERVER_START_TOOL, DEVSERVER_STOP_TOOL, DEVSERVER_WAIT_FOR_BUILD_TOOL];

/**
* Experimental tools that are grouped together under a single name.
*
* Used for enabling them as a group.
*/
export const EXPERIMENTAL_TOOL_GROUPS = {
'devserver': DEVSERVER_TOOLS,
};

/**
* The set of tools that are enabled by default for the MCP server.
Expand All @@ -47,7 +56,7 @@ const STABLE_TOOLS = [
* The set of tools that are available but not enabled by default.
* These tools are considered experimental and may have limitations.
*/
export const EXPERIMENTAL_TOOLS = [BUILD_TOOL, MODERNIZE_TOOL, ...SERVE_TOOLS] as const;
export const EXPERIMENTAL_TOOLS = [BUILD_TOOL, MODERNIZE_TOOL, ...DEVSERVER_TOOLS] as const;

export async function createMcpServer(
options: {
Expand Down Expand Up @@ -114,7 +123,7 @@ equivalent actions.
workspace: options.workspace,
logger,
exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'),
devServers: new Map<string, DevServer>(),
devservers: new Map<string, Devserver>(),
},
toolDeclarations,
);
Expand Down Expand Up @@ -146,6 +155,13 @@ export function assembleToolDeclarations(
if (process.env['NG_MCP_CODE_EXAMPLES'] === '1') {
enabledExperimentalTools.add('find_examples');
}
for (const [toolGroupName, toolGroup] of Object.entries(EXPERIMENTAL_TOOL_GROUPS)) {
if (enabledExperimentalTools.delete(toolGroupName)) {
for (const tool of toolGroup) {
enabledExperimentalTools.add(tool.name);
}
}
}

if (enabledExperimentalTools.size > 0) {
const experimentalToolsMap = new Map(experimentalDeclarations.map((tool) => [tool.name, tool]));
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/mcp/tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Perform a one-off, non-watched build using "ng build". Use this tool whenever th
</Use Cases>
<Operational Notes>
* This tool runs "ng build" so it expects to run within an Angular workspace.
* If you want a watched build which updates as files are changed, use "start_devserver" instead, which also serves the app.
* If you want a watched build which updates as files are changed, use "devserver_start" instead, which also serves the app.
* You can provide a project instead of building the root one. The "list_projects" MCP tool could be used to obtain the list of projects.
* This tool defaults to a development environment while a regular "ng build" defaults to a production environment. An unexpected build
failure might suggest the project is not configured for the requested environment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
*/

import { z } from 'zod';
import { LocalDevServer, devServerKey } from '../../dev-server';
import { LocalDevserver, devserverKey } from '../../devserver';
import { type Host, LocalWorkspaceHost } from '../../host';
import { createStructuredContentOutput } from '../../utils';
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';

const startDevServerToolInputSchema = z.object({
const devserverStartToolInputSchema = z.object({
project: z
.string()
.optional()
Expand All @@ -21,9 +21,9 @@ const startDevServerToolInputSchema = z.object({
),
});

export type StartDevserverToolInput = z.infer<typeof startDevServerToolInputSchema>;
export type DevserverStartToolInput = z.infer<typeof devserverStartToolInputSchema>;

const startDevServerToolOutputSchema = z.object({
const devserverStartToolOutputSchema = z.object({
message: z.string().describe('A message indicating the result of the operation.'),
address: z
.string()
Expand All @@ -33,71 +33,71 @@ const startDevServerToolOutputSchema = z.object({
),
});

export type StartDevserverToolOutput = z.infer<typeof startDevServerToolOutputSchema>;
export type DevserverStartToolOutput = z.infer<typeof devserverStartToolOutputSchema>;

function localhostAddress(port: number) {
return `http://localhost:${port}/`;
}

export async function startDevServer(
input: StartDevserverToolInput,
export async function startDevserver(
input: DevserverStartToolInput,
context: McpToolContext,
host: Host,
) {
const projectKey = devServerKey(input.project);
const projectKey = devserverKey(input.project);

let devServer = context.devServers.get(projectKey);
if (devServer) {
let devserver = context.devservers.get(projectKey);
if (devserver) {
return createStructuredContentOutput({
message: `Development server for project '${projectKey}' is already running.`,
address: localhostAddress(devServer.port),
address: localhostAddress(devserver.port),
});
}

const port = await host.getAvailablePort();

devServer = new LocalDevServer({ host, project: input.project, port });
devServer.start();
devserver = new LocalDevserver({ host, project: input.project, port });
devserver.start();

context.devServers.set(projectKey, devServer);
context.devservers.set(projectKey, devserver);

return createStructuredContentOutput({
message: `Development server for project '${projectKey}' started and watching for workspace changes.`,
address: localhostAddress(port),
});
}

export const START_DEVSERVER_TOOL: McpToolDeclaration<
typeof startDevServerToolInputSchema.shape,
typeof startDevServerToolOutputSchema.shape
export const DEVSERVER_START_TOOL: McpToolDeclaration<
typeof devserverStartToolInputSchema.shape,
typeof devserverStartToolOutputSchema.shape
> = declareTool({
name: 'start_devserver',
name: 'devserver_start',
title: 'Start Development Server',
description: `
<Purpose>
Starts the Angular development server ("ng serve") as a background process. Follow this up with "wait_for_devserver_build" to wait until
Starts the Angular development server ("ng serve") as a background process. Follow this up with "devserver_wait_for_build" to wait until
the first build completes.
</Purpose>
<Use Cases>
* **Starting the Server:** Use this tool to begin serving the application. The tool will return immediately while the server runs in the
background.
* **Get Initial Build Logs:** Once a dev server has started, use the "wait_for_devserver_build" tool to ensure it's alive. If there are any
build errors, "wait_for_devserver_build" would provide them back and you can give them to the user or rely on them to propose a fix.
* **Get Updated Build Logs:** Important: as long as a devserver is alive (i.e. "stop_devserver" wasn't called), after every time you make a
change to the workspace, re-run "wait_for_devserver_build" to see whether the change was successfully built and wait for the devserver to
* **Get Initial Build Logs:** Once a dev server has started, use the "devserver_wait_for_build" tool to ensure it's alive. If there are any
build errors, "devserver_wait_for_build" would provide them back and you can give them to the user or rely on them to propose a fix.
* **Get Updated Build Logs:** Important: as long as a devserver is alive (i.e. "devserver_stop" wasn't called), after every time you make a
change to the workspace, re-run "devserver_wait_for_build" to see whether the change was successfully built and wait for the devserver to
be updated.
</Use Cases>
<Operational Notes>
* This tool manages development servers by itself. It maintains at most a single dev server instance for each project in the monorepo.
* This is an asynchronous operation. Subsequent commands can be ran while the server is active.
* Use 'stop_devserver' to gracefully shut down the server and access the full log output.
* Use 'devserver_stop' to gracefully shut down the server and access the full log output.
</Operational Notes>
`,
isReadOnly: true,
isLocalOnly: true,
inputSchema: startDevServerToolInputSchema.shape,
outputSchema: startDevServerToolOutputSchema.shape,
inputSchema: devserverStartToolInputSchema.shape,
outputSchema: devserverStartToolOutputSchema.shape,
factory: (context) => (input) => {
return startDevServer(input, context, LocalWorkspaceHost);
return startDevserver(input, context, LocalWorkspaceHost);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
*/

import { z } from 'zod';
import { devServerKey } from '../../dev-server';
import { devserverKey } from '../../devserver';
import { createStructuredContentOutput } from '../../utils';
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';

const stopDevserverToolInputSchema = z.object({
const devserverStopToolInputSchema = z.object({
project: z
.string()
.optional()
Expand All @@ -20,18 +20,18 @@ const stopDevserverToolInputSchema = z.object({
),
});

export type StopDevserverToolInput = z.infer<typeof stopDevserverToolInputSchema>;
export type DevserverStopToolInput = z.infer<typeof devserverStopToolInputSchema>;

const stopDevserverToolOutputSchema = z.object({
const devserverStopToolOutputSchema = z.object({
message: z.string().describe('A message indicating the result of the operation.'),
logs: z.array(z.string()).optional().describe('The full logs from the dev server.'),
});

export type StopDevserverToolOutput = z.infer<typeof stopDevserverToolOutputSchema>;
export type DevserverStopToolOutput = z.infer<typeof devserverStopToolOutputSchema>;

export function stopDevserver(input: StopDevserverToolInput, context: McpToolContext) {
const projectKey = devServerKey(input.project);
const devServer = context.devServers.get(projectKey);
export function stopDevserver(input: DevserverStopToolInput, context: McpToolContext) {
const projectKey = devserverKey(input.project);
const devServer = context.devservers.get(projectKey);

if (!devServer) {
return createStructuredContentOutput({
Expand All @@ -41,23 +41,23 @@ export function stopDevserver(input: StopDevserverToolInput, context: McpToolCon
}

devServer.stop();
context.devServers.delete(projectKey);
context.devservers.delete(projectKey);

return createStructuredContentOutput({
message: `Development server for project '${projectKey}' stopped.`,
logs: devServer.getServerLogs(),
});
}

export const STOP_DEVSERVER_TOOL: McpToolDeclaration<
typeof stopDevserverToolInputSchema.shape,
typeof stopDevserverToolOutputSchema.shape
export const DEVSERVER_STOP_TOOL: McpToolDeclaration<
typeof devserverStopToolInputSchema.shape,
typeof devserverStopToolOutputSchema.shape
> = declareTool({
name: 'stop_devserver',
name: 'devserver_stop',
title: 'Stop Development Server',
description: `
<Purpose>
Stops a running Angular development server ("ng serve") that was started with the "start_devserver" tool.
Stops a running Angular development server ("ng serve") that was started with the "devserver_start" tool.
</Purpose>
<Use Cases>
* **Stopping the Server:** Use this tool to terminate a running development server and retrieve the logs.
Expand All @@ -70,8 +70,8 @@ Stops a running Angular development server ("ng serve") that was started with th
`,
isReadOnly: true,
isLocalOnly: true,
inputSchema: stopDevserverToolInputSchema.shape,
outputSchema: stopDevserverToolOutputSchema.shape,
inputSchema: devserverStopToolInputSchema.shape,
outputSchema: devserverStopToolOutputSchema.shape,
factory: (context) => (input) => {
return stopDevserver(input, context);
},
Expand Down
Loading