Skip to content

Commit 46068a7

Browse files
committed
fix(@angular/cli): dynamically resolve project Angular CLI executable inside MCP tools
The MCP server host runtime now dynamically resolves the precise Angular CLI binary from the workspace's node_modules rather than relying on global system path invocations. This guarantees full compatibility between a project's specific configuration constraints and the active framework version, preventing mismatches across monorepos or containerized setups.
1 parent 0f2342c commit 46068a7

File tree

1 file changed

+36
-2
lines changed
  • packages/angular/cli/src/commands/mcp

1 file changed

+36
-2
lines changed

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { Stats } from 'node:fs';
1919
import { glob as nodeGlob, readFile as nodeReadFile, stat } from 'node:fs/promises';
2020
import { createRequire } from 'node:module';
2121
import { createServer } from 'node:net';
22+
import { dirname, resolve } from 'node:path';
2223

2324
/**
2425
* An error thrown when a command fails to execute.
@@ -125,6 +126,36 @@ export interface Host {
125126
isPortAvailable(port: number): Promise<boolean>;
126127
}
127128

129+
function resolveCommand(
130+
command: string,
131+
args: readonly string[],
132+
cwd?: string,
133+
): { command: string; args: readonly string[] } {
134+
if (command !== 'ng' || !cwd) {
135+
return { command, args };
136+
}
137+
138+
try {
139+
const workspaceRequire = createRequire(cwd);
140+
const pkgJsonPath = workspaceRequire.resolve('@angular/cli/package.json');
141+
const pkgJson = workspaceRequire(pkgJsonPath) as { bin?: string | Record<string, string> };
142+
const binPath = typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.['ng'];
143+
144+
if (binPath) {
145+
const ngJsPath = resolve(dirname(pkgJsonPath), binPath);
146+
147+
return {
148+
command: process.execPath,
149+
args: [ngJsPath, ...args],
150+
};
151+
}
152+
} catch {
153+
// Failed to resolve the CLI binary, fall back to assuming `ng` is on PATH.
154+
}
155+
156+
return { command, args };
157+
}
158+
128159
/**
129160
* A concrete implementation of the `Host` interface that runs on a local workspace.
130161
*/
@@ -156,10 +187,11 @@ export const LocalWorkspaceHost: Host = {
156187
env?: Record<string, string>;
157188
} = {},
158189
): Promise<{ logs: string[] }> => {
190+
const resolved = resolveCommand(command, args, options.cwd);
159191
const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined;
160192

161193
return new Promise((resolve, reject) => {
162-
const childProcess = spawn(command, args, {
194+
const childProcess = spawn(resolved.command, resolved.args, {
163195
shell: false,
164196
stdio: options.stdio ?? 'pipe',
165197
signal,
@@ -205,7 +237,9 @@ export const LocalWorkspaceHost: Host = {
205237
env?: Record<string, string>;
206238
} = {},
207239
): ChildProcess {
208-
return spawn(command, args, {
240+
const resolved = resolveCommand(command, args, options.cwd);
241+
242+
return spawn(resolved.command, resolved.args, {
209243
shell: false,
210244
stdio: options.stdio ?? 'pipe',
211245
cwd: options.cwd,

0 commit comments

Comments
 (0)