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
31 changes: 29 additions & 2 deletions packages/angular/cli/src/commands/mcp/tools/best-practices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createRequire } from 'node:module';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import { z } from 'zod';
import { VERSION } from '../../../utilities/version';
import { isAllowedWorkspacePath } from '../workspace-utils';
import { type McpToolContext, declareTool } from './tool-registry';

const bestPracticesInputSchema = z.object({
Expand Down Expand Up @@ -86,13 +87,35 @@ async function getBundledBestPractices(): Promise<string> {
*
* @param workspacePath The absolute path to the user's `angular.json` file.
* @param logger The MCP tool context logger for reporting warnings.
* @param server The MCP server context, used to enforce the client's declared roots.
* @returns A promise that resolves to an object containing the guide's content and source,
* or `undefined` if the guide could not be resolved.
*/
async function getVersionSpecificBestPractices(
workspacePath: string,
logger: McpToolContext['logger'],
server: McpToolContext['server'],
): Promise<{ content: string; source: string } | undefined> {
if (server) {
try {
if (!(await isAllowedWorkspacePath(server, workspacePath))) {
logger.warn(
`Workspace path is outside the allowed MCP roots: ${workspacePath}. ` +
'Falling back to the bundled guide.',
);

return undefined;
}
} catch (e) {
logger.warn(
`Failed to verify workspace path '${workspacePath}': ` +
`${e instanceof Error ? e.message : e}. Falling back to the bundled guide.`,
);

return undefined;
}
}

// 1. Resolve the path to package.json
let pkgJsonPath: string;
try {
Expand Down Expand Up @@ -175,7 +198,7 @@ async function getVersionSpecificBestPractices(
* @param context The MCP tool context, containing the logger.
* @returns An async function that serves as the tool's executor.
*/
function createBestPracticesHandler({ logger }: McpToolContext) {
function createBestPracticesHandler({ logger, server }: McpToolContext) {
let bundledBestPractices: Promise<string>;

return async (input: BestPracticesInput) => {
Expand All @@ -184,7 +207,11 @@ function createBestPracticesHandler({ logger }: McpToolContext) {

// First, try to get the version-specific guide.
if (input.workspacePath) {
const versionSpecific = await getVersionSpecificBestPractices(input.workspacePath, logger);
const versionSpecific = await getVersionSpecificBestPractices(
input.workspacePath,
logger,
server,
);
if (versionSpecific) {
content = versionSpecific.content;
source = versionSpecific.source;
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/cli/src/commands/mcp/workspace-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ async function getAllowedWorkspaceRoots(server: McpToolContext['server']): Promi
.filter((root): root is string => root !== null);
}

async function isAllowedWorkspacePath(
export async function isAllowedWorkspacePath(
server: McpToolContext['server'],
workspacePath: string,
): Promise<boolean> {
Expand Down
Loading