Skip to content

Commit d014630

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular/cli): add advanced filtering to MCP example search
This commit enhances the `find_examples` MCP tool by introducing advanced filtering capabilities, allowing for more precise and powerful queries. The tool's input schema now accepts optional array-based filters for `keywords`, `required_packages`, and `related_concepts`. These filters are combined with the main full-text search query to narrow down results. To support this, the underlying SQLite database schema has been extended with dedicated columns for this metadata. The build-time database generator and the runtime tool have both been updated to parse, validate, and store this structured data from the example file's front matter. The query logic is now fully dynamic, constructing parameterized SQL queries to safely and efficiently filter the examples based on the provided criteria.
1 parent 0ddefb6 commit d014630

File tree

2 files changed

+108
-19
lines changed

2 files changed

+108
-19
lines changed

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

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { glob, readFile } from 'node:fs/promises';
1010
import path from 'node:path';
11+
import type { SQLInputValue } from 'node:sqlite';
1112
import { z } from 'zod';
1213
import { McpToolContext, declareTool } from './tool-registry';
1314

@@ -36,6 +37,15 @@ Examples of queries:
3637
- Find lazy loading a route: 'lazy load route'
3738
- Find forms with validation: 'form AND (validation OR validator)'`,
3839
),
40+
keywords: z.array(z.string()).optional().describe('Filter examples by specific keywords.'),
41+
required_packages: z
42+
.array(z.string())
43+
.optional()
44+
.describe('Filter examples by required NPM packages (e.g., "@angular/forms").'),
45+
related_concepts: z
46+
.array(z.string())
47+
.optional()
48+
.describe('Filter examples by related high-level concepts.'),
3949
});
4050
type FindExampleInput = z.infer<typeof findExampleInputSchema>;
4151

@@ -55,7 +65,9 @@ new or evolving features.
5565
* **Modern Implementation:** Finding the correct modern syntax for features
5666
(e.g., query: 'functional route guard' or 'http client with fetch').
5767
* **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
58-
(e.g., query: 'built-in control flow' to replace "*ngIf').
68+
(e.g., query: 'built-in control flow' to replace "*ngIf").
69+
* **Advanced Filtering:** Combining a full-text search with filters to narrow results.
70+
(e.g., query: 'forms', required_packages: ['@angular/forms'], keywords: ['validation'])
5971
</Use Cases>
6072
<Operational Notes>
6173
* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
@@ -64,6 +76,8 @@ new or evolving features.
6476
* The examples in this database are the single source of truth for modern Angular coding patterns.
6577
* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
6678
parameter description for detailed syntax rules and examples.
79+
* You can combine the main 'query' with optional filters like 'keywords', 'required_packages',
80+
and 'related_concepts' to create highly specific searches.
6781
</Operational Notes>`,
6882
inputSchema: findExampleInputSchema.shape,
6983
outputSchema: {
@@ -104,7 +118,7 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext)
104118

105119
suppressSqliteWarning();
106120

107-
return async ({ query }: FindExampleInput) => {
121+
return async (input: FindExampleInput) => {
108122
if (!db) {
109123
if (!exampleDatabasePath) {
110124
// This should be prevented by the registration logic in mcp-server.ts
@@ -113,18 +127,45 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext)
113127
const { DatabaseSync } = await import('node:sqlite');
114128
db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
115129
}
116-
if (!queryStatement) {
117-
queryStatement = db.prepare(
118-
'SELECT content from examples_fts WHERE examples_fts MATCH ? ORDER BY rank;',
119-
);
130+
131+
const { query, keywords, required_packages, related_concepts } = input;
132+
133+
// Build the query dynamically
134+
const params: SQLInputValue[] = [];
135+
let sql = 'SELECT content FROM examples_fts';
136+
const whereClauses = [];
137+
138+
// FTS query
139+
if (query) {
140+
whereClauses.push('examples_fts MATCH ?');
141+
params.push(escapeSearchQuery(query));
120142
}
121143

122-
const sanitizedQuery = escapeSearchQuery(query);
144+
// JSON array filters
145+
const addJsonFilter = (column: string, values: string[] | undefined) => {
146+
if (values?.length) {
147+
for (const value of values) {
148+
whereClauses.push(`${column} LIKE ?`);
149+
params.push(`%"${value}"%`);
150+
}
151+
}
152+
};
153+
154+
addJsonFilter('keywords', keywords);
155+
addJsonFilter('required_packages', required_packages);
156+
addJsonFilter('related_concepts', related_concepts);
157+
158+
if (whereClauses.length > 0) {
159+
sql += ` WHERE ${whereClauses.join(' AND ')}`;
160+
}
161+
sql += ' ORDER BY rank;';
162+
163+
const queryStatement = db.prepare(sql);
123164

124165
// Query database and return results
125166
const examples = [];
126167
const textContent = [];
127-
for (const exampleRecord of queryStatement.all(sanitizedQuery)) {
168+
for (const exampleRecord of queryStatement.all(...params)) {
128169
const exampleContent = exampleRecord['content'] as string;
129170
examples.push({ content: exampleContent });
130171
textContent.push({ type: 'text' as const, text: exampleContent });
@@ -287,17 +328,22 @@ async function setupRuntimeExamples(
287328
title TEXT NOT NULL,
288329
summary TEXT NOT NULL,
289330
keywords TEXT,
331+
required_packages TEXT,
332+
related_concepts TEXT,
333+
related_tools TEXT,
290334
content TEXT NOT NULL
291335
);
292336
`);
293337

294338
// Create an FTS5 virtual table to provide full-text search capabilities.
295-
// It indexes the title, summary, keywords, and the full content.
296339
db.exec(`
297340
CREATE VIRTUAL TABLE examples_fts USING fts5(
298341
title,
299342
summary,
300343
keywords,
344+
required_packages,
345+
related_concepts,
346+
related_tools,
301347
content,
302348
content='examples',
303349
content_rowid='id',
@@ -308,19 +354,27 @@ async function setupRuntimeExamples(
308354
// Create triggers to keep the FTS table synchronized with the examples table.
309355
db.exec(`
310356
CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
311-
INSERT INTO examples_fts(rowid, title, summary, keywords, content)
312-
VALUES (new.id, new.title, new.summary, new.keywords, new.content);
357+
INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
358+
VALUES (
359+
new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
360+
new.related_tools, new.content
361+
);
313362
END;
314363
`);
315364

316365
const insertStatement = db.prepare(
317-
'INSERT INTO examples(title, summary, keywords, content) VALUES(?, ?, ?, ?);',
366+
'INSERT INTO examples(' +
367+
'title, summary, keywords, required_packages, related_concepts, related_tools, content' +
368+
') VALUES(?, ?, ?, ?, ?, ?, ?);',
318369
);
319370

320371
const frontmatterSchema = z.object({
321372
title: z.string(),
322373
summary: z.string(),
323374
keywords: z.array(z.string()).optional(),
375+
required_packages: z.array(z.string()).optional(),
376+
related_concepts: z.array(z.string()).optional(),
377+
related_tools: z.array(z.string()).optional(),
324378
});
325379

326380
db.exec('BEGIN TRANSACTION');
@@ -339,9 +393,18 @@ async function setupRuntimeExamples(
339393
continue;
340394
}
341395

342-
const { title, summary, keywords } = validation.data;
396+
const { title, summary, keywords, required_packages, related_concepts, related_tools } =
397+
validation.data;
343398

344-
insertStatement.run(title, summary, JSON.stringify(keywords ?? []), content);
399+
insertStatement.run(
400+
title,
401+
summary,
402+
JSON.stringify(keywords ?? []),
403+
JSON.stringify(required_packages ?? []),
404+
JSON.stringify(related_concepts ?? []),
405+
JSON.stringify(related_tools ?? []),
406+
content,
407+
);
345408
}
346409
db.exec('END TRANSACTION');
347410

tools/example_db_generator.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ function generate(inPath, outPath) {
8181
title TEXT NOT NULL,
8282
summary TEXT NOT NULL,
8383
keywords TEXT,
84+
required_packages TEXT,
85+
related_concepts TEXT,
86+
related_tools TEXT,
8487
content TEXT NOT NULL
8588
);
8689
`);
@@ -91,6 +94,9 @@ function generate(inPath, outPath) {
9194
title,
9295
summary,
9396
keywords,
97+
required_packages,
98+
related_concepts,
99+
related_tools,
94100
content,
95101
content='examples',
96102
content_rowid='id',
@@ -101,19 +107,30 @@ function generate(inPath, outPath) {
101107
// Create triggers to keep the FTS table synchronized with the examples table.
102108
db.exec(`
103109
CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
104-
INSERT INTO examples_fts(rowid, title, summary, keywords, content)
105-
VALUES (new.id, new.title, new.summary, new.keywords, new.content);
110+
INSERT INTO examples_fts(
111+
rowid, title, summary, keywords, required_packages, related_concepts, related_tools,
112+
content
113+
)
114+
VALUES (
115+
new.id, new.title, new.summary, new.keywords, new.required_packages,
116+
new.related_concepts, new.related_tools, new.content
117+
);
106118
END;
107119
`);
108120

109121
const insertStatement = db.prepare(
110-
'INSERT INTO examples(title, summary, keywords, content) VALUES(?, ?, ?, ?);',
122+
'INSERT INTO examples(' +
123+
'title, summary, keywords, required_packages, related_concepts, related_tools, content' +
124+
') VALUES(?, ?, ?, ?, ?, ?, ?);',
111125
);
112126

113127
const frontmatterSchema = z.object({
114128
title: z.string(),
115129
summary: z.string(),
116130
keywords: z.array(z.string()).optional(),
131+
required_packages: z.array(z.string()).optional(),
132+
related_concepts: z.array(z.string()).optional(),
133+
related_tools: z.array(z.string()).optional(),
117134
});
118135

119136
db.exec('BEGIN TRANSACTION');
@@ -135,8 +152,17 @@ function generate(inPath, outPath) {
135152
throw new Error(`Invalid front matter in ${entry.name}`);
136153
}
137154

138-
const { title, summary, keywords } = validation.data;
139-
insertStatement.run(title, summary, JSON.stringify(keywords ?? []), content);
155+
const { title, summary, keywords, required_packages, related_concepts, related_tools } =
156+
validation.data;
157+
insertStatement.run(
158+
title,
159+
summary,
160+
JSON.stringify(keywords ?? []),
161+
JSON.stringify(required_packages ?? []),
162+
JSON.stringify(related_concepts ?? []),
163+
JSON.stringify(related_tools ?? []),
164+
content,
165+
);
140166
}
141167
db.exec('END TRANSACTION');
142168

0 commit comments

Comments
 (0)