Skip to content

Commit def412a

Browse files
clydinalan-agius4
authored andcommitted
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.
1 parent af65d3a commit def412a

File tree

1 file changed

+106
-40
lines changed

1 file changed

+106
-40
lines changed

packages/angular/cli/src/commands/mcp/tools/examples.ts

Lines changed: 106 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,42 +13,103 @@ import { z } from 'zod';
1313
import { McpToolContext, declareTool } from './tool-registry';
1414

1515
const findExampleInputSchema = z.object({
16-
query: z.string().describe(
17-
`Performs a full-text search using FTS5 syntax. The query should target relevant Angular concepts.
18-
19-
Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):
20-
- AND (default): Space-separated terms are combined with AND.
21-
- Example: 'standalone component' (finds results with both "standalone" and "component")
22-
- OR: Use the OR operator to find results with either term.
23-
- Example: 'validation OR validator'
24-
- NOT: Use the NOT operator to exclude terms.
25-
- Example: 'forms NOT reactive'
26-
- Grouping: Use parentheses () to group expressions.
27-
- Example: '(validation OR validator) AND forms'
28-
- Phrase Search: Use double quotes "" for exact phrases.
29-
- Example: '"template-driven forms"'
30-
- Prefix Search: Use an asterisk * for prefix matching.
31-
- Example: 'rout*' (matches "route", "router", "routing")
32-
33-
Examples of queries:
34-
- Find standalone components: 'standalone component'
35-
- Find ngFor with trackBy: 'ngFor trackBy'
36-
- Find signal inputs: 'signal input'
37-
- Find lazy loading a route: 'lazy load route'
38-
- Find forms with validation: 'form AND (validation OR validator)'`,
39-
),
40-
keywords: z.array(z.string()).optional().describe('Filter examples by specific keywords.'),
16+
query: z
17+
.string()
18+
.describe(
19+
`The primary, conceptual search query. This should capture the user's main goal or question ` +
20+
`(e.g., 'lazy loading a route' or 'how to use signal inputs'). The query will be processed ` +
21+
'by a powerful full-text search engine.\n\n' +
22+
'Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):\n' +
23+
' - AND (default): Space-separated terms are combined with AND.\n' +
24+
' - Example: \'standalone component\' (finds results with both "standalone" and "component")\n' +
25+
' - OR: Use the OR operator to find results with either term.\n' +
26+
" - Example: 'validation OR validator'\n" +
27+
' - NOT: Use the NOT operator to exclude terms.\n' +
28+
" - Example: 'forms NOT reactive'\n" +
29+
' - Grouping: Use parentheses () to group expressions.\n' +
30+
" - Example: '(validation OR validator) AND forms'\n" +
31+
' - Phrase Search: Use double quotes "" for exact phrases.\n' +
32+
' - Example: \'"template-driven forms"\'\n' +
33+
' - Prefix Search: Use an asterisk * for prefix matching.\n' +
34+
' - Example: \'rout*\' (matches "route", "router", "routing")',
35+
),
36+
keywords: z
37+
.array(z.string())
38+
.optional()
39+
.describe(
40+
'A list of specific, exact keywords to narrow the search. Use this for precise terms like ' +
41+
'API names, function names, or decorators (e.g., `ngFor`, `trackBy`, `inject`).',
42+
),
4143
required_packages: z
4244
.array(z.string())
4345
.optional()
44-
.describe('Filter examples by required NPM packages (e.g., "@angular/forms").'),
46+
.describe(
47+
"A list of NPM packages that an example must use. Use this when the user's request is " +
48+
'specific to a feature within a certain package (e.g., if the user asks about `ngModel`, ' +
49+
'you should filter by `@angular/forms`).',
50+
),
4551
related_concepts: z
4652
.array(z.string())
4753
.optional()
48-
.describe('Filter examples by related high-level concepts.'),
54+
.describe(
55+
'A list of high-level concepts to filter by. Use this to find examples related to broader ' +
56+
'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).',
57+
),
4958
});
59+
5060
type FindExampleInput = z.infer<typeof findExampleInputSchema>;
5161

62+
const findExampleOutputSchema = z.object({
63+
examples: z.array(
64+
z.object({
65+
title: z
66+
.string()
67+
.describe(
68+
'The title of the example. Use this as a heading when presenting the example to the user.',
69+
),
70+
summary: z
71+
.string()
72+
.describe(
73+
"A one-sentence summary of the example's purpose. Use this to help the user decide " +
74+
'if the example is relevant to them.',
75+
),
76+
keywords: z
77+
.array(z.string())
78+
.optional()
79+
.describe(
80+
'A list of keywords for the example. You can use these to explain why this example ' +
81+
"was a good match for the user's query.",
82+
),
83+
required_packages: z
84+
.array(z.string())
85+
.optional()
86+
.describe(
87+
'A list of NPM packages required for the example to work. Before presenting the code, ' +
88+
'you should inform the user if any of these packages need to be installed.',
89+
),
90+
related_concepts: z
91+
.array(z.string())
92+
.optional()
93+
.describe(
94+
'A list of related concepts. You can suggest these to the user as topics for ' +
95+
'follow-up questions.',
96+
),
97+
related_tools: z
98+
.array(z.string())
99+
.optional()
100+
.describe(
101+
'A list of related MCP tools. You can suggest these as potential next steps for the user.',
102+
),
103+
content: z
104+
.string()
105+
.describe(
106+
'A complete, self-contained Angular code example in Markdown format. This should be ' +
107+
'presented to the user inside a markdown code block.',
108+
),
109+
}),
110+
),
111+
});
112+
52113
export const FIND_EXAMPLE_TOOL = declareTool({
53114
name: 'find_examples',
54115
title: 'Find Angular Code Examples',
@@ -80,15 +141,7 @@ new or evolving features.
80141
and 'related_concepts' to create highly specific searches.
81142
</Operational Notes>`,
82143
inputSchema: findExampleInputSchema.shape,
83-
outputSchema: {
84-
examples: z.array(
85-
z.object({
86-
content: z
87-
.string()
88-
.describe('A complete, self-contained Angular code example in Markdown format.'),
89-
}),
90-
),
91-
},
144+
outputSchema: findExampleOutputSchema.shape,
92145
isReadOnly: true,
93146
isLocalOnly: true,
94147
shouldRegister: ({ logger }) => {
@@ -132,7 +185,8 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext)
132185

133186
// Build the query dynamically
134187
const params: SQLInputValue[] = [];
135-
let sql = 'SELECT content FROM examples_fts';
188+
let sql =
189+
'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content FROM examples_fts';
136190
const whereClauses = [];
137191

138192
// FTS query
@@ -171,9 +225,21 @@ async function createFindExampleHandler({ exampleDatabasePath }: McpToolContext)
171225
const examples = [];
172226
const textContent = [];
173227
for (const exampleRecord of queryStatement.all(...params)) {
174-
const exampleContent = exampleRecord['content'] as string;
175-
examples.push({ content: exampleContent });
176-
textContent.push({ type: 'text' as const, text: exampleContent });
228+
const record = exampleRecord as Record<string, string>;
229+
const example = {
230+
title: record['title'],
231+
summary: record['summary'],
232+
keywords: JSON.parse(record['keywords'] || '[]') as string[],
233+
required_packages: JSON.parse(record['required_packages'] || '[]') as string[],
234+
related_concepts: JSON.parse(record['related_concepts'] || '[]') as string[],
235+
related_tools: JSON.parse(record['related_tools'] || '[]') as string[],
236+
content: record['content'],
237+
};
238+
examples.push(example);
239+
240+
// Also create a more structured text output
241+
const text = `## Example: ${example.title}\n**Summary:** ${example.summary}\n\n---\n\n${example.content}`;
242+
textContent.push({ type: 'text' as const, text });
177243
}
178244

179245
return {

0 commit comments

Comments
 (0)