Skip to content

Commit 8fc2eed

Browse files
committed
feat(@angular/cli): Split "driver" MCP tool into start/stop/wait.
1 parent ef008be commit 8fc2eed

File tree

9 files changed

+413
-133
lines changed

9 files changed

+413
-133
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { ChildProcess } from 'child_process';
10+
import { Host } from './host';
11+
12+
const BUILD_SUCCEEDED_MESSAGE = 'Application bundle generation complete.';
13+
const BUILD_FAILED_MESSAGE = 'Application bundle generation failed.';
14+
const WAITING_FOR_CHANGES_MESSAGE = 'Watch mode enabled. Watching for file changes...';
15+
const CHANGES_DETECTED_START_MESSAGE = '❯ Changes detected. Rebuilding...';
16+
const CHANGES_DETECTED_SUCCESS_MESSAGE = '✔ Changes detected. Rebuilding...';
17+
18+
const BUILD_START_MESSAGES = [CHANGES_DETECTED_START_MESSAGE];
19+
const BUILD_END_MESSAGES = [
20+
BUILD_SUCCEEDED_MESSAGE,
21+
BUILD_FAILED_MESSAGE,
22+
WAITING_FOR_CHANGES_MESSAGE,
23+
CHANGES_DETECTED_SUCCESS_MESSAGE,
24+
];
25+
26+
export type BuildStatus = 'success' | 'failure' | 'unknown';
27+
28+
/**
29+
* An Angular development server managed by the MCP server.
30+
*/
31+
export interface DevServer {
32+
/**
33+
* Launches the dev server and returns immediately.
34+
*
35+
* Throws if this server is already running.
36+
*/
37+
start(): void;
38+
39+
/**
40+
* If the dev server is running, stops it.
41+
*/
42+
stop(): void;
43+
44+
/**
45+
* Gets all the server logs so far (stdout + stderr).
46+
*/
47+
getServerLogs(): string[];
48+
49+
/**
50+
* Gets all the server logs from the latest build.
51+
*/
52+
getMostRecentBuild(): { status: BuildStatus; logs: string[] };
53+
54+
/**
55+
* Whether the dev server is currently being built, or is awaiting further changes.
56+
*/
57+
isBuilding(): boolean;
58+
}
59+
60+
/**
61+
* A local Angular development server managed by the MCP server.
62+
*/
63+
export class LocalDevServer implements DevServer {
64+
readonly host: Host;
65+
readonly port?: number;
66+
readonly project?: string;
67+
68+
private devServerProcess: ChildProcess | null = null;
69+
private serverLogs: string[] = [];
70+
private buildInProgress = false;
71+
private latestBuildLogStartIndex = 0;
72+
private latestBuildLogEndIndex = 0;
73+
private latestBuildStatus?: BuildStatus;
74+
75+
constructor({ host, port, project }: { host: Host; port?: number; project?: string }) {
76+
this.host = host;
77+
this.project = project;
78+
this.port = port;
79+
}
80+
81+
start() {
82+
if (this.devServerProcess) {
83+
throw Error('Dev server already started.');
84+
}
85+
86+
const args = ['serve'];
87+
if (this.project) {
88+
args.push(this.project);
89+
}
90+
if (this.port) {
91+
args.push(`--port=${this.port}`);
92+
}
93+
94+
this.devServerProcess = this.host.spawn('ng', args, { stdio: 'pipe' });
95+
this.devServerProcess.stdout?.on('data', (data) => {
96+
this.serverLogs.push(data.toString());
97+
});
98+
this.devServerProcess.stderr?.on('data', (data) => {
99+
this.serverLogs.push(data.toString());
100+
});
101+
}
102+
103+
private addLog(log: string) {
104+
this.serverLogs.push(log);
105+
106+
if (BUILD_START_MESSAGES.some((message) => log.startsWith(message))) {
107+
this.buildInProgress = true;
108+
this.latestBuildLogStartIndex = this.serverLogs.length;
109+
} else if (BUILD_END_MESSAGES.some((message) => log.startsWith(message))) {
110+
this.buildInProgress = false;
111+
this.latestBuildLogEndIndex = this.serverLogs.length;
112+
this.latestBuildStatus = log.startsWith(BUILD_FAILED_MESSAGE) ? 'success' : 'failure';
113+
}
114+
}
115+
116+
stop() {
117+
this.devServerProcess?.kill();
118+
this.devServerProcess = null;
119+
}
120+
121+
getServerLogs(): string[] {
122+
return [...this.serverLogs];
123+
}
124+
125+
getMostRecentBuild() {
126+
return {
127+
status: this.latestBuildStatus ?? 'unknown',
128+
logs: this.serverLogs.slice(this.latestBuildLogStartIndex, this.latestBuildLogEndIndex),
129+
};
130+
}
131+
132+
isBuilding() {
133+
return this.buildInProgress;
134+
}
135+
}

packages/angular/cli/src/commands/mcp/mcp-server.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import { FIND_EXAMPLE_TOOL } from './tools/examples';
1919
import { MODERNIZE_TOOL } from './tools/modernize';
2020
import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration';
2121
import { LIST_PROJECTS_TOOL } from './tools/projects';
22-
import { SERVE_TOOL } from './tools/serve';
22+
import { START_DEVSERVER_TOOL } from './tools/start-devserver';
23+
import { STOP_DEVSERVER_TOOL } from './tools/stop-devserver';
2324
import { AnyMcpToolDeclaration, registerTools } from './tools/tool-registry';
25+
import { WAIT_FOR_DEVSERVER_BUILD_TOOL } from './tools/wait-for-devserver-build';
2426

2527
/**
2628
* The set of tools that are enabled by default for the MCP server.
@@ -35,11 +37,20 @@ const STABLE_TOOLS = [
3537
LIST_PROJECTS_TOOL,
3638
] as const;
3739

40+
/**
41+
* Tools to manage devservers. Should be bundled together.
42+
*/
43+
const SERVE_TOOLS = [START_DEVSERVER_TOOL, STOP_DEVSERVER_TOOL, WAIT_FOR_DEVSERVER_BUILD_TOOL];
44+
3845
/**
3946
* The set of tools that are available but not enabled by default.
4047
* These tools are considered experimental and may have limitations.
4148
*/
42-
export const EXPERIMENTAL_TOOLS = [MODERNIZE_TOOL, ZONELESS_MIGRATION_TOOL, SERVE_TOOL] as const;
49+
export const EXPERIMENTAL_TOOLS = [
50+
MODERNIZE_TOOL,
51+
ZONELESS_MIGRATION_TOOL,
52+
...SERVE_TOOLS,
53+
] as const;
4354

4455
export async function createMcpServer(
4556
options: {

packages/angular/cli/src/commands/mcp/tools/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { z } from 'zod';
1010
import { CommandError, Host, LocalWorkspaceHost } from '../host';
11+
import { createStructureContentOutput } from '../utils';
1112
import { McpToolContext, McpToolDeclaration, declareTool } from './tool-registry';
1213

1314
const CONFIGURATIONS = {

packages/angular/cli/src/commands/mcp/tools/serve.ts

Lines changed: 0 additions & 130 deletions
This file was deleted.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { z } from 'zod';
10+
import { DevServer, LocalDevServer } from '../dev-server';
11+
import { Host, LocalWorkspaceHost } from '../host';
12+
import { createStructureContentOutput } from '../utils';
13+
import { McpToolContext, McpToolDeclaration, declareTool } from './tool-registry';
14+
15+
const startDevserverToolInputSchema = z.object({
16+
project: z
17+
.string()
18+
.optional()
19+
.describe(
20+
'Which project to serve in a monorepo context. If not provided, serves the default project.',
21+
),
22+
port: z.number().optional().describe('Port to listen on.'),
23+
});
24+
25+
export type StartDevserverToolInput = z.infer<typeof startDevserverToolInputSchema>;
26+
27+
const startDevserverToolOutputSchema = z.object({
28+
message: z.string().describe('A message indicating the result of the operation.'),
29+
});
30+
31+
export type StartDevserverToolOutput = z.infer<typeof startDevserverToolOutputSchema>;
32+
33+
export const DEFAULT_PROJECT_KEY = '<default>';
34+
35+
function startDevserver(input: StartDevserverToolInput, context: McpToolContext, host: Host) {
36+
const projectKey = input.project ?? DEFAULT_PROJECT_KEY;
37+
if (context.devServers?.has(projectKey)) {
38+
return createStructureContentOutput({
39+
message: `Development server for project '${projectKey}' is already running.`,
40+
});
41+
}
42+
43+
const devServer = new LocalDevServer({ host, project: input.project, port: input.port });
44+
devServer.start();
45+
46+
if (!context.devServers) {
47+
context.devServers = new Map<string, DevServer>();
48+
}
49+
context.devServers.set(projectKey, devServer);
50+
51+
return createStructureContentOutput({
52+
message: `Development server for project '${projectKey}' started.`,
53+
});
54+
}
55+
56+
export const START_DEVSERVER_TOOL: McpToolDeclaration<
57+
typeof startDevserverToolInputSchema.shape,
58+
typeof startDevserverToolOutputSchema.shape
59+
> = declareTool({
60+
name: 'start_devserver',
61+
title: 'Start Development Server',
62+
description: `
63+
<Purpose>
64+
Starts the Angular development server ("ng serve") as a background process.
65+
</Purpose>
66+
<Use Cases>
67+
* **Starting the Server:** Use this tool to begin serving the application. The tool will return immediately
68+
while the server runs in the background.
69+
</Use Cases>
70+
<Operational Notes>
71+
* This tool manages a development server instance for each project.
72+
* This is an asynchronous operation. Subsequent commands can be run while the server is active.
73+
* Use 'stop_devserver' to gracefully shut down the server and access the full log output.
74+
</Operational Notes>
75+
`,
76+
isReadOnly: false,
77+
isLocalOnly: true,
78+
inputSchema: startDevserverToolInputSchema.shape,
79+
outputSchema: startDevserverToolOutputSchema.shape,
80+
factory: (context) => (input) => {
81+
return startDevserver(input, context, LocalWorkspaceHost);
82+
},
83+
});

0 commit comments

Comments
 (0)