Skip to content

Commit f70b1b7

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 5af81b7 commit f70b1b7

File tree

10 files changed

+365
-138
lines changed

10 files changed

+365
-138
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.17.19",
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(
@@ -73,6 +75,17 @@ ts_project(
7375
],
7476
)
7577

78+
cli_example_db(
79+
name = "cli_example_database",
80+
srcs = glob(
81+
include = [
82+
"lib/examples/**/*.md",
83+
],
84+
),
85+
out = "lib/code-examples.db",
86+
path = "packages/angular/cli/lib/examples",
87+
)
88+
7689
CLI_SCHEMA_DATA = [
7790
"//packages/angular/build:schemas",
7891
"//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
@@ -12,10 +12,14 @@ import path from 'node:path';
1212
import { z } from 'zod';
1313
import type { AngularWorkspace } from '../../utilities/config';
1414
import { VERSION } from '../../utilities/version';
15+
import { registerFindExampleTool } from './tools/examples';
1516

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

136+
// sqlite database support requires Node.js 22.16+
137+
const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(Number);
138+
if (nodeMajor < 22 || (nodeMajor === 22 && nodeMinor < 16)) {
139+
logger.warn(
140+
`MCP tool 'find_examples' requires Node.js 22.16 (or higher). ` +
141+
' Registration of this tool has been skipped.',
142+
);
143+
} else {
144+
await registerFindExampleTool(server, path.join(__dirname, '../../../lib/code-examples.db'));
145+
}
146+
132147
return server;
133148
}
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)