From 6d7666ca9d7ac2553e2f157a0e5416a67df18c7f Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:55:52 -0400 Subject: [PATCH 1/3] fix(@angular/cli): enhance find_examples MCP tool with structured output This commit significantly enhances the `find_examples` MCP tool to improve its usability and the quality of its output, primarily from an LLM interaction perspective. Key changes include: 1. **Structured Output:** The tool's output schema is updated to return rich metadata for each example, including its `title`, `summary`, `keywords`, and `required_packages`. This allows the AI to present results more intelligently and check for prerequisites. 2. **Prescriptive Schema Descriptions:** The descriptions for both the input and output schemas have been rewritten to be more prescriptive. They now guide the AI on *how* and *why* to use specific fields, acting as a form of prompt engineering to elicit more precise queries and better-formatted responses. 3. **Code Refactoring:** The output schema definition has been moved to a module-level constant to improve code readability and consistency, reducing nesting in the main tool declaration. --- .../cli/src/commands/mcp/tools/examples.ts | 146 +++++++++++++----- 1 file changed, 106 insertions(+), 40 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/examples.ts b/packages/angular/cli/src/commands/mcp/tools/examples.ts index 2a066535d302..0986e9d2d795 100644 --- a/packages/angular/cli/src/commands/mcp/tools/examples.ts +++ b/packages/angular/cli/src/commands/mcp/tools/examples.ts @@ -13,42 +13,103 @@ import { z } from 'zod'; import { McpToolContext, declareTool } from './tool-registry'; const findExampleInputSchema = z.object({ - query: z.string().describe( - `Performs a full-text search using FTS5 syntax. The query should target relevant Angular concepts. - -Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation): - - AND (default): Space-separated terms are combined with AND. - - Example: 'standalone component' (finds results with both "standalone" and "component") - - OR: Use the OR operator to find results with either term. - - Example: 'validation OR validator' - - NOT: Use the NOT operator to exclude terms. - - Example: 'forms NOT reactive' - - Grouping: Use parentheses () to group expressions. - - Example: '(validation OR validator) AND forms' - - Phrase Search: Use double quotes "" for exact phrases. - - Example: '"template-driven forms"' - - Prefix Search: Use an asterisk * for prefix matching. - - Example: 'rout*' (matches "route", "router", "routing") - -Examples of queries: - - Find standalone components: 'standalone component' - - Find ngFor with trackBy: 'ngFor trackBy' - - Find signal inputs: 'signal input' - - Find lazy loading a route: 'lazy load route' - - Find forms with validation: 'form AND (validation OR validator)'`, - ), - keywords: z.array(z.string()).optional().describe('Filter examples by specific keywords.'), + query: z + .string() + .describe( + `The primary, conceptual search query. This should capture the user's main goal or question ` + + `(e.g., 'lazy loading a route' or 'how to use signal inputs'). The query will be processed ` + + 'by a powerful full-text search engine.\n\n' + + 'Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):\n' + + ' - AND (default): Space-separated terms are combined with AND.\n' + + ' - Example: \'standalone component\' (finds results with both "standalone" and "component")\n' + + ' - OR: Use the OR operator to find results with either term.\n' + + " - Example: 'validation OR validator'\n" + + ' - NOT: Use the NOT operator to exclude terms.\n' + + " - Example: 'forms NOT reactive'\n" + + ' - Grouping: Use parentheses () to group expressions.\n' + + " - Example: '(validation OR validator) AND forms'\n" + + ' - Phrase Search: Use double quotes "" for exact phrases.\n' + + ' - Example: \'"template-driven forms"\'\n' + + ' - Prefix Search: Use an asterisk * for prefix matching.\n' + + ' - Example: \'rout*\' (matches "route", "router", "routing")', + ), + keywords: z + .array(z.string()) + .optional() + .describe( + 'A list of specific, exact keywords to narrow the search. Use this for precise terms like ' + + 'API names, function names, or decorators (e.g., `ngFor`, `trackBy`, `inject`).', + ), required_packages: z .array(z.string()) .optional() - .describe('Filter examples by required NPM packages (e.g., "@angular/forms").'), + .describe( + "A list of NPM packages that an example must use. Use this when the user's request is " + + 'specific to a feature within a certain package (e.g., if the user asks about `ngModel`, ' + + 'you should filter by `@angular/forms`).', + ), related_concepts: z .array(z.string()) .optional() - .describe('Filter examples by related high-level concepts.'), + .describe( + 'A list of high-level concepts to filter by. Use this to find examples related to broader ' + + 'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).', + ), }); + type FindExampleInput = z.infer; +const findExampleOutputSchema = z.object({ + examples: z.array( + z.object({ + title: z + .string() + .describe( + 'The title of the example. Use this as a heading when presenting the example to the user.', + ), + summary: z + .string() + .describe( + "A one-sentence summary of the example's purpose. Use this to help the user decide " + + 'if the example is relevant to them.', + ), + keywords: z + .array(z.string()) + .optional() + .describe( + 'A list of keywords for the example. You can use these to explain why this example ' + + "was a good match for the user's query.", + ), + required_packages: z + .array(z.string()) + .optional() + .describe( + 'A list of NPM packages required for the example to work. Before presenting the code, ' + + 'you should inform the user if any of these packages need to be installed.', + ), + related_concepts: z + .array(z.string()) + .optional() + .describe( + 'A list of related concepts. You can suggest these to the user as topics for ' + + 'follow-up questions.', + ), + related_tools: z + .array(z.string()) + .optional() + .describe( + 'A list of related MCP tools. You can suggest these as potential next steps for the user.', + ), + content: z + .string() + .describe( + 'A complete, self-contained Angular code example in Markdown format. This should be ' + + 'presented to the user inside a markdown code block.', + ), + }), + ), +}); + export const FIND_EXAMPLE_TOOL = declareTool({ name: 'find_examples', title: 'Find Angular Code Examples', @@ -80,15 +141,7 @@ new or evolving features. and 'related_concepts' to create highly specific searches. `, inputSchema: findExampleInputSchema.shape, - outputSchema: { - examples: z.array( - z.object({ - content: z - .string() - .describe('A complete, self-contained Angular code example in Markdown format.'), - }), - ), - }, + outputSchema: findExampleOutputSchema.shape, isReadOnly: true, isLocalOnly: true, shouldRegister: ({ logger }) => { @@ -132,7 +185,8 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext) // Build the query dynamically const params: SQLInputValue[] = []; - let sql = 'SELECT content FROM examples_fts'; + let sql = + 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content FROM examples_fts'; const whereClauses = []; // FTS query @@ -171,9 +225,21 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext) const examples = []; const textContent = []; for (const exampleRecord of queryStatement.all(...params)) { - const exampleContent = exampleRecord['content'] as string; - examples.push({ content: exampleContent }); - textContent.push({ type: 'text' as const, text: exampleContent }); + const record = exampleRecord as Record; + const example = { + title: record['title'], + summary: record['summary'], + keywords: JSON.parse(record['keywords'] || '[]') as string[], + required_packages: JSON.parse(record['required_packages'] || '[]') as string[], + related_concepts: JSON.parse(record['related_concepts'] || '[]') as string[], + related_tools: JSON.parse(record['related_tools'] || '[]') as string[], + content: record['content'], + }; + examples.push(example); + + // Also create a more structured text output + const text = `## Example: ${example.title}\n**Summary:** ${example.summary}\n\n---\n\n${example.content}`; + textContent.push({ type: 'text' as const, text }); } return { From 867cddbb0f58e5e1793281b1e47c9c2f97cdb9a0 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 8 Sep 2025 18:42:04 -0400 Subject: [PATCH 2/3] refactor(@angular/cli): add experimental flag for MCP examples This commit introduces a feature to mark and filter code examples that use experimental APIs, ensuring that the `find_examples` tool provides production-safe results by default. Key changes: - The example format now supports an optional `experimental: true` flag in the front matter. - The database schema is updated with an `experimental` column. Both the build-time and runtime database generators now parse and store this flag. - The `find_examples` tool's input schema is enhanced with an `includeExperimental` boolean flag, which defaults to `false`. - The query logic is updated to filter out experimental examples unless `includeExperimental` is explicitly set to `true`. - The schema description for the new flag includes a strong prescriptive guardrail, instructing the AI to warn the user when it shows an experimental example. --- .../cli/src/commands/mcp/tools/examples.ts | 34 ++++++++++++++++--- tools/example_db_generator.js | 18 +++++++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/examples.ts b/packages/angular/cli/src/commands/mcp/tools/examples.ts index 0986e9d2d795..364145f832ab 100644 --- a/packages/angular/cli/src/commands/mcp/tools/examples.ts +++ b/packages/angular/cli/src/commands/mcp/tools/examples.ts @@ -55,6 +55,16 @@ const findExampleInputSchema = z.object({ 'A list of high-level concepts to filter by. Use this to find examples related to broader ' + 'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).', ), + includeExperimental: z + .boolean() + .optional() + .default(false) + .describe( + 'By default, this tool returns only production-safe examples. Set this to `true` **only if** ' + + 'the user explicitly asks for a bleeding-edge feature or if a stable solution to their ' + + 'problem cannot be found. If you set this to `true`, you **MUST** preface your answer by ' + + 'warning the user that the example uses experimental APIs that are not suitable for production.', + ), }); type FindExampleInput = z.infer; @@ -181,7 +191,7 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext) db = new DatabaseSync(exampleDatabasePath, { readOnly: true }); } - const { query, keywords, required_packages, related_concepts } = input; + const { query, keywords, required_packages, related_concepts, includeExperimental } = input; // Build the query dynamically const params: SQLInputValue[] = []; @@ -209,6 +219,10 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext) addJsonFilter('required_packages', required_packages); addJsonFilter('related_concepts', related_concepts); + if (!includeExperimental) { + whereClauses.push('experimental = 0'); + } + if (whereClauses.length > 0) { sql += ` WHERE ${whereClauses.join(' AND ')}`; } @@ -402,6 +416,7 @@ async function setupRuntimeExamples( required_packages TEXT, related_concepts TEXT, related_tools TEXT, + experimental INTEGER NOT NULL DEFAULT 0, content TEXT NOT NULL ); `); @@ -435,8 +450,8 @@ async function setupRuntimeExamples( const insertStatement = db.prepare( 'INSERT INTO examples(' + - 'title, summary, keywords, required_packages, related_concepts, related_tools, content' + - ') VALUES(?, ?, ?, ?, ?, ?, ?);', + 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' + + ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);', ); const frontmatterSchema = z.object({ @@ -446,6 +461,7 @@ async function setupRuntimeExamples( required_packages: z.array(z.string()).optional(), related_concepts: z.array(z.string()).optional(), related_tools: z.array(z.string()).optional(), + experimental: z.boolean().optional(), }); db.exec('BEGIN TRANSACTION'); @@ -464,8 +480,15 @@ async function setupRuntimeExamples( continue; } - const { title, summary, keywords, required_packages, related_concepts, related_tools } = - validation.data; + const { + title, + summary, + keywords, + required_packages, + related_concepts, + related_tools, + experimental, + } = validation.data; insertStatement.run( title, @@ -474,6 +497,7 @@ async function setupRuntimeExamples( JSON.stringify(required_packages ?? []), JSON.stringify(related_concepts ?? []), JSON.stringify(related_tools ?? []), + experimental ? 1 : 0, content, ); } diff --git a/tools/example_db_generator.js b/tools/example_db_generator.js index c85b63e497cf..784ff52128dd 100644 --- a/tools/example_db_generator.js +++ b/tools/example_db_generator.js @@ -84,6 +84,7 @@ function generate(inPath, outPath) { required_packages TEXT, related_concepts TEXT, related_tools TEXT, + experimental INTEGER NOT NULL DEFAULT 0, content TEXT NOT NULL ); `); @@ -120,8 +121,8 @@ function generate(inPath, outPath) { const insertStatement = db.prepare( 'INSERT INTO examples(' + - 'title, summary, keywords, required_packages, related_concepts, related_tools, content' + - ') VALUES(?, ?, ?, ?, ?, ?, ?);', + 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' + + ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);', ); const frontmatterSchema = z.object({ @@ -131,6 +132,7 @@ function generate(inPath, outPath) { required_packages: z.array(z.string()).optional(), related_concepts: z.array(z.string()).optional(), related_tools: z.array(z.string()).optional(), + experimental: z.boolean().optional(), }); db.exec('BEGIN TRANSACTION'); @@ -152,8 +154,15 @@ function generate(inPath, outPath) { throw new Error(`Invalid front matter in ${entry.name}`); } - const { title, summary, keywords, required_packages, related_concepts, related_tools } = - validation.data; + const { + title, + summary, + keywords, + required_packages, + related_concepts, + related_tools, + experimental, + } = validation.data; insertStatement.run( title, summary, @@ -161,6 +170,7 @@ function generate(inPath, outPath) { JSON.stringify(required_packages ?? []), JSON.stringify(related_concepts ?? []), JSON.stringify(related_tools ?? []), + experimental ? 1 : 0, content, ); } From 40dd17b70c20b24fafb701d3f852e290df5a92ea Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:16:47 -0400 Subject: [PATCH 3/3] fix(@angular/cli): add snippet support to example search MCP tool This commit enhances the `find_examples` tool by adding support for contextual snippets in the search results. Key changes: - The SQL query now uses the FTS5 `snippet()` function to generate a snippet of the matched content, with search terms highlighted. - The tool's output schema is updated with a new optional `snippet` field. - The description for the new `snippet` field is highly prescriptive, teaching the AI how to use snippets to enable more sophisticated and efficient agentic workflows, such as summarizing results for the user or performing internal evaluation before selecting the best result. --- .../cli/src/commands/mcp/tools/examples.ts | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/angular/cli/src/commands/mcp/tools/examples.ts b/packages/angular/cli/src/commands/mcp/tools/examples.ts index 364145f832ab..445ff04667e8 100644 --- a/packages/angular/cli/src/commands/mcp/tools/examples.ts +++ b/packages/angular/cli/src/commands/mcp/tools/examples.ts @@ -116,6 +116,18 @@ const findExampleOutputSchema = z.object({ 'A complete, self-contained Angular code example in Markdown format. This should be ' + 'presented to the user inside a markdown code block.', ), + snippet: z + .string() + .optional() + .describe( + 'A contextual snippet from the content showing the matched search term. This field is ' + + 'critical for efficiently evaluating a result`s relevance. It enables two primary ' + + 'workflows:\n\n' + + '1. For direct questions: You can internally review snippets to select the single best ' + + 'result before generating a comprehensive answer from its full `content`.\n' + + '2. For ambiguous or exploratory questions: You can present a summary of titles and ' + + 'snippets to the user, allowing them to guide the next step.', + ), }), ), }); @@ -196,7 +208,11 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext) // Build the query dynamically const params: SQLInputValue[] = []; let sql = - 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content FROM examples_fts'; + 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content, ' + + // The `snippet` function generates a contextual snippet of the matched text. + // Column 6 is the `content` column. We highlight matches with asterisks and limit the snippet size. + "snippet(examples_fts, 6, '**', '**', '...', 15) AS snippet " + + 'FROM examples_fts'; const whereClauses = []; // FTS query @@ -248,11 +264,16 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext) related_concepts: JSON.parse(record['related_concepts'] || '[]') as string[], related_tools: JSON.parse(record['related_tools'] || '[]') as string[], content: record['content'], + snippet: record['snippet'], }; examples.push(example); // Also create a more structured text output - const text = `## Example: ${example.title}\n**Summary:** ${example.summary}\n\n---\n\n${example.content}`; + let text = `## Example: ${example.title}\n**Summary:** ${example.summary}`; + if (example.snippet) { + text += `\n**Snippet:** ${example.snippet}`; + } + text += `\n\n---\n\n${example.content}`; textContent.push({ type: 'text' as const, text }); }