diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index e06af1316a27..41201feb3c1f 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -49,6 +49,7 @@ ts_project( ":node_modules/@angular-devkit/schematics", ":node_modules/@inquirer/prompts", ":node_modules/@listr2/prompt-adapter-inquirer", + ":node_modules/@modelcontextprotocol/sdk", ":node_modules/@yarnpkg/lockfile", ":node_modules/ini", ":node_modules/jsonc-parser", diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 3617470d27e3..33a7a338b428 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -27,6 +27,7 @@ "@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER", "@inquirer/prompts": "7.5.3", "@listr2/prompt-adapter-inquirer": "2.0.22", + "@modelcontextprotocol/sdk": "1.13.1", "@schematics/angular": "workspace:0.0.0-PLACEHOLDER", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", diff --git a/packages/angular/cli/src/commands/command-config.ts b/packages/angular/cli/src/commands/command-config.ts index cd048cbb2240..a74d81f5e911 100644 --- a/packages/angular/cli/src/commands/command-config.ts +++ b/packages/angular/cli/src/commands/command-config.ts @@ -21,6 +21,7 @@ export type CommandNames = | 'generate' | 'lint' | 'make-this-awesome' + | 'mcp' | 'new' | 'run' | 'serve' @@ -77,6 +78,9 @@ export const RootCommands: Record< 'make-this-awesome': { factory: () => import('./make-this-awesome/cli'), }, + 'mcp': { + factory: () => import('./mcp/cli'), + }, 'new': { factory: () => import('./new/cli'), aliases: ['n'], diff --git a/packages/angular/cli/src/commands/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts new file mode 100644 index 000000000000..81260f09f6b6 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -0,0 +1,50 @@ +/** + * @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 { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import { isTTY } from '../../utilities/tty'; +import { createMcpServer } from './mcp-server'; + +const INTERACTIVE_MESSAGE = ` +To start using the Angular CLI MCP Server, add this configuration to your host: + +{ + "mcpServers": { + "angular-cli": { + "command": "npx", + "args": ["@angular/cli", "mcp"] + } + } +} + +Exact configuration may differ depending on the host. +`; + +export default class McpCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'mcp'; + describe = false as const; + longDescriptionPath = undefined; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + async run(): Promise { + if (isTTY()) { + this.context.logger.info(INTERACTIVE_MESSAGE); + + return; + } + + const server = await createMcpServer({ workspace: this.context.workspace }); + const transport = new StdioServerTransport(); + await server.connect(transport); + } +} diff --git a/packages/angular/cli/src/commands/mcp/instructions/best-practices.md b/packages/angular/cli/src/commands/mcp/instructions/best-practices.md new file mode 100644 index 000000000000..e50d16473640 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/instructions/best-practices.md @@ -0,0 +1,44 @@ +You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices. + +## TypeScript Best Practices + +- Use strict type checking +- Prefer type inference when the type is obvious +- Avoid the `any` type; use `unknown` when type is uncertain + +## Angular Best Practices + +- Always use standalone components over NgModules +- Don't use explicit `standalone: true` (it is implied by default) +- Use signals for state management +- Implement lazy loading for feature routes +- Use `NgOptimizedImage` for all static images. + +## Components + +- Keep components small and focused on a single responsibility +- Use `input()` and `output()` functions instead of decorators +- Use `computed()` for derived state +- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator +- Prefer inline templates for small components +- Prefer Reactive forms instead of Template-driven ones +- Do NOT use `ngClass`, use `class` bindings instead +- DO NOT use `ngStyle`, use `style` bindings instead + +## State Management + +- Use signals for local component state +- Use `computed()` for derived state +- Keep state transformations pure and predictable + +## Templates + +- Keep templates simple and avoid complex logic +- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` +- Use the async pipe to handle observables + +## Services + +- Design services around a single responsibility +- Use the `providedIn: 'root'` option for singleton services +- Use the `inject()` function instead of constructor injection diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts new file mode 100644 index 000000000000..33df33f71ba9 --- /dev/null +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -0,0 +1,80 @@ +/** + * @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/sdk/server/mcp.js'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import type { AngularWorkspace } from '../../utilities/config'; +import { VERSION } from '../../utilities/version'; + +export async function createMcpServer(context: { + workspace?: AngularWorkspace; +}): Promise { + const server = new McpServer({ + name: 'angular-cli-server', + version: VERSION.full, + capabilities: { + resources: {}, + tools: {}, + }, + }); + + server.registerResource( + 'instructions', + 'instructions://best-practices', + { + title: 'Angular System Instructions', + description: + 'A set of instructions to help LLMs generate correct code that follows Angular best practices.', + mimeType: 'text/markdown', + }, + async () => { + const text = await readFile( + path.join(__dirname, 'instructions', 'best-practices.md'), + 'utf-8', + ); + + return { contents: [{ uri: 'instructions://best-practices', text }] }; + }, + ); + + server.registerTool( + 'list_projects', + { + title: 'List projects', + description: + 'List projects within an Angular workspace.' + + ' This information is read from the `angular.json` file at the root path of the Angular workspace', + }, + () => { + if (!context.workspace) { + return { + content: [ + { + type: 'text', + text: 'Not within an Angular project.', + }, + ], + }; + } + + return { + content: [ + { + type: 'text', + text: + 'Projects in the Angular workspace: ' + + [...context.workspace.projects.keys()].join(','), + }, + ], + }; + }, + ); + + return server; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07933e35a613..23cdd22001c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ importers: version: 20.1.0-next.1(l32yggn2tzdh3a577e6frrsqxm) '@angular/ng-dev': specifier: https://github.com/angular/dev-infra-private-ng-dev-builds.git#af92dd0055dbfb437c73fd2ab963149a4341d018 - version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/af92dd0055dbfb437c73fd2ab963149a4341d018(encoding@0.1.13) + version: https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/af92dd0055dbfb437c73fd2ab963149a4341d018(@modelcontextprotocol/sdk@1.13.1)(encoding@0.1.13) '@angular/platform-browser': specifier: 20.1.0-next.2 version: 20.1.0-next.2(@angular/animations@20.1.0-next.2(@angular/common@20.1.0-next.2(@angular/core@20.1.0-next.2(@angular/compiler@20.1.0-next.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.2(@angular/compiler@20.1.0-next.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.0-next.2(@angular/core@20.1.0-next.2(@angular/compiler@20.1.0-next.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.0-next.2(@angular/compiler@20.1.0-next.2)(rxjs@7.8.2)(zone.js@0.15.1)) @@ -471,6 +471,9 @@ importers: '@listr2/prompt-adapter-inquirer': specifier: 2.0.22 version: 2.0.22(@inquirer/prompts@7.5.3(@types/node@20.19.1)) + '@modelcontextprotocol/sdk': + specifier: 1.13.1 + version: 1.13.1 '@schematics/angular': specifier: workspace:0.0.0-PLACEHOLDER version: link:../../schematics/angular @@ -2075,6 +2078,10 @@ packages: '@mdn/browser-compat-data@6.0.25': resolution: {integrity: sha512-S2XYLTeiJIKHa1nTghEKPzGktxc1fGMfIbNb/A6BwXUwl97FrLJyLYkLfvWHZv2kLCiMZekC/5hpeoLM/qvreA==} + '@modelcontextprotocol/sdk@1.13.1': + resolution: {integrity: sha512-8q6+9aF0yA39/qWT/uaIj6zTpC+Qu07DnN/lb9mjoquCJsAh6l3HyYqc9O3t2j7GilseOQOQimLg7W3By6jqvg==} + engines: {node: '>=18'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -4572,6 +4579,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.2: + resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -4590,6 +4605,12 @@ packages: express-rate-limit@5.5.1: resolution: {integrity: sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==} + express-rate-limit@7.5.0: + resolution: {integrity: sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==} + engines: {node: '>= 16'} + peerDependencies: + express: ^4.11 || 5 || ^5.0.0-beta.1 + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -6598,6 +6619,10 @@ packages: resolution: {integrity: sha512-9rPDIPsCwOivatEZGM8+apgM7AiTDLSnpwMmLaSmdm2PeND8bFJzZLZZxyrJjLH8Xx/MpKoVaKf+vZOWALNHbw==} engines: {node: '>=20.x'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pkg-dir@8.0.0: resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==} engines: {node: '>=18'} @@ -6772,7 +6797,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qjobs@1.2.0: @@ -8403,10 +8427,10 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/af92dd0055dbfb437c73fd2ab963149a4341d018(encoding@0.1.13)': + '@angular/ng-dev@https://codeload.github.com/angular/dev-infra-private-ng-dev-builds/tar.gz/af92dd0055dbfb437c73fd2ab963149a4341d018(@modelcontextprotocol/sdk@1.13.1)(encoding@0.1.13)': dependencies: '@google-cloud/spanner': 8.0.0(supports-color@10.0.0) - '@google/genai': 1.6.0(encoding@0.1.13)(supports-color@10.0.0) + '@google/genai': 1.6.0(@modelcontextprotocol/sdk@1.13.1)(encoding@0.1.13)(supports-color@10.0.0) '@octokit/rest': 22.0.0 '@types/semver': 7.7.0 '@types/supports-color': 10.0.0 @@ -9358,12 +9382,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@google/genai@1.6.0(encoding@0.1.13)(supports-color@10.0.0)': + '@google/genai@1.6.0(@modelcontextprotocol/sdk@1.13.1)(encoding@0.1.13)(supports-color@10.0.0)': dependencies: google-auth-library: 9.15.1(encoding@0.1.13)(supports-color@10.0.0) ws: 8.18.2 zod: 3.25.67 zod-to-json-schema: 3.24.5(zod@3.25.67) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.13.1 transitivePeerDependencies: - bufferutil - encoding @@ -9607,6 +9633,22 @@ snapshots: '@mdn/browser-compat-data@6.0.25': {} + '@modelcontextprotocol/sdk@1.13.1': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + express: 5.1.0 + express-rate-limit: 7.5.0(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.67 + zod-to-json-schema: 3.24.5(zod@3.25.67) + transitivePeerDependencies: + - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -12519,6 +12561,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.2: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.2 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -12539,6 +12587,10 @@ snapshots: express-rate-limit@5.5.1: {} + express-rate-limit@7.5.0(express@5.1.0): + dependencies: + express: 5.1.0 + express@4.21.2: dependencies: accepts: 1.3.8 @@ -14761,6 +14813,8 @@ snapshots: optionalDependencies: '@napi-rs/nice': 1.0.1 + pkce-challenge@5.0.0: {} + pkg-dir@8.0.0: dependencies: find-up-simple: 1.0.1