@@ -72,6 +72,15 @@ Examples of queries:
72
72
- Find signal inputs: 'signal input'
73
73
- Find lazy loading a route: 'lazy load route'
74
74
- Find forms with validation: 'form AND (validation OR validator)'` ) ,
75
+ keywords : zod_1 . z . array ( zod_1 . z . string ( ) ) . optional ( ) . describe ( 'Filter examples by specific keywords.' ) ,
76
+ required_packages : zod_1 . z
77
+ . array ( zod_1 . z . string ( ) )
78
+ . optional ( )
79
+ . describe ( 'Filter examples by required NPM packages (e.g., "@angular/forms").' ) ,
80
+ related_concepts : zod_1 . z
81
+ . array ( zod_1 . z . string ( ) )
82
+ . optional ( )
83
+ . describe ( 'Filter examples by related high-level concepts.' ) ,
75
84
} ) ;
76
85
exports . FIND_EXAMPLE_TOOL = ( 0 , tool_registry_1 . declareTool ) ( {
77
86
name : 'find_examples' ,
@@ -89,7 +98,9 @@ new or evolving features.
89
98
* **Modern Implementation:** Finding the correct modern syntax for features
90
99
(e.g., query: 'functional route guard' or 'http client with fetch').
91
100
* **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
92
- (e.g., query: 'built-in control flow' to replace "*ngIf').
101
+ (e.g., query: 'built-in control flow' to replace "*ngIf").
102
+ * **Advanced Filtering:** Combining a full-text search with filters to narrow results.
103
+ (e.g., query: 'forms', required_packages: ['@angular/forms'], keywords: ['validation'])
93
104
</Use Cases>
94
105
<Operational Notes>
95
106
* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
@@ -98,6 +109,8 @@ new or evolving features.
98
109
* The examples in this database are the single source of truth for modern Angular coding patterns.
99
110
* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
100
111
parameter description for detailed syntax rules and examples.
112
+ * You can combine the main 'query' with optional filters like 'keywords', 'required_packages',
113
+ and 'related_concepts' to create highly specific searches.
101
114
</Operational Notes>` ,
102
115
inputSchema : findExampleInputSchema . shape ,
103
116
outputSchema : {
@@ -128,7 +141,7 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
128
141
db = await setupRuntimeExamples ( process . env [ 'NG_MCP_EXAMPLES_DIR' ] ) ;
129
142
}
130
143
suppressSqliteWarning ( ) ;
131
- return async ( { query } ) => {
144
+ return async ( input ) => {
132
145
if ( ! db ) {
133
146
if ( ! exampleDatabasePath ) {
134
147
// This should be prevented by the registration logic in mcp-server.ts
@@ -137,14 +150,41 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
137
150
const { DatabaseSync } = await Promise . resolve ( ) . then ( ( ) => __importStar ( require ( 'node:sqlite' ) ) ) ;
138
151
db = new DatabaseSync ( exampleDatabasePath , { readOnly : true } ) ;
139
152
}
140
- if ( ! queryStatement ) {
141
- queryStatement = db . prepare ( 'SELECT * from examples WHERE examples MATCH ? ORDER BY rank;' ) ;
153
+ const { query, keywords, required_packages, related_concepts } = input ;
154
+ // Build the query dynamically
155
+ const params = [ ] ;
156
+ let sql = 'SELECT content FROM examples_fts' ;
157
+ const whereClauses = [ ] ;
158
+ // FTS query
159
+ if ( query ) {
160
+ whereClauses . push ( 'examples_fts MATCH ?' ) ;
161
+ params . push ( escapeSearchQuery ( query ) ) ;
142
162
}
143
- const sanitizedQuery = escapeSearchQuery ( query ) ;
163
+ // JSON array filters
164
+ const addJsonFilter = ( column , values ) => {
165
+ if ( values ?. length ) {
166
+ for ( const value of values ) {
167
+ whereClauses . push ( `${ column } LIKE ?` ) ;
168
+ params . push ( `%"${ value } "%` ) ;
169
+ }
170
+ }
171
+ } ;
172
+ addJsonFilter ( 'keywords' , keywords ) ;
173
+ addJsonFilter ( 'required_packages' , required_packages ) ;
174
+ addJsonFilter ( 'related_concepts' , related_concepts ) ;
175
+ if ( whereClauses . length > 0 ) {
176
+ sql += ` WHERE ${ whereClauses . join ( ' AND ' ) } ` ;
177
+ }
178
+ // Order the results by relevance using the BM25 algorithm.
179
+ // The weights assigned to each column boost the ranking of documents where the
180
+ // search term appears in a more important field.
181
+ // Column order: title, summary, keywords, required_packages, related_concepts, related_tools, content
182
+ sql += ' ORDER BY bm25(examples_fts, 10.0, 5.0, 5.0, 1.0, 2.0, 1.0, 1.0);' ;
183
+ const queryStatement = db . prepare ( sql ) ;
144
184
// Query database and return results
145
185
const examples = [ ] ;
146
186
const textContent = [ ] ;
147
- for ( const exampleRecord of queryStatement . all ( sanitizedQuery ) ) {
187
+ for ( const exampleRecord of queryStatement . all ( ... params ) ) {
148
188
const exampleContent = exampleRecord [ 'content' ] ;
149
189
examples . push ( { content : exampleContent } ) ;
150
190
textContent . push ( { type : 'text' , text : exampleContent } ) ;
@@ -232,18 +272,120 @@ function suppressSqliteWarning() {
232
272
return originalProcessEmit . apply ( process , arguments ) ;
233
273
} ;
234
274
}
275
+ /**
276
+ * A simple YAML front matter parser.
277
+ *
278
+ * This function extracts the YAML block enclosed by `---` at the beginning of a string
279
+ * and parses it into a JavaScript object. It is not a full YAML parser and only
280
+ * supports simple key-value pairs and string arrays.
281
+ *
282
+ * @param content The string content to parse.
283
+ * @returns A record containing the parsed front matter data.
284
+ */
285
+ function parseFrontmatter ( content ) {
286
+ const match = content . match ( / ^ - - - \r ? \n ( .* ?) \r ? \n - - - / s) ;
287
+ if ( ! match ) {
288
+ return { } ;
289
+ }
290
+ const frontmatter = match [ 1 ] ;
291
+ const data = { } ;
292
+ const lines = frontmatter . split ( / \r ? \n / ) ;
293
+ let currentKey = '' ;
294
+ let isArray = false ;
295
+ const arrayValues = [ ] ;
296
+ for ( const line of lines ) {
297
+ const keyValueMatch = line . match ( / ^ ( [ ^ : ] + ) : \s * ( .* ) / ) ;
298
+ if ( keyValueMatch ) {
299
+ if ( currentKey && isArray ) {
300
+ data [ currentKey ] = arrayValues . slice ( ) ;
301
+ arrayValues . length = 0 ;
302
+ }
303
+ const [ , key , value ] = keyValueMatch ;
304
+ currentKey = key . trim ( ) ;
305
+ isArray = value . trim ( ) === '' ;
306
+ if ( ! isArray ) {
307
+ data [ currentKey ] = value . trim ( ) ;
308
+ }
309
+ }
310
+ else {
311
+ const arrayItemMatch = line . match ( / ^ \s * - \s * ( .* ) / ) ;
312
+ if ( arrayItemMatch && currentKey && isArray ) {
313
+ arrayValues . push ( arrayItemMatch [ 1 ] . trim ( ) ) ;
314
+ }
315
+ }
316
+ }
317
+ if ( currentKey && isArray ) {
318
+ data [ currentKey ] = arrayValues ;
319
+ }
320
+ return data ;
321
+ }
235
322
async function setupRuntimeExamples ( examplesPath ) {
236
323
const { DatabaseSync } = await Promise . resolve ( ) . then ( ( ) => __importStar ( require ( 'node:sqlite' ) ) ) ;
237
324
const db = new DatabaseSync ( ':memory:' ) ;
238
- db . exec ( `CREATE VIRTUAL TABLE examples USING fts5(content, tokenize = 'porter ascii');` ) ;
239
- const insertStatement = db . prepare ( 'INSERT INTO examples(content) VALUES(?);' ) ;
325
+ // Create a relational table to store the structured example data.
326
+ db . exec ( `
327
+ CREATE TABLE examples (
328
+ id INTEGER PRIMARY KEY,
329
+ title TEXT NOT NULL,
330
+ summary TEXT NOT NULL,
331
+ keywords TEXT,
332
+ required_packages TEXT,
333
+ related_concepts TEXT,
334
+ related_tools TEXT,
335
+ content TEXT NOT NULL
336
+ );
337
+ ` ) ;
338
+ // Create an FTS5 virtual table to provide full-text search capabilities.
339
+ db . exec ( `
340
+ CREATE VIRTUAL TABLE examples_fts USING fts5(
341
+ title,
342
+ summary,
343
+ keywords,
344
+ required_packages,
345
+ related_concepts,
346
+ related_tools,
347
+ content,
348
+ content='examples',
349
+ content_rowid='id',
350
+ tokenize = 'porter ascii'
351
+ );
352
+ ` ) ;
353
+ // Create triggers to keep the FTS table synchronized with the examples table.
354
+ db . exec ( `
355
+ CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
356
+ INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
357
+ VALUES (
358
+ new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
359
+ new.related_tools, new.content
360
+ );
361
+ END;
362
+ ` ) ;
363
+ const insertStatement = db . prepare ( 'INSERT INTO examples(' +
364
+ 'title, summary, keywords, required_packages, related_concepts, related_tools, content' +
365
+ ') VALUES(?, ?, ?, ?, ?, ?, ?);' ) ;
366
+ const frontmatterSchema = zod_1 . z . object ( {
367
+ title : zod_1 . z . string ( ) ,
368
+ summary : zod_1 . z . string ( ) ,
369
+ keywords : zod_1 . z . array ( zod_1 . z . string ( ) ) . optional ( ) ,
370
+ required_packages : zod_1 . z . array ( zod_1 . z . string ( ) ) . optional ( ) ,
371
+ related_concepts : zod_1 . z . array ( zod_1 . z . string ( ) ) . optional ( ) ,
372
+ related_tools : zod_1 . z . array ( zod_1 . z . string ( ) ) . optional ( ) ,
373
+ } ) ;
240
374
db . exec ( 'BEGIN TRANSACTION' ) ;
241
- for await ( const entry of ( 0 , promises_1 . glob ) ( '*.md' , { cwd : examplesPath , withFileTypes : true } ) ) {
375
+ for await ( const entry of ( 0 , promises_1 . glob ) ( '**/* .md' , { cwd : examplesPath , withFileTypes : true } ) ) {
242
376
if ( ! entry . isFile ( ) ) {
243
377
continue ;
244
378
}
245
- const example = await ( 0 , promises_1 . readFile ) ( node_path_1 . default . join ( entry . parentPath , entry . name ) , 'utf-8' ) ;
246
- insertStatement . run ( example ) ;
379
+ const content = await ( 0 , promises_1 . readFile ) ( node_path_1 . default . join ( entry . parentPath , entry . name ) , 'utf-8' ) ;
380
+ const frontmatter = parseFrontmatter ( content ) ;
381
+ const validation = frontmatterSchema . safeParse ( frontmatter ) ;
382
+ if ( ! validation . success ) {
383
+ // eslint-disable-next-line no-console
384
+ console . warn ( `Skipping invalid example file ${ entry . name } :` , validation . error . issues ) ;
385
+ continue ;
386
+ }
387
+ const { title, summary, keywords, required_packages, related_concepts, related_tools } = validation . data ;
388
+ insertStatement . run ( title , summary , JSON . stringify ( keywords ?? [ ] ) , JSON . stringify ( required_packages ?? [ ] ) , JSON . stringify ( related_concepts ?? [ ] ) , JSON . stringify ( related_tools ?? [ ] ) , content ) ;
247
389
}
248
390
db . exec ( 'END TRANSACTION' ) ;
249
391
return db ;
0 commit comments