88
99import { glob , readFile } from 'node:fs/promises' ;
1010import path from 'node:path' ;
11+ import type { SQLInputValue } from 'node:sqlite' ;
1112import { z } from 'zod' ;
1213import { 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} ) ;
4050type 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
0 commit comments