/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/cli/src/commands/mcp/mcp-server.ts
223 строки
8 KB
Charles Lyding
feat(@angular/cli): add `--root` command line option to `mcp` command
04 авг 2026, 19:37
04 авг 2026, 19:37
41555df
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import { McpServer } from '@modelcontextprotocol/server'; import { join, normalize, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; import type { Devserver } from './devserver'; import { LocalWorkspaceHost, createRootRestrictedHost } from './host'; import { registerInstructionsResource } from './resources/instructions'; import { AI_TUTOR_TOOL } from './tools/ai-tutor'; import { BEST_PRACTICES_TOOL } from './tools/best-practices'; import { DEVSERVER_START_TOOL } from './tools/devserver/devserver-start'; import { DEVSERVER_STOP_TOOL } from './tools/devserver/devserver-stop'; import { DEVSERVER_WAIT_FOR_BUILD_TOOL } from './tools/devserver/devserver-wait-for-build'; import { DOC_SEARCH_TOOL } from './tools/doc-search'; import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration'; import { LIST_PROJECTS_TOOL } from './tools/projects'; import { RUN_TARGET_TOOL } from './tools/run-target/run-target'; import { type AnyMcpToolDeclaration, registerTools } from './tools/tool-registry'; /** * Tools to manage devservers. Should be bundled together, then added to experimental or stable as a group. */ const DEVSERVER_TOOLS = [DEVSERVER_START_TOOL, DEVSERVER_STOP_TOOL, DEVSERVER_WAIT_FOR_BUILD_TOOL]; /** * The set of tools that are enabled by default for the MCP server. * These tools are considered stable and suitable for general use. */ const STABLE_TOOLS = [ AI_TUTOR_TOOL, BEST_PRACTICES_TOOL, DOC_SEARCH_TOOL, LIST_PROJECTS_TOOL, ZONELESS_MIGRATION_TOOL, RUN_TARGET_TOOL, ...DEVSERVER_TOOLS, ] as const; /** * The set of tools that are available but not enabled by default. * These tools are considered experimental and may have limitations. */ export const EXPERIMENTAL_TOOLS: readonly AnyMcpToolDeclaration[] = [] as const; /** * Experimental tools that are grouped together under a single name. * * Used for enabling them as a group. */ export const EXPERIMENTAL_TOOL_GROUPS: Record<string, readonly AnyMcpToolDeclaration[]> = { 'all': EXPERIMENTAL_TOOLS, 'devserver': [], }; export async function createMcpServer( options: { workspace?: AngularWorkspace; readOnly?: boolean; localOnly?: boolean; experimentalTools?: string[]; roots?: string[]; }, logger: { warn(text: string): void }, ): Promise<McpServer> { const server = new McpServer( { name: 'angular-cli-server', version: VERSION.full, }, { capabilities: { resources: {}, tools: {}, logging: {}, }, instructions: ` <General Purpose> This server provides a safe, programmatic interface to the Angular CLI. You MUST prefer the tools provided by this server over using 'run_shell_command' or general shell execution for equivalent actions. </General Purpose> <Core Workflows & Tool Guide> * **1. Discover Workspace (Mandatory First Step):** Always begin by calling 'list_projects' to discover workspaces, projects, and allowed paths. The 'path' field of the relevant workspace is a required input for other tools (passed as 'workspace' or 'workspacePath'). * **2. Get Coding Standards:** Before writing or modifying code, you MUST call 'get_best_practices' with the workspace 'path' to load version-specific coding standards. * **3. Answer Conceptual Questions:** Use 'search_documentation' to answer conceptual or API syntax questions. * **4. Discover Schematics:** To discover available package migrations, use a shell command (if available) with 'ng generate <package-name>: --help' (e.g., 'ng generate @angular/core: --help'). </Core Workflows & Tool Guide> <Key Concepts> * **Workspace vs. Project:** A 'workspace' contains an 'angular.json' file and defines 'projects' (applications or libraries). A monorepo can contain multiple workspaces. * **Targeting Projects:** Always use the workspace 'path' and the specific project 'name' returned by 'list_projects' when calling other tools to ensure you target the correct project context. </Key Concepts> `, }, ); registerInstructionsResource(server); const toolDeclarations = assembleToolDeclarations(STABLE_TOOLS, EXPERIMENTAL_TOOLS, { ...options, logger, }); const resolvedRoots = options.roots?.map((r) => resolve(r)); const restrictedHost = createRootRestrictedHost( LocalWorkspaceHost, resolvedRoots?.length ? resolvedRoots : [process.cwd()], ); server.server.oninitialized = () => { void (async () => { try { const clientCapabilities = server.server.getClientCapabilities(); if (clientCapabilities?.roots) { const { roots } = await server.server.listRoots(); const searchRoots = roots?.map((r) => normalize(fileURLToPath(r.uri))) ?? []; restrictedHost.setRoots(searchRoots); if (clientCapabilities.roots.listChanged) { server.server.setNotificationHandler('notifications/roots/list_changed', async () => { try { const { roots: updatedRoots } = await server.server.listRoots(); const updatedSearchRoots = updatedRoots?.map((r) => normalize(fileURLToPath(r.uri))) ?? []; restrictedHost.setRoots(updatedSearchRoots); } catch (e) { logger.warn( `Failed to update roots on notification: ${e instanceof Error ? e.message : e}`, ); } }); } } } catch (e) { logger.warn( `Failed to initialize roots on connection: ${e instanceof Error ? e.message : e}`, ); } })(); }; await registerTools( server, { workspace: options.workspace, logger, exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'), devservers: new Map<string, Devserver>(), host: restrictedHost, roots: resolvedRoots, }, toolDeclarations, ); return server; } export function assembleToolDeclarations( stableDeclarations: readonly AnyMcpToolDeclaration[], experimentalDeclarations: readonly AnyMcpToolDeclaration[], options: { readOnly?: boolean; localOnly?: boolean; experimentalTools?: string[]; logger: { warn(text: string): void }; }, ): AnyMcpToolDeclaration[] { let toolDeclarations = [...stableDeclarations]; if (options.readOnly) { toolDeclarations = toolDeclarations.filter((tool) => tool.isReadOnly); } if (options.localOnly) { toolDeclarations = toolDeclarations.filter((tool) => tool.isLocalOnly); } const enabledExperimentalTools = new Set(options.experimentalTools); for (const [toolGroupName, toolGroup] of Object.entries(EXPERIMENTAL_TOOL_GROUPS)) { if (enabledExperimentalTools.delete(toolGroupName)) { for (const tool of toolGroup) { enabledExperimentalTools.add(tool.name); } } } if (enabledExperimentalTools.size > 0) { const experimentalToolsMap = new Map(experimentalDeclarations.map((tool) => [tool.name, tool])); for (const toolName of enabledExperimentalTools) { const tool = experimentalToolsMap.get(toolName); if (tool) { toolDeclarations.push(tool); } else if (!stableDeclarations.some((t) => t.name === toolName)) { options.logger.warn(`Unknown experimental tool: ${toolName}`); } } } return toolDeclarations; }