8
8
9
9
import { glob , readFile } from 'node:fs/promises' ;
10
10
import path from 'node:path' ;
11
+ import type { SQLInputValue } from 'node:sqlite' ;
11
12
import { z } from 'zod' ;
12
13
import { McpToolContext , declareTool } from './tool-registry' ;
13
14
@@ -36,6 +37,15 @@ Examples of queries:
36
37
- Find lazy loading a route: 'lazy load route'
37
38
- Find forms with validation: 'form AND (validation OR validator)'` ,
38
39
) ,
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.' ) ,
39
49
} ) ;
40
50
type FindExampleInput = z . infer < typeof findExampleInputSchema > ;
41
51
@@ -55,7 +65,9 @@ new or evolving features.
55
65
* **Modern Implementation:** Finding the correct modern syntax for features
56
66
(e.g., query: 'functional route guard' or 'http client with fetch').
57
67
* **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'])
59
71
</Use Cases>
60
72
<Operational Notes>
61
73
* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
@@ -64,6 +76,8 @@ new or evolving features.
64
76
* The examples in this database are the single source of truth for modern Angular coding patterns.
65
77
* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
66
78
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.
67
81
</Operational Notes>` ,
68
82
inputSchema : findExampleInputSchema . shape ,
69
83
outputSchema : {
@@ -104,7 +118,7 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext)
104
118
105
119
suppressSqliteWarning ( ) ;
106
120
107
- return async ( { query } : FindExampleInput ) => {
121
+ return async ( input : FindExampleInput ) => {
108
122
if ( ! db ) {
109
123
if ( ! exampleDatabasePath ) {
110
124
// This should be prevented by the registration logic in mcp-server.ts
@@ -113,18 +127,45 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext)
113
127
const { DatabaseSync } = await import ( 'node:sqlite' ) ;
114
128
db = new DatabaseSync ( exampleDatabasePath , { readOnly : true } ) ;
115
129
}
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 ) ) ;
120
142
}
121
143
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 ) ;
123
164
124
165
// Query database and return results
125
166
const examples = [ ] ;
126
167
const textContent = [ ] ;
127
- for ( const exampleRecord of queryStatement . all ( sanitizedQuery ) ) {
168
+ for ( const exampleRecord of queryStatement . all ( ... params ) ) {
128
169
const exampleContent = exampleRecord [ 'content' ] as string ;
129
170
examples . push ( { content : exampleContent } ) ;
130
171
textContent . push ( { type : 'text' as const , text : exampleContent } ) ;
@@ -287,17 +328,22 @@ async function setupRuntimeExamples(
287
328
title TEXT NOT NULL,
288
329
summary TEXT NOT NULL,
289
330
keywords TEXT,
331
+ required_packages TEXT,
332
+ related_concepts TEXT,
333
+ related_tools TEXT,
290
334
content TEXT NOT NULL
291
335
);
292
336
` ) ;
293
337
294
338
// Create an FTS5 virtual table to provide full-text search capabilities.
295
- // It indexes the title, summary, keywords, and the full content.
296
339
db . exec ( `
297
340
CREATE VIRTUAL TABLE examples_fts USING fts5(
298
341
title,
299
342
summary,
300
343
keywords,
344
+ required_packages,
345
+ related_concepts,
346
+ related_tools,
301
347
content,
302
348
content='examples',
303
349
content_rowid='id',
@@ -308,19 +354,27 @@ async function setupRuntimeExamples(
308
354
// Create triggers to keep the FTS table synchronized with the examples table.
309
355
db . exec ( `
310
356
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
+ );
313
362
END;
314
363
` ) ;
315
364
316
365
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(?, ?, ?, ?, ?, ?, ?);' ,
318
369
) ;
319
370
320
371
const frontmatterSchema = z . object ( {
321
372
title : z . string ( ) ,
322
373
summary : z . string ( ) ,
323
374
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 ( ) ,
324
378
} ) ;
325
379
326
380
db . exec ( 'BEGIN TRANSACTION' ) ;
@@ -339,9 +393,18 @@ async function setupRuntimeExamples(
339
393
continue ;
340
394
}
341
395
342
- const { title, summary, keywords } = validation . data ;
396
+ const { title, summary, keywords, required_packages, related_concepts, related_tools } =
397
+ validation . data ;
343
398
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
+ ) ;
345
408
}
346
409
db . exec ( 'END TRANSACTION' ) ;
347
410
0 commit comments