Skip to content

Commit 2822522

Browse files
committed
fixup! feat(@angular/cli): standardize MCP tools around workspace/project options
1 parent b98a089 commit 2822522

File tree

13 files changed

+458
-494
lines changed

13 files changed

+458
-494
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,20 @@ export class LocalDevserver implements Devserver {
171171
export function getDevserverKey(workspacePath: string, projectName: string): string {
172172
return `${workspacePath}:${projectName}`;
173173
}
174+
175+
export function createDevServerNotFoundError(
176+
devservers: Map<string, { project: string; workspacePath: string }>,
177+
): Error {
178+
if (devservers.size === 0) {
179+
return new Error('No development servers are currently running.');
180+
}
181+
182+
const runningServers = Array.from(devservers.values())
183+
.map((server) => `- Project '${server.project}' in workspace path '${server.workspacePath}'`)
184+
.join('\n');
185+
186+
return new Error(
187+
`Dev server not found. Currently running servers:\n${runningServers}\n` +
188+
'Please provide the correct workspace and project arguments.',
189+
);
190+
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88

99
import { z } from 'zod';
1010
import { workspaceAndProjectOptions } from '../shared-options';
11-
import {
12-
createStructuredContentOutput,
13-
getCommandErrorLogs,
14-
resolveWorkspaceAndProject,
15-
} from '../utils';
11+
import { createStructuredContentOutput, getCommandErrorLogs } from '../utils';
12+
import { resolveWorkspaceAndProject } from '../workspace-utils';
1613
import { type McpToolContext, type McpToolDeclaration, declareTool } from './tool-registry';
1714

1815
const DEFAULT_CONFIGURATION = 'development';

packages/angular/cli/src/commands/mcp/tools/devserver/devserver-start.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
import { z } from 'zod';
1010
import { LocalDevserver, getDevserverKey } from '../../devserver';
1111
import { workspaceAndProjectOptions } from '../../shared-options';
12-
import { createStructuredContentOutput, resolveWorkspaceAndProject } from '../../utils';
12+
import { createStructuredContentOutput } from '../../utils';
13+
import { resolveWorkspaceAndProject } from '../../workspace-utils';
1314
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';
1415

1516
const devserverStartToolInputSchema = z.object({

packages/angular/cli/src/commands/mcp/tools/devserver/devserver-stop.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77
*/
88

99
import { z } from 'zod';
10-
import { getDevserverKey } from '../../devserver';
10+
import { createDevServerNotFoundError, getDevserverKey } from '../../devserver';
1111
import { workspaceAndProjectOptions } from '../../shared-options';
12-
import {
13-
createDevServerNotFoundError,
14-
createStructuredContentOutput,
15-
resolveWorkspaceAndProject,
16-
} from '../../utils';
12+
import { createStructuredContentOutput } from '../../utils';
13+
import { resolveWorkspaceAndProject } from '../../workspace-utils';
1714
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';
1815

1916
const devserverStopToolInputSchema = z.object({

packages/angular/cli/src/commands/mcp/tools/devserver/devserver-wait-for-build.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77
*/
88

99
import { z } from 'zod';
10-
import { Devserver, getDevserverKey } from '../../devserver';
10+
import { Devserver, createDevServerNotFoundError, getDevserverKey } from '../../devserver';
1111
import { workspaceAndProjectOptions } from '../../shared-options';
12-
import {
13-
createDevServerNotFoundError,
14-
createStructuredContentOutput,
15-
resolveWorkspaceAndProject,
16-
} from '../../utils';
12+
import { createStructuredContentOutput } from '../../utils';
13+
import { resolveWorkspaceAndProject } from '../../workspace-utils';
1714
import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry';
1815

1916
/**

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@
99
import { z } from 'zod';
1010
import { type Host } from '../host';
1111
import { workspaceAndProjectOptions } from '../shared-options';
12-
import {
13-
createStructuredContentOutput,
14-
getCommandErrorLogs,
15-
resolveWorkspaceAndProject,
16-
} from '../utils';
12+
import { createStructuredContentOutput, getCommandErrorLogs } from '../utils';
13+
import { resolveWorkspaceAndProject } from '../workspace-utils';
1714
import { type McpToolContext, type McpToolDeclaration, declareTool } from './tool-registry';
1815

1916
const e2eStatusSchema = z.enum(['success', 'failure']);

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88

99
import { z } from 'zod';
1010
import { workspaceAndProjectOptions } from '../shared-options';
11-
import {
12-
createStructuredContentOutput,
13-
getCommandErrorLogs,
14-
resolveWorkspaceAndProject,
15-
} from '../utils';
11+
import { createStructuredContentOutput, getCommandErrorLogs } from '../utils';
12+
import { resolveWorkspaceAndProject } from '../workspace-utils';
1613
import { type McpToolContext, type McpToolDeclaration, declareTool } from './tool-registry';
1714

1815
interface Transformation {

packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-test-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from 'node:fs';
1010
import { glob } from 'node:fs/promises';
1111
import { dirname, join } from 'node:path';
1212
import type { SourceFile } from 'typescript';
13-
import { findAngularJsonDir } from '../../utils';
13+
import { findAngularJsonDir } from '../../workspace-utils';
1414
import { createFixResponseForZoneTests, createProvideZonelessForTestsSetupPrompt } from './prompts';
1515
import { loadTypescript } from './ts-utils';
1616
import { MigrationResponse } from './types';

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88

99
import { z } from 'zod';
1010
import { workspaceAndProjectOptions } from '../shared-options';
11-
import {
12-
createStructuredContentOutput,
13-
getCommandErrorLogs,
14-
resolveWorkspaceAndProject,
15-
} from '../utils';
11+
import { createStructuredContentOutput, getCommandErrorLogs } from '../utils';
12+
import { resolveWorkspaceAndProject } from '../workspace-utils';
1613
import { type McpToolContext, type McpToolDeclaration, declareTool } from './tool-registry';
1714

1815
const testStatusSchema = z.enum(['success', 'failure']);

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

Lines changed: 1 addition & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@
1111
* Utility functions shared across MCP tools.
1212
*/
1313

14-
import { workspaces } from '@angular-devkit/core';
15-
import { dirname, join } from 'node:path';
16-
import { AngularWorkspace } from '../../utilities/config';
17-
import { CommandError, type Host, LocalWorkspaceHost } from './host';
18-
import { McpToolContext } from './tools/tool-registry';
14+
import { CommandError } from './host';
1915

2016
/**
2117
* Returns simple structured content output from an MCP tool.
@@ -29,74 +25,6 @@ export function createStructuredContentOutput<OutputType>(structuredContent: Out
2925
};
3026
}
3127

32-
/**
33-
* Searches for an angular.json file by traversing up the directory tree from a starting directory.
34-
*
35-
* @param startDir The directory path to start searching from
36-
* @param host The workspace host instance used to check file existence. Defaults to LocalWorkspaceHost
37-
* @returns The absolute path to the directory containing angular.json, or null if not found
38-
*
39-
* @remarks
40-
* This function performs an upward directory traversal starting from `startDir`.
41-
* It checks each directory for the presence of an angular.json file until either:
42-
* - The file is found (returns the directory path)
43-
* - The root of the filesystem is reached (returns null)
44-
*/
45-
export function findAngularJsonDir(startDir: string, host = LocalWorkspaceHost): string | null {
46-
let currentDir = startDir;
47-
while (true) {
48-
if (host.existsSync(join(currentDir, 'angular.json'))) {
49-
return currentDir;
50-
}
51-
const parentDir = dirname(currentDir);
52-
if (parentDir === currentDir) {
53-
return null;
54-
}
55-
currentDir = parentDir;
56-
}
57-
}
58-
59-
/**
60-
* Searches for a project in the current workspace, by name.
61-
*/
62-
export function getProject(
63-
context: McpToolContext,
64-
name: string,
65-
): workspaces.ProjectDefinition | undefined {
66-
const projects = context.workspace?.projects;
67-
if (!projects) {
68-
return undefined;
69-
}
70-
71-
return projects.get(name);
72-
}
73-
74-
/**
75-
* Returns the name of the default project in the current workspace, or undefined if none exists.
76-
*
77-
* If no default project is defined but there's only a single project in the workspace, its name will
78-
* be returned.
79-
*/
80-
export function getDefaultProjectName(workspace: AngularWorkspace | undefined): string | undefined {
81-
const projects = workspace?.projects;
82-
83-
if (!projects) {
84-
return undefined;
85-
}
86-
87-
const defaultProjectName = workspace?.extensions['defaultProject'] as string | undefined;
88-
if (defaultProjectName) {
89-
return defaultProjectName;
90-
}
91-
92-
// No default project defined? This might still be salvageable if only a single project exists.
93-
if (projects.size === 1) {
94-
return Array.from(projects.keys())[0];
95-
}
96-
97-
return undefined;
98-
}
99-
10028
/**
10129
* Get the logs of a failing command.
10230
*
@@ -111,139 +39,3 @@ export function getCommandErrorLogs(e: unknown): string[] {
11139
return [String(e)];
11240
}
11341
}
114-
115-
export function createWorkspaceNotFoundError(): Error {
116-
return new Error(
117-
'Could not find an Angular workspace (angular.json) in the current directory. ' +
118-
"You can use 'list_projects' to find available workspaces.",
119-
);
120-
}
121-
122-
export function createWorkspacePathDoesNotExistError(path: string): Error {
123-
return new Error(
124-
`Workspace path does not exist: ${path}. ` +
125-
"You can use 'list_projects' to find available workspaces.",
126-
);
127-
}
128-
129-
export function createNoAngularJsonFoundError(path: string): Error {
130-
return new Error(
131-
`No angular.json found at ${path}. ` +
132-
"You can use 'list_projects' to find available workspaces.",
133-
);
134-
}
135-
136-
export function createProjectNotFoundError(projectName: string, workspacePath: string): Error {
137-
return new Error(
138-
`Project '${projectName}' not found in workspace path ${workspacePath}. ` +
139-
"You can use 'list_projects' to find available projects.",
140-
);
141-
}
142-
143-
export function createNoProjectResolvedError(workspacePath: string): Error {
144-
return new Error(
145-
`No project name provided and no default project found in workspace path ${workspacePath}. ` +
146-
'Please provide a project name or set a default project in angular.json. ' +
147-
"You can use 'list_projects' to find available projects.",
148-
);
149-
}
150-
151-
export function createDevServerNotFoundError(
152-
devservers: Map<string, { project: string; workspacePath: string }>,
153-
): Error {
154-
if (devservers.size === 0) {
155-
return new Error('No development servers are currently running.');
156-
}
157-
158-
const runningServers = Array.from(devservers.values())
159-
.map((server) => `- Project '${server.project}' in workspace path '${server.workspacePath}'`)
160-
.join('\n');
161-
162-
return new Error(
163-
`Dev server not found. Currently running servers:\n${runningServers}\n` +
164-
'Please provide the correct workspace and project arguments.',
165-
);
166-
}
167-
168-
/**
169-
* Resolves workspace and project for tools to operate on.
170-
*
171-
* If `workspacePathInput` is absent, uses the MCP's configured workspace. If none is configured, use the
172-
* current directory as the workspace.
173-
* If `projectNameInput` is absent, uses the default project in the workspace.
174-
*/
175-
export async function resolveWorkspaceAndProject({
176-
host,
177-
workspacePathInput,
178-
projectNameInput,
179-
mcpWorkspace,
180-
workspaceLoader = AngularWorkspace.load,
181-
}: {
182-
host: Host;
183-
workspacePathInput?: string;
184-
projectNameInput?: string;
185-
mcpWorkspace?: AngularWorkspace;
186-
workspaceLoader?: (path: string) => Promise<AngularWorkspace>;
187-
}): Promise<{
188-
workspace: AngularWorkspace;
189-
workspacePath: string;
190-
projectName: string;
191-
}> {
192-
let workspacePath: string;
193-
let workspace: AngularWorkspace;
194-
195-
if (workspacePathInput) {
196-
if (!host.existsSync(workspacePathInput)) {
197-
throw createWorkspacePathDoesNotExistError(workspacePathInput);
198-
}
199-
if (!host.existsSync(join(workspacePathInput, 'angular.json'))) {
200-
throw createNoAngularJsonFoundError(workspacePathInput);
201-
}
202-
workspacePath = workspacePathInput;
203-
const configPath = join(workspacePath, 'angular.json');
204-
try {
205-
workspace = await workspaceLoader(configPath);
206-
} catch (e) {
207-
throw new Error(
208-
`Failed to load workspace configuration at ${configPath}: ${
209-
e instanceof Error ? e.message : e
210-
}`,
211-
);
212-
}
213-
} else if (mcpWorkspace) {
214-
workspace = mcpWorkspace;
215-
workspacePath = workspace.basePath;
216-
} else {
217-
const found = findAngularJsonDir(process.cwd(), host);
218-
219-
if (!found) {
220-
throw createWorkspaceNotFoundError();
221-
}
222-
workspacePath = found;
223-
const configPath = join(workspacePath, 'angular.json');
224-
try {
225-
workspace = await workspaceLoader(configPath);
226-
} catch (e) {
227-
throw new Error(
228-
`Failed to load workspace configuration at ${configPath}: ${
229-
e instanceof Error ? e.message : e
230-
}`,
231-
);
232-
}
233-
}
234-
235-
let projectName = projectNameInput;
236-
if (projectName) {
237-
if (!workspace.projects.has(projectName)) {
238-
throw createProjectNotFoundError(projectName, workspacePath);
239-
}
240-
} else {
241-
projectName = getDefaultProjectName(workspace);
242-
}
243-
244-
if (!projectName) {
245-
throw createNoProjectResolvedError(workspacePath);
246-
}
247-
248-
return { workspace, workspacePath, projectName };
249-
}

0 commit comments

Comments
 (0)