Skip to content

Commit e9e37a9

Browse files
committed
refactor(@angular/cli): provide a find examples MCP server tool
The built-in stdio MCP server (`ng mcp`) for the Angular CLI now includes a tool that can find Angular code usage examples. The code examples are indexed and stored locally within the Angular CLI install. This removes the need for network requests to find the examples. It also ensures that the examples are relevant for the version of Angular currently being used. This tool requires Node.js 22.16 or higher. Lower versions will not have this specific tool available for use.
1 parent 16ae6a1 commit e9e37a9

File tree

10 files changed

+361
-141
lines changed

10 files changed

+361
-141
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"@types/less": "^3.0.3",
8383
"@types/loader-utils": "^2.0.0",
8484
"@types/lodash": "^4.17.0",
85-
"@types/node": "^20.19.7",
85+
"@types/node": "^22.12.0",
8686
"@types/npm-package-arg": "^6.1.0",
8787
"@types/pacote": "^11.1.3",
8888
"@types/picomatch": "^4.0.0",

packages/angular/cli/BUILD.bazel

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
load("@npm//:defs.bzl", "npm_link_all_packages")
77
load("//tools:defaults.bzl", "jasmine_test", "npm_package", "ts_project")
8+
load("//tools:example_db_generator.bzl", "cli_example_db")
89
load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema")
910
load("//tools:ts_json_schema.bzl", "ts_json_schema")
1011

@@ -25,6 +26,7 @@ RUNTIME_ASSETS = glob(
2526
],
2627
) + [
2728
"//packages/angular/cli:lib/config/schema.json",
29+
"//packages/angular/cli:lib/code-examples.db",
2830
]
2931

3032
ts_project(
@@ -74,6 +76,17 @@ ts_project(
7476
],
7577
)
7678

79+
cli_example_db(
80+
name = "cli_example_database",
81+
srcs = glob(
82+
include = [
83+
"lib/examples/**/*.md",
84+
],
85+
),
86+
out = "lib/code-examples.db",
87+
path = "packages/angular/cli/lib/examples",
88+
)
89+
7790
CLI_SCHEMA_DATA = [
7891
"//packages/angular/build:schemas",
7992
"//packages/angular_devkit/build_angular:schemas",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Angular @if Control Flow Example
2+
3+
This example demonstrates how to use the `@if` control flow block in an Angular template. The visibility of a `<div>` element is controlled by a boolean field in the component's TypeScript code.
4+
5+
## Angular Template
6+
7+
```html
8+
<!-- The @if directive will only render this div if the 'isVisible' field in the component is true. -->
9+
@if (isVisible) {
10+
<div>This content is conditionally displayed.</div>
11+
}
12+
```
13+
14+
## Component TypeScript
15+
16+
```typescript
17+
import { Component } from '@angular/core';
18+
19+
@Component({
20+
selector: 'app-example',
21+
templateUrl: './example.component.html',
22+
styleUrls: ['./example.component.css'],
23+
})
24+
export class ExampleComponent {
25+
// This boolean field controls the visibility of the element in the template.
26+
isVisible: boolean = true;
27+
}
28+
```

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ export default class McpCommandModule extends CommandModule implements CommandMo
4343
return;
4444
}
4545

46-
const server = await createMcpServer({ workspace: this.context.workspace });
46+
const server = await createMcpServer(
47+
{ workspace: this.context.workspace },
48+
this.context.logger,
49+
);
4750
const transport = new StdioServerTransport();
4851
await server.connect(transport);
4952
}

packages/angular/cli/src/commands/mcp/mcp-server.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ import { z } from 'zod';
1313
import type { AngularWorkspace } from '../../utilities/config';
1414
import { VERSION } from '../../utilities/version';
1515
import { registerDocSearchTool } from './tools/doc-search';
16+
import { registerFindExampleTool } from './tools/examples';
1617

17-
export async function createMcpServer(context: {
18-
workspace?: AngularWorkspace;
19-
}): Promise<McpServer> {
18+
export async function createMcpServer(
19+
context: {
20+
workspace?: AngularWorkspace;
21+
},
22+
logger: { warn(text: string): void },
23+
): Promise<McpServer> {
2024
const server = new McpServer({
2125
name: 'angular-cli-server',
2226
version: VERSION.full,
@@ -132,5 +136,16 @@ export async function createMcpServer(context: {
132136

133137
await registerDocSearchTool(server);
134138

139+
// sqlite database support requires Node.js 22.16+
140+
const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number);
141+
if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) {
142+
logger.warn(
143+
`MCP tool 'find_examples' requires Node.js 22.16 (or higher). ` +
144+
' Registration of this tool has been skipped.',
145+
);
146+
} else {
147+
await registerFindExampleTool(server, path.join(__dirname, '../../../lib/code-examples.db'));
148+
}
149+
135150
return server;
136151
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10+
import { z } from 'zod';
11+
12+
/**
13+
* Registers the `find_examples` tool with the MCP server.
14+
*
15+
* This tool allows users to search for best-practice Angular code examples
16+
* from a local SQLite database.
17+
*
18+
* @param server The MCP server instance.
19+
* @param exampleDatabasePath The path to the SQLite database file containing the examples.
20+
*/
21+
export async function registerFindExampleTool(
22+
server: McpServer,
23+
exampleDatabasePath: string,
24+
): Promise<void> {
25+
let db: import('node:sqlite').DatabaseSync | undefined;
26+
let queryStatement: import('node:sqlite').StatementSync | undefined;
27+
28+
server.registerTool(
29+
'find_examples',
30+
{
31+
title: 'Find Angular Code Examples',
32+
description:
33+
'Searches for and returns best-practice Angular code examples based on a query from a local set of packaged examples.' +
34+
' This should be used before creating any Angular code to ensure that best practices are followed.' +
35+
' Results are ranked in order of relevance with most relevant being first.',
36+
inputSchema: {
37+
query: z.string().describe('The search query to find Angular code examples.'),
38+
},
39+
},
40+
async ({ query }) => {
41+
if (!db || !queryStatement) {
42+
suppressSqliteWarning();
43+
44+
const { DatabaseSync } = await import('node:sqlite');
45+
db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
46+
queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
47+
}
48+
49+
// Query database and return results as text content
50+
const content = [];
51+
for (const exampleRecord of queryStatement.all(query)) {
52+
content.push({ type: 'text' as const, text: exampleRecord['content'] as string });
53+
}
54+
55+
return {
56+
content,
57+
};
58+
},
59+
);
60+
}
61+
62+
function suppressSqliteWarning() {
63+
const originalProcessEmit = process.emit;
64+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
65+
process.emit = function (event: string, error?: unknown): any {
66+
if (
67+
event === 'warning' &&
68+
error instanceof Error &&
69+
error.name === 'ExperimentalWarning' &&
70+
error.message.includes('SQLite')
71+
) {
72+
return false;
73+
}
74+
75+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, prefer-rest-params
76+
return originalProcessEmit.apply(process, arguments as any);
77+
};
78+
}

0 commit comments

Comments
 (0)