@@ -199,6 +199,13 @@ new or evolving features.
199199 factory : createFindExampleHandler ,
200200} ) ;
201201
202+ const SQLITE_FORMAT = 'sqlite' ;
203+ const MARKDOWN_DIR_FORMAT = 'markdown-dir' ;
204+
205+ type ExampleSource =
206+ | { type : typeof SQLITE_FORMAT ; path : string ; source : string }
207+ | { type : typeof MARKDOWN_DIR_FORMAT ; path : string ; source : string } ;
208+
202209/**
203210 * A list of known Angular packages that may contain example databases.
204211 * The tool will attempt to resolve and load example databases from these packages.
@@ -229,9 +236,9 @@ const KNOWN_EXAMPLE_PACKAGES = ['@angular/core', '@angular/aria', '@angular/form
229236async function getVersionSpecificExampleDatabases (
230237 workspacePath : string ,
231238 logger : McpToolContext [ 'logger' ] ,
232- ) : Promise < { dbPath : string ; source : string } [ ] > {
239+ ) : Promise < ExampleSource [ ] > {
233240 const workspaceRequire = createRequire ( workspacePath ) ;
234- const databases : { dbPath : string ; source : string } [ ] = [ ] ;
241+ const databases : ExampleSource [ ] = [ ] ;
235242
236243 for ( const packageName of KNOWN_EXAMPLE_PACKAGES ) {
237244 // 1. Resolve the path to package.json
@@ -251,7 +258,7 @@ async function getVersionSpecificExampleDatabases(
251258
252259 if (
253260 examplesInfo &&
254- examplesInfo . format === 'sqlite' &&
261+ ( examplesInfo . format === SQLITE_FORMAT || examplesInfo . format === MARKDOWN_DIR_FORMAT ) &&
255262 typeof examplesInfo . path === 'string'
256263 ) {
257264 const packageDirectory = dirname ( pkgJsonPath ) ;
@@ -268,19 +275,21 @@ async function getVersionSpecificExampleDatabases(
268275 continue ;
269276 }
270277
271- // Check the file size to prevent reading a very large file.
272- const stats = await stat ( dbPath ) ;
273- if ( stats . size > 10 * 1024 * 1024 ) {
274- // 10MB
275- logger . warn (
276- `The example database at '${ dbPath } ' is larger than 10MB (${ stats . size } bytes). ` +
277- 'This is unexpected and the file will not be used.' ,
278- ) ;
279- continue ;
278+ if ( examplesInfo . format === SQLITE_FORMAT ) {
279+ // Check the file size to prevent reading a very large file.
280+ const stats = await stat ( dbPath ) ;
281+ if ( stats . size > 10 * 1024 * 1024 ) {
282+ // 10MB
283+ logger . warn (
284+ `The example database at '${ dbPath } ' is larger than 10MB (${ stats . size } bytes). ` +
285+ 'This is unexpected and the file will not be used.' ,
286+ ) ;
287+ continue ;
288+ }
280289 }
281290
282291 const source = `package ${ packageName } @${ pkgJson . version } ` ;
283- databases . push ( { dbPath, source } ) ;
292+ databases . push ( { type : examplesInfo . format , path : dbPath , source } ) ;
284293 }
285294 } catch ( e ) {
286295 logger . warn (
@@ -307,42 +316,45 @@ async function createFindExampleHandler({ logger, exampleDatabasePath }: McpTool
307316 return queryDatabase ( [ runtimeDb ] , input ) ;
308317 }
309318
310- const resolvedDbs : { path : string ; source : string } [ ] = [ ] ;
319+ const resolvedSources : ExampleSource [ ] = [ ] ;
311320
312321 // First, try to get all available version-specific guides.
313322 if ( input . workspacePath ) {
314323 const versionSpecificDbs = await getVersionSpecificExampleDatabases (
315324 input . workspacePath ,
316325 logger ,
317326 ) ;
318- for ( const db of versionSpecificDbs ) {
319- resolvedDbs . push ( { path : db . dbPath , source : db . source } ) ;
320- }
327+ resolvedSources . push ( ...versionSpecificDbs ) ;
321328 }
322329
323330 // If no version-specific guides were found for any reason, fall back to the bundled version.
324- if ( resolvedDbs . length === 0 && exampleDatabasePath ) {
325- resolvedDbs . push ( { path : exampleDatabasePath , source : 'bundled' } ) ;
331+ if ( resolvedSources . length === 0 && exampleDatabasePath ) {
332+ resolvedSources . push ( { type : SQLITE_FORMAT , path : exampleDatabasePath , source : 'bundled' } ) ;
326333 }
327334
328- if ( resolvedDbs . length === 0 ) {
335+ if ( resolvedSources . length === 0 ) {
329336 // This should be prevented by the registration logic in mcp-server.ts
330337 throw new Error ( 'No example databases are available.' ) ;
331338 }
332339
333340 const { DatabaseSync } = await import ( 'node:sqlite' ) ;
334341 const dbConnections : DatabaseSync [ ] = [ ] ;
335342
336- for ( const { path, source } of resolvedDbs ) {
337- const db = new DatabaseSync ( path , { readOnly : true } ) ;
338- try {
339- validateDatabaseSchema ( db , source ) ;
343+ for ( const source of resolvedSources ) {
344+ if ( source . type === SQLITE_FORMAT ) {
345+ const db = new DatabaseSync ( source . path , { readOnly : true } ) ;
346+ try {
347+ validateDatabaseSchema ( db , source . source ) ;
348+ dbConnections . push ( db ) ;
349+ } catch ( e ) {
350+ logger . warn ( ( e as Error ) . message ) ;
351+ // If a database is invalid, we should not query it, but we should not fail the whole tool.
352+ // We will just skip this database and try to use the others.
353+ continue ;
354+ }
355+ } else if ( source . type === MARKDOWN_DIR_FORMAT ) {
356+ const db = await setupRuntimeExamples ( source . path ) ;
340357 dbConnections . push ( db ) ;
341- } catch ( e ) {
342- logger . warn ( ( e as Error ) . message ) ;
343- // If a database is invalid, we should not query it, but we should not fail the whole tool.
344- // We will just skip this database and try to use the others.
345- continue ;
346358 }
347359 }
348360
@@ -672,21 +684,56 @@ async function setupRuntimeExamples(examplesPath: string): Promise<DatabaseSync>
672684 related_concepts : z . array ( z . string ( ) ) . optional ( ) ,
673685 related_tools : z . array ( z . string ( ) ) . optional ( ) ,
674686 experimental : z . boolean ( ) . optional ( ) ,
687+ format_version : z . preprocess (
688+ ( val ) => ( val === undefined ? 1 : val ) ,
689+ z . literal ( 1 , {
690+ errorMap : ( ) => ( {
691+ message :
692+ 'The example format is incompatible. This version of the CLI requires format_version: 1.' ,
693+ } ) ,
694+ } ) ,
695+ ) ,
675696 } ) ;
676697
698+ const MAX_FILE_COUNT = 1000 ;
699+ const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024 ; // 1MB
700+
677701 db . exec ( 'BEGIN TRANSACTION' ) ;
678- for await ( const entry of glob ( '**/*.md' , { cwd : examplesPath , withFileTypes : true } ) ) {
679- if ( ! entry . isFile ( ) ) {
702+ let fileCount = 0 ;
703+ for await ( const filePath of glob ( '**/*.md' , { cwd : examplesPath } ) ) {
704+ if ( fileCount >= MAX_FILE_COUNT ) {
705+ // eslint-disable-next-line no-console
706+ console . warn (
707+ `Warning: Example directory '${ examplesPath } ' contains more than the maximum allowed ` +
708+ `${ MAX_FILE_COUNT } files. Only the first ${ MAX_FILE_COUNT } files will be processed.` ,
709+ ) ;
710+ break ;
711+ }
712+
713+ const fullPath = join ( examplesPath , filePath ) ;
714+ const stats = await stat ( fullPath ) ;
715+
716+ if ( ! stats . isFile ( ) ) {
717+ continue ;
718+ }
719+ fileCount ++ ;
720+
721+ if ( stats . size > MAX_FILE_SIZE_BYTES ) {
722+ // eslint-disable-next-line no-console
723+ console . warn (
724+ `Warning: Skipping example file '${ filePath } ' because it exceeds the ` +
725+ `maximum file size of ${ MAX_FILE_SIZE_BYTES } bytes.` ,
726+ ) ;
680727 continue ;
681728 }
682729
683- const content = await readFile ( join ( entry . parentPath , entry . name ) , 'utf-8' ) ;
730+ const content = await readFile ( fullPath , 'utf-8' ) ;
684731 const frontmatter = parseFrontmatter ( content ) ;
685732
686733 const validation = frontmatterSchema . safeParse ( frontmatter ) ;
687734 if ( ! validation . success ) {
688735 // eslint-disable-next-line no-console
689- console . warn ( `Skipping invalid example file ${ entry . name } :` , validation . error . issues ) ;
736+ console . warn ( `Skipping invalid example file ${ filePath } :` , validation . error . issues ) ;
690737 continue ;
691738 }
692739
0 commit comments