Skip to content

Commit ef008be

Browse files
committed
refactor(@angular/cli): Add spawn to MCP's host
1 parent 44b2a02 commit ef008be

File tree

3 files changed

+114
-69
lines changed

3 files changed

+114
-69
lines changed

packages/angular/cli/src/commands/mcp/host.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515

1616
import { existsSync as nodeExistsSync } from 'fs';
17-
import { spawn } from 'node:child_process';
17+
import { ChildProcess, spawn } from 'node:child_process';
1818
import { Stats } from 'node:fs';
1919
import { stat } from 'node:fs/promises';
2020

@@ -68,14 +68,33 @@ export interface Host {
6868
env?: Record<string, string>;
6969
},
7070
): Promise<{ stdout: string; stderr: string }>;
71+
72+
/**
73+
* Spawns a long-running child process and returns the `ChildProcess` object.
74+
* @param command The command to run.
75+
* @param args The arguments to pass to the command.
76+
* @param options Options for the child process.
77+
* @returns The spawned `ChildProcess` instance.
78+
*/
79+
spawn(
80+
command: string,
81+
args: readonly string[],
82+
options?: {
83+
stdio?: 'pipe' | 'ignore';
84+
cwd?: string;
85+
env?: Record<string, string>;
86+
},
87+
): ChildProcess;
7188
}
7289

7390
/**
7491
* A concrete implementation of the `Host` interface that runs on a local workspace.
7592
*/
7693
export const LocalWorkspaceHost: Host = {
7794
stat,
95+
7896
existsSync: nodeExistsSync,
97+
7998
runCommand: async (
8099
command: string,
81100
args: readonly string[],
@@ -127,4 +146,24 @@ export const LocalWorkspaceHost: Host = {
127146
});
128147
});
129148
},
149+
150+
spawn(
151+
command: string,
152+
args: readonly string[],
153+
options: {
154+
stdio?: 'pipe' | 'ignore';
155+
cwd?: string;
156+
env?: Record<string, string>;
157+
} = {},
158+
): ChildProcess {
159+
return spawn(command, args, {
160+
shell: false,
161+
stdio: options.stdio ?? 'pipe',
162+
cwd: options.cwd,
163+
env: {
164+
...process.env,
165+
...options.env,
166+
},
167+
});
168+
},
130169
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('Build Tool', () => {
1717
runCommand: jasmine.createSpy('runCommand').and.resolveTo({ stdout: '', stderr: '' }),
1818
stat: jasmine.createSpy('stat'),
1919
existsSync: jasmine.createSpy('existsSync'),
20-
};
20+
} as Partial<Host> as Host;
2121
});
2222

2323
it('should handle a successful build and extract the output path', async () => {

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

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88

99
import { ChildProcess, spawn } from 'child_process';
1010
import { z } from 'zod';
11-
import { McpToolContext, declareTool } from './tool-registry';
12-
13-
let devServerProcess: ChildProcess | null = null;
14-
const serverLogs: string[] = [];
11+
import { Host, LocalWorkspaceHost } from '../host';
12+
import { McpToolDeclaration, declareTool } from './tool-registry';
1513

1614
const serveToolInputSchema = z.object({
1715
command: z.enum(['start_devserver', 'stop_devserver']).describe('The subcommand to execute.'),
@@ -37,77 +35,83 @@ const serveToolOutputSchema = z.object({
3735

3836
export type ServeToolOutput = z.infer<typeof serveToolOutputSchema>;
3937

40-
function serveToolFactory(context: McpToolContext) {
41-
return (input: ServeToolInput) => {
42-
switch (input.command) {
43-
case 'start_devserver':
44-
if (devServerProcess) {
45-
return {
46-
structuredContent: {
47-
message: 'Development server is already running.',
48-
},
49-
};
50-
}
51-
52-
const args = ['serve'];
53-
if (input.project) {
54-
args.push(input.project);
55-
}
56-
if (input.configuration) {
57-
args.push(`--configuration=${input.configuration}`);
58-
}
59-
60-
serverLogs.length = 0; // Clear previous logs
61-
devServerProcess = spawn('ng', args, { stdio: 'pipe' });
62-
63-
devServerProcess.stdout?.on('data', (data) => {
64-
serverLogs.push(data.toString());
65-
});
66-
devServerProcess.stderr?.on('data', (data) => {
67-
serverLogs.push(data.toString());
68-
});
69-
70-
devServerProcess.on('close', () => {
71-
devServerProcess = null;
72-
});
73-
74-
return {
75-
structuredContent: {
76-
message: 'Development server started.',
77-
},
78-
};
79-
80-
case 'stop_devserver':
81-
if (!devServerProcess) {
82-
return {
83-
structuredContent: {
84-
message: 'Development server is not running.',
85-
},
86-
};
87-
}
88-
89-
devServerProcess.kill('SIGTERM');
90-
devServerProcess = null;
91-
92-
return {
93-
structuredContent: {
94-
message: 'Development server stopped.',
95-
logs: serverLogs,
96-
},
97-
};
98-
}
99-
};
38+
interface ServeContext {
39+
devServerProcess: ChildProcess | null;
40+
serverLogs: string[];
41+
}
42+
43+
function startDevserver(input: ServeToolInput, host: Host, context: ServeContext) {
44+
if (context.devServerProcess) {
45+
return createStructureContentOutput({
46+
message: 'Development server is already running.',
47+
});
48+
}
49+
50+
const args = ['serve'];
51+
if (input.project) {
52+
args.push(input.project);
53+
}
54+
if (input.configuration) {
55+
args.push(`--configuration=${input.configuration}`);
56+
}
57+
58+
context.serverLogs.length = 0; // Clear previous logs
59+
context.devServerProcess = host.spawn('ng', args, { stdio: 'pipe' });
60+
61+
context.devServerProcess.stdout?.on('data', (data) => {
62+
context.serverLogs.push(data.toString());
63+
});
64+
context.devServerProcess.stderr?.on('data', (data) => {
65+
context.serverLogs.push(data.toString());
66+
});
67+
68+
context.devServerProcess.on('close', () => {
69+
context.devServerProcess = null;
70+
});
71+
72+
return createStructureContentOutput({
73+
message: 'Development server started.',
74+
});
75+
}
76+
77+
function stopDevserver(context: ServeContext) {
78+
if (!context.devServerProcess) {
79+
return createStructureContentOutput({
80+
message: 'Development server is already not running.',
81+
});
82+
}
83+
84+
context.devServerProcess.kill('SIGTERM');
85+
context.devServerProcess = null;
86+
87+
return createStructureContentOutput({
88+
message: 'Development server stopped.',
89+
logs: context.serverLogs,
90+
});
91+
}
92+
93+
function runServeTool(input: ServeToolInput, host: Host, context: ServeContext) {
94+
switch (input.command) {
95+
case 'start_devserver':
96+
return startDevserver(input, host, context);
97+
case 'stop_devserver':
98+
return stopDevserver(context);
99+
}
100100
}
101101

102-
export const SERVE_TOOL = declareTool({
102+
export const SERVE_TOOL: McpToolDeclaration<
103+
typeof serveToolInputSchema.shape,
104+
typeof serveToolOutputSchema.shape
105+
> = declareTool({
103106
name: 'serve',
104107
title: 'Serve Tool',
105108
description: `
106109
<Purpose>
107110
Manages the Angular development server ("ng serve"). It allows you to start and stop the server as a background process.
108111
</Purpose>
109112
<Use Cases>
110-
* **Starting the Server:** Use the 'start_devserver' command to begin serving the application. The tool will return immediately while the server runs in the background.
113+
* **Starting the Server:** Use the 'start_devserver' command to begin serving the application. The tool will return immediately
114+
while the server runs in the background.
111115
* **Stopping the Server:** Use the 'stop_devserver' command to terminate the running development server and retrieve the logs.
112116
</Use Cases>
113117
<Operational Notes>
@@ -120,5 +124,7 @@ Manages the Angular development server ("ng serve"). It allows you to start and
120124
isLocalOnly: true,
121125
inputSchema: serveToolInputSchema.shape,
122126
outputSchema: serveToolOutputSchema.shape,
123-
factory: serveToolFactory,
127+
factory: () => (input) => {
128+
return runServeTool(input, LocalWorkspaceHost, { devServerProcess: null, serverLogs: [] });
129+
},
124130
});

0 commit comments

Comments
 (0)