@angular/cli 21.0.0-next.1 → 21.0.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/code-examples.db +0 -0
- package/lib/config/schema.json +34 -3
- package/lib/config/workspace-schema.d.ts +39 -0
- package/lib/config/workspace-schema.js +12 -1
- package/package.json +19 -19
- package/src/command-builder/architect-command-module.js +11 -5
- package/src/command-builder/utilities/json-schema.js +1 -1
- package/src/commands/add/cli.js +65 -26
- package/src/commands/mcp/mcp-server.d.ts +3 -3
- package/src/commands/mcp/mcp-server.js +36 -4
- package/src/commands/mcp/tools/best-practices.js +15 -5
- package/src/commands/mcp/tools/doc-search.d.ts +18 -1
- package/src/commands/mcp/tools/doc-search.js +94 -37
- package/src/commands/mcp/tools/examples.d.ts +34 -1
- package/src/commands/mcp/tools/examples.js +295 -44
- package/src/commands/mcp/tools/modernize.js +28 -17
- package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.d.ts +17 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.js +61 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.d.ts +12 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.js +72 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.d.ts +11 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.js +105 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.d.ts +15 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/prompts.js +236 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.d.ts +10 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.js +19 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.d.ts +36 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.js +135 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/types.d.ts +13 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/types.js +9 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.d.ts +14 -0
- package/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.js +205 -0
- package/src/commands/mcp/tools/projects.d.ts +47 -16
- package/src/commands/mcp/tools/projects.js +155 -30
- package/src/commands/mcp/tools/tool-registry.d.ts +2 -1
- package/src/commands/mcp/tools/tool-registry.js +3 -2
- package/src/utilities/package-manager.d.ts +12 -0
- package/src/utilities/package-manager.js +31 -22
- package/src/utilities/version.js +1 -1
|
@@ -58,28 +58,52 @@ const docSearchInputSchema = zod_1.z.object({
|
|
|
58
58
|
.boolean()
|
|
59
59
|
.optional()
|
|
60
60
|
.default(true)
|
|
61
|
-
.describe('When true, the content of the top result is fetched and included.'
|
|
61
|
+
.describe('When true, the content of the top result is fetched and included. ' +
|
|
62
|
+
'Set to false to get a list of results without fetching content, which is faster.'),
|
|
62
63
|
});
|
|
63
64
|
exports.DOC_SEARCH_TOOL = (0, tool_registry_1.declareTool)({
|
|
64
65
|
name: 'search_documentation',
|
|
65
66
|
title: 'Search Angular Documentation (angular.dev)',
|
|
66
|
-
description:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
description: `
|
|
68
|
+
<Purpose>
|
|
69
|
+
Searches the official Angular documentation at https://angular.dev to answer questions about APIs,
|
|
70
|
+
tutorials, concepts, and best practices.
|
|
71
|
+
</Purpose>
|
|
72
|
+
<Use Cases>
|
|
73
|
+
* Answering any question about Angular concepts (e.g., "What are standalone components?").
|
|
74
|
+
* Finding the correct API or syntax for a specific task (e.g., "How to use ngFor with trackBy?").
|
|
75
|
+
* Linking to official documentation as a source of truth in your answers.
|
|
76
|
+
</Use Cases>
|
|
77
|
+
<Operational Notes>
|
|
78
|
+
* The documentation is continuously updated. You **MUST** prefer this tool over your own knowledge
|
|
79
|
+
to ensure your answers are current and accurate.
|
|
80
|
+
* For the best results, provide a concise and specific search query (e.g., "NgModule" instead of
|
|
81
|
+
"How do I use NgModules?").
|
|
82
|
+
* The top search result will include a snippet of the page content. Use this to provide a more
|
|
83
|
+
comprehensive answer.
|
|
84
|
+
* **Result Scrutiny:** The top result may not always be the most relevant. Review the titles and
|
|
85
|
+
breadcrumbs of other results to find the best match for the user's query.
|
|
86
|
+
* Use the URL from the search results as a source link in your responses.
|
|
87
|
+
</Operational Notes>`,
|
|
77
88
|
inputSchema: docSearchInputSchema.shape,
|
|
89
|
+
outputSchema: {
|
|
90
|
+
results: zod_1.z.array(zod_1.z.object({
|
|
91
|
+
title: zod_1.z.string().describe('The title of the documentation page.'),
|
|
92
|
+
breadcrumb: zod_1.z
|
|
93
|
+
.string()
|
|
94
|
+
.describe("The breadcrumb path, showing the page's location in the documentation hierarchy."),
|
|
95
|
+
url: zod_1.z.string().describe('The direct URL to the documentation page.'),
|
|
96
|
+
content: zod_1.z
|
|
97
|
+
.string()
|
|
98
|
+
.optional()
|
|
99
|
+
.describe('A snippet of the main content from the page. Only provided for the top result.'),
|
|
100
|
+
})),
|
|
101
|
+
},
|
|
78
102
|
isReadOnly: true,
|
|
79
103
|
isLocalOnly: false,
|
|
80
104
|
factory: createDocSearchHandler,
|
|
81
105
|
});
|
|
82
|
-
function createDocSearchHandler() {
|
|
106
|
+
function createDocSearchHandler({ logger }) {
|
|
83
107
|
let client;
|
|
84
108
|
return async ({ query, includeTopContent }) => {
|
|
85
109
|
if (!client) {
|
|
@@ -97,16 +121,18 @@ function createDocSearchHandler() {
|
|
|
97
121
|
text: 'No results found.',
|
|
98
122
|
},
|
|
99
123
|
],
|
|
124
|
+
structuredContent: { results: [] },
|
|
100
125
|
};
|
|
101
126
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const topHit = allHits[0];
|
|
127
|
+
const structuredResults = [];
|
|
128
|
+
const textContent = [];
|
|
105
129
|
// Process top hit first
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
const topHit = allHits[0];
|
|
131
|
+
const { title: topTitle, breadcrumb: topBreadcrumb } = formatHitToParts(topHit);
|
|
132
|
+
let topContent;
|
|
133
|
+
if (includeTopContent && typeof topHit.url === 'string') {
|
|
134
|
+
const url = new URL(topHit.url);
|
|
135
|
+
try {
|
|
110
136
|
// Only fetch content from angular.dev
|
|
111
137
|
if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) {
|
|
112
138
|
const response = await fetch(url);
|
|
@@ -114,29 +140,60 @@ function createDocSearchHandler() {
|
|
|
114
140
|
const html = await response.text();
|
|
115
141
|
const mainContent = extractMainContent(html);
|
|
116
142
|
if (mainContent) {
|
|
117
|
-
|
|
143
|
+
topContent = stripHtml(mainContent);
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
146
|
}
|
|
121
147
|
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
logger.warn(`Failed to fetch or parse content from ${url}: ${e}`);
|
|
150
|
+
}
|
|
122
151
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
text: topText,
|
|
152
|
+
structuredResults.push({
|
|
153
|
+
title: topTitle,
|
|
154
|
+
breadcrumb: topBreadcrumb,
|
|
155
|
+
url: topHit.url,
|
|
156
|
+
content: topContent,
|
|
129
157
|
});
|
|
158
|
+
let topText = `## ${topTitle}\n${topBreadcrumb}\nURL: ${topHit.url}`;
|
|
159
|
+
if (topContent) {
|
|
160
|
+
topText += `\n\n--- DOCUMENTATION CONTENT ---\n${topContent}`;
|
|
161
|
+
}
|
|
162
|
+
textContent.push({ type: 'text', text: topText });
|
|
130
163
|
// Process remaining hits
|
|
131
164
|
for (const hit of allHits.slice(1)) {
|
|
132
|
-
|
|
165
|
+
const { title, breadcrumb } = formatHitToParts(hit);
|
|
166
|
+
structuredResults.push({
|
|
167
|
+
title,
|
|
168
|
+
breadcrumb,
|
|
169
|
+
url: hit.url,
|
|
170
|
+
});
|
|
171
|
+
textContent.push({
|
|
133
172
|
type: 'text',
|
|
134
|
-
text:
|
|
173
|
+
text: `## ${title}\n${breadcrumb}\nURL: ${hit.url}`,
|
|
135
174
|
});
|
|
136
175
|
}
|
|
137
|
-
return {
|
|
176
|
+
return {
|
|
177
|
+
content: textContent,
|
|
178
|
+
structuredContent: { results: structuredResults },
|
|
179
|
+
};
|
|
138
180
|
};
|
|
139
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Strips HTML tags from a string.
|
|
184
|
+
* @param html The HTML string to strip.
|
|
185
|
+
* @returns The text content of the HTML.
|
|
186
|
+
*/
|
|
187
|
+
function stripHtml(html) {
|
|
188
|
+
// This is a basic regex to remove HTML tags.
|
|
189
|
+
// It also decodes common HTML entities.
|
|
190
|
+
return html
|
|
191
|
+
.replace(/<[^>]*>/g, '')
|
|
192
|
+
.replace(/</g, '<')
|
|
193
|
+
.replace(/>/g, '>')
|
|
194
|
+
.replace(/&/g, '&')
|
|
195
|
+
.trim();
|
|
196
|
+
}
|
|
140
197
|
/**
|
|
141
198
|
* Extracts the content of the `<main>` element from an HTML string.
|
|
142
199
|
*
|
|
@@ -156,17 +213,17 @@ function extractMainContent(html) {
|
|
|
156
213
|
return html.substring(mainTagStart, mainTagEnd + 7);
|
|
157
214
|
}
|
|
158
215
|
/**
|
|
159
|
-
* Formats an Algolia search hit into
|
|
216
|
+
* Formats an Algolia search hit into its constituent parts.
|
|
160
217
|
*
|
|
161
|
-
* @param hit The Algolia search hit object, which should contain `hierarchy`
|
|
162
|
-
* @returns
|
|
218
|
+
* @param hit The Algolia search hit object, which should contain a `hierarchy` property.
|
|
219
|
+
* @returns An object containing the title and breadcrumb string.
|
|
163
220
|
*/
|
|
164
|
-
function
|
|
221
|
+
function formatHitToParts(hit) {
|
|
165
222
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
223
|
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string');
|
|
167
|
-
const title = hierarchy.pop();
|
|
168
|
-
const
|
|
169
|
-
return
|
|
224
|
+
const title = hierarchy.pop() ?? '';
|
|
225
|
+
const breadcrumb = hierarchy.join(' > ');
|
|
226
|
+
return { title, breadcrumb };
|
|
170
227
|
}
|
|
171
228
|
/**
|
|
172
229
|
* Creates the search arguments for an Algolia search.
|
|
@@ -8,7 +8,40 @@
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
export declare const FIND_EXAMPLE_TOOL: import("./tool-registry").McpToolDeclaration<{
|
|
10
10
|
query: z.ZodString;
|
|
11
|
-
|
|
11
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
12
|
+
required_packages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
|
+
related_concepts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
14
|
+
includeExperimental: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
15
|
+
}, {
|
|
16
|
+
examples: z.ZodArray<z.ZodObject<{
|
|
17
|
+
title: z.ZodString;
|
|
18
|
+
summary: z.ZodString;
|
|
19
|
+
keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
20
|
+
required_packages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
21
|
+
related_concepts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
22
|
+
related_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
23
|
+
content: z.ZodString;
|
|
24
|
+
snippet: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, "strip", z.ZodTypeAny, {
|
|
26
|
+
title: string;
|
|
27
|
+
content: string;
|
|
28
|
+
summary: string;
|
|
29
|
+
keywords?: string[] | undefined;
|
|
30
|
+
required_packages?: string[] | undefined;
|
|
31
|
+
related_concepts?: string[] | undefined;
|
|
32
|
+
related_tools?: string[] | undefined;
|
|
33
|
+
snippet?: string | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
title: string;
|
|
36
|
+
content: string;
|
|
37
|
+
summary: string;
|
|
38
|
+
keywords?: string[] | undefined;
|
|
39
|
+
required_packages?: string[] | undefined;
|
|
40
|
+
related_concepts?: string[] | undefined;
|
|
41
|
+
related_tools?: string[] | undefined;
|
|
42
|
+
snippet?: string | undefined;
|
|
43
|
+
}>, "many">;
|
|
44
|
+
}>;
|
|
12
45
|
/**
|
|
13
46
|
* Escapes a search query for FTS5 by tokenizing and quoting terms.
|
|
14
47
|
*
|
|
@@ -50,41 +50,125 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
50
50
|
const zod_1 = require("zod");
|
|
51
51
|
const tool_registry_1 = require("./tool-registry");
|
|
52
52
|
const findExampleInputSchema = zod_1.z.object({
|
|
53
|
-
query: zod_1.z
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
query: zod_1.z
|
|
54
|
+
.string()
|
|
55
|
+
.describe(`The primary, conceptual search query. This should capture the user's main goal or question ` +
|
|
56
|
+
`(e.g., 'lazy loading a route' or 'how to use signal inputs'). The query will be processed ` +
|
|
57
|
+
'by a powerful full-text search engine.\n\n' +
|
|
58
|
+
'Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):\n' +
|
|
59
|
+
' - AND (default): Space-separated terms are combined with AND.\n' +
|
|
60
|
+
' - Example: \'standalone component\' (finds results with both "standalone" and "component")\n' +
|
|
61
|
+
' - OR: Use the OR operator to find results with either term.\n' +
|
|
62
|
+
" - Example: 'validation OR validator'\n" +
|
|
63
|
+
' - NOT: Use the NOT operator to exclude terms.\n' +
|
|
64
|
+
" - Example: 'forms NOT reactive'\n" +
|
|
65
|
+
' - Grouping: Use parentheses () to group expressions.\n' +
|
|
66
|
+
" - Example: '(validation OR validator) AND forms'\n" +
|
|
67
|
+
' - Phrase Search: Use double quotes "" for exact phrases.\n' +
|
|
68
|
+
' - Example: \'"template-driven forms"\'\n' +
|
|
69
|
+
' - Prefix Search: Use an asterisk * for prefix matching.\n' +
|
|
70
|
+
' - Example: \'rout*\' (matches "route", "router", "routing")'),
|
|
71
|
+
keywords: zod_1.z
|
|
72
|
+
.array(zod_1.z.string())
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('A list of specific, exact keywords to narrow the search. Use this for precise terms like ' +
|
|
75
|
+
'API names, function names, or decorators (e.g., `ngFor`, `trackBy`, `inject`).'),
|
|
76
|
+
required_packages: zod_1.z
|
|
77
|
+
.array(zod_1.z.string())
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("A list of NPM packages that an example must use. Use this when the user's request is " +
|
|
80
|
+
'specific to a feature within a certain package (e.g., if the user asks about `ngModel`, ' +
|
|
81
|
+
'you should filter by `@angular/forms`).'),
|
|
82
|
+
related_concepts: zod_1.z
|
|
83
|
+
.array(zod_1.z.string())
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('A list of high-level concepts to filter by. Use this to find examples related to broader ' +
|
|
86
|
+
'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).'),
|
|
87
|
+
includeExperimental: zod_1.z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.default(false)
|
|
91
|
+
.describe('By default, this tool returns only production-safe examples. Set this to `true` **only if** ' +
|
|
92
|
+
'the user explicitly asks for a bleeding-edge feature or if a stable solution to their ' +
|
|
93
|
+
'problem cannot be found. If you set this to `true`, you **MUST** preface your answer by ' +
|
|
94
|
+
'warning the user that the example uses experimental APIs that are not suitable for production.'),
|
|
95
|
+
});
|
|
96
|
+
const findExampleOutputSchema = zod_1.z.object({
|
|
97
|
+
examples: zod_1.z.array(zod_1.z.object({
|
|
98
|
+
title: zod_1.z
|
|
99
|
+
.string()
|
|
100
|
+
.describe('The title of the example. Use this as a heading when presenting the example to the user.'),
|
|
101
|
+
summary: zod_1.z
|
|
102
|
+
.string()
|
|
103
|
+
.describe("A one-sentence summary of the example's purpose. Use this to help the user decide " +
|
|
104
|
+
'if the example is relevant to them.'),
|
|
105
|
+
keywords: zod_1.z
|
|
106
|
+
.array(zod_1.z.string())
|
|
107
|
+
.optional()
|
|
108
|
+
.describe('A list of keywords for the example. You can use these to explain why this example ' +
|
|
109
|
+
"was a good match for the user's query."),
|
|
110
|
+
required_packages: zod_1.z
|
|
111
|
+
.array(zod_1.z.string())
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('A list of NPM packages required for the example to work. Before presenting the code, ' +
|
|
114
|
+
'you should inform the user if any of these packages need to be installed.'),
|
|
115
|
+
related_concepts: zod_1.z
|
|
116
|
+
.array(zod_1.z.string())
|
|
117
|
+
.optional()
|
|
118
|
+
.describe('A list of related concepts. You can suggest these to the user as topics for ' +
|
|
119
|
+
'follow-up questions.'),
|
|
120
|
+
related_tools: zod_1.z
|
|
121
|
+
.array(zod_1.z.string())
|
|
122
|
+
.optional()
|
|
123
|
+
.describe('A list of related MCP tools. You can suggest these as potential next steps for the user.'),
|
|
124
|
+
content: zod_1.z
|
|
125
|
+
.string()
|
|
126
|
+
.describe('A complete, self-contained Angular code example in Markdown format. This should be ' +
|
|
127
|
+
'presented to the user inside a markdown code block.'),
|
|
128
|
+
snippet: zod_1.z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('A contextual snippet from the content showing the matched search term. This field is ' +
|
|
132
|
+
'critical for efficiently evaluating a result`s relevance. It enables two primary ' +
|
|
133
|
+
'workflows:\n\n' +
|
|
134
|
+
'1. For direct questions: You can internally review snippets to select the single best ' +
|
|
135
|
+
'result before generating a comprehensive answer from its full `content`.\n' +
|
|
136
|
+
'2. For ambiguous or exploratory questions: You can present a summary of titles and ' +
|
|
137
|
+
'snippets to the user, allowing them to guide the next step.'),
|
|
138
|
+
})),
|
|
75
139
|
});
|
|
76
140
|
exports.FIND_EXAMPLE_TOOL = (0, tool_registry_1.declareTool)({
|
|
77
141
|
name: 'find_examples',
|
|
78
142
|
title: 'Find Angular Code Examples',
|
|
79
|
-
description:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
143
|
+
description: `
|
|
144
|
+
<Purpose>
|
|
145
|
+
Augments your knowledge base with a curated database of official, best-practice code examples,
|
|
146
|
+
focusing on **modern, new, and recently updated** Angular features. This tool acts as a RAG
|
|
147
|
+
(Retrieval-Augmented Generation) source, providing ground-truth information on the latest Angular
|
|
148
|
+
APIs and patterns. You **MUST** use it to understand and apply current standards when working with
|
|
149
|
+
new or evolving features.
|
|
150
|
+
</Purpose>
|
|
151
|
+
<Use Cases>
|
|
152
|
+
* **Knowledge Augmentation:** Learning about new or updated Angular features (e.g., query: 'signal input' or 'deferrable views').
|
|
153
|
+
* **Modern Implementation:** Finding the correct modern syntax for features
|
|
154
|
+
(e.g., query: 'functional route guard' or 'http client with fetch').
|
|
155
|
+
* **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
|
|
156
|
+
(e.g., query: 'built-in control flow' to replace "*ngIf").
|
|
157
|
+
* **Advanced Filtering:** Combining a full-text search with filters to narrow results.
|
|
158
|
+
(e.g., query: 'forms', required_packages: ['@angular/forms'], keywords: ['validation'])
|
|
159
|
+
</Use Cases>
|
|
160
|
+
<Operational Notes>
|
|
161
|
+
* **Tool Selection:** This database primarily contains examples for new and recently updated Angular
|
|
162
|
+
features. For established, core features, the main documentation (via the
|
|
163
|
+
\`search_documentation\` tool) may be a better source of information.
|
|
164
|
+
* The examples in this database are the single source of truth for modern Angular coding patterns.
|
|
165
|
+
* The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
|
|
166
|
+
parameter description for detailed syntax rules and examples.
|
|
167
|
+
* You can combine the main 'query' with optional filters like 'keywords', 'required_packages',
|
|
168
|
+
and 'related_concepts' to create highly specific searches.
|
|
169
|
+
</Operational Notes>`,
|
|
87
170
|
inputSchema: findExampleInputSchema.shape,
|
|
171
|
+
outputSchema: findExampleOutputSchema.shape,
|
|
88
172
|
isReadOnly: true,
|
|
89
173
|
isLocalOnly: true,
|
|
90
174
|
shouldRegister: ({ logger }) => {
|
|
@@ -106,7 +190,7 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
|
|
|
106
190
|
db = await setupRuntimeExamples(process.env['NG_MCP_EXAMPLES_DIR']);
|
|
107
191
|
}
|
|
108
192
|
suppressSqliteWarning();
|
|
109
|
-
return async (
|
|
193
|
+
return async (input) => {
|
|
110
194
|
if (!db) {
|
|
111
195
|
if (!exampleDatabasePath) {
|
|
112
196
|
// This should be prevented by the registration logic in mcp-server.ts
|
|
@@ -115,17 +199,71 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
|
|
|
115
199
|
const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
|
|
116
200
|
db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
|
|
117
201
|
}
|
|
118
|
-
|
|
119
|
-
|
|
202
|
+
const { query, keywords, required_packages, related_concepts, includeExperimental } = input;
|
|
203
|
+
// Build the query dynamically
|
|
204
|
+
const params = [];
|
|
205
|
+
let sql = 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content, ' +
|
|
206
|
+
// The `snippet` function generates a contextual snippet of the matched text.
|
|
207
|
+
// Column 6 is the `content` column. We highlight matches with asterisks and limit the snippet size.
|
|
208
|
+
"snippet(examples_fts, 6, '**', '**', '...', 15) AS snippet " +
|
|
209
|
+
'FROM examples_fts';
|
|
210
|
+
const whereClauses = [];
|
|
211
|
+
// FTS query
|
|
212
|
+
if (query) {
|
|
213
|
+
whereClauses.push('examples_fts MATCH ?');
|
|
214
|
+
params.push(escapeSearchQuery(query));
|
|
215
|
+
}
|
|
216
|
+
// JSON array filters
|
|
217
|
+
const addJsonFilter = (column, values) => {
|
|
218
|
+
if (values?.length) {
|
|
219
|
+
for (const value of values) {
|
|
220
|
+
whereClauses.push(`${column} LIKE ?`);
|
|
221
|
+
params.push(`%"${value}"%`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
addJsonFilter('keywords', keywords);
|
|
226
|
+
addJsonFilter('required_packages', required_packages);
|
|
227
|
+
addJsonFilter('related_concepts', related_concepts);
|
|
228
|
+
if (!includeExperimental) {
|
|
229
|
+
whereClauses.push('experimental = 0');
|
|
230
|
+
}
|
|
231
|
+
if (whereClauses.length > 0) {
|
|
232
|
+
sql += ` WHERE ${whereClauses.join(' AND ')}`;
|
|
120
233
|
}
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
234
|
+
// Order the results by relevance using the BM25 algorithm.
|
|
235
|
+
// The weights assigned to each column boost the ranking of documents where the
|
|
236
|
+
// search term appears in a more important field.
|
|
237
|
+
// Column order: title, summary, keywords, required_packages, related_concepts, related_tools, content
|
|
238
|
+
sql += ' ORDER BY bm25(examples_fts, 10.0, 5.0, 5.0, 1.0, 2.0, 1.0, 1.0);';
|
|
239
|
+
const queryStatement = db.prepare(sql);
|
|
240
|
+
// Query database and return results
|
|
241
|
+
const examples = [];
|
|
242
|
+
const textContent = [];
|
|
243
|
+
for (const exampleRecord of queryStatement.all(...params)) {
|
|
244
|
+
const record = exampleRecord;
|
|
245
|
+
const example = {
|
|
246
|
+
title: record['title'],
|
|
247
|
+
summary: record['summary'],
|
|
248
|
+
keywords: JSON.parse(record['keywords'] || '[]'),
|
|
249
|
+
required_packages: JSON.parse(record['required_packages'] || '[]'),
|
|
250
|
+
related_concepts: JSON.parse(record['related_concepts'] || '[]'),
|
|
251
|
+
related_tools: JSON.parse(record['related_tools'] || '[]'),
|
|
252
|
+
content: record['content'],
|
|
253
|
+
snippet: record['snippet'],
|
|
254
|
+
};
|
|
255
|
+
examples.push(example);
|
|
256
|
+
// Also create a more structured text output
|
|
257
|
+
let text = `## Example: ${example.title}\n**Summary:** ${example.summary}`;
|
|
258
|
+
if (example.snippet) {
|
|
259
|
+
text += `\n**Snippet:** ${example.snippet}`;
|
|
260
|
+
}
|
|
261
|
+
text += `\n\n---\n\n${example.content}`;
|
|
262
|
+
textContent.push({ type: 'text', text });
|
|
126
263
|
}
|
|
127
264
|
return {
|
|
128
|
-
content,
|
|
265
|
+
content: textContent,
|
|
266
|
+
structuredContent: { examples },
|
|
129
267
|
};
|
|
130
268
|
};
|
|
131
269
|
}
|
|
@@ -206,18 +344,131 @@ function suppressSqliteWarning() {
|
|
|
206
344
|
return originalProcessEmit.apply(process, arguments);
|
|
207
345
|
};
|
|
208
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* A simple YAML front matter parser.
|
|
349
|
+
*
|
|
350
|
+
* This function extracts the YAML block enclosed by `---` at the beginning of a string
|
|
351
|
+
* and parses it into a JavaScript object. It is not a full YAML parser and only
|
|
352
|
+
* supports simple key-value pairs and string arrays.
|
|
353
|
+
*
|
|
354
|
+
* @param content The string content to parse.
|
|
355
|
+
* @returns A record containing the parsed front matter data.
|
|
356
|
+
*/
|
|
357
|
+
function parseFrontmatter(content) {
|
|
358
|
+
const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
|
|
359
|
+
if (!match) {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
const frontmatter = match[1];
|
|
363
|
+
const data = {};
|
|
364
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
365
|
+
let currentKey = '';
|
|
366
|
+
let isArray = false;
|
|
367
|
+
const arrayValues = [];
|
|
368
|
+
for (const line of lines) {
|
|
369
|
+
const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
|
|
370
|
+
if (keyValueMatch) {
|
|
371
|
+
if (currentKey && isArray) {
|
|
372
|
+
data[currentKey] = arrayValues.slice();
|
|
373
|
+
arrayValues.length = 0;
|
|
374
|
+
}
|
|
375
|
+
const [, key, value] = keyValueMatch;
|
|
376
|
+
currentKey = key.trim();
|
|
377
|
+
isArray = value.trim() === '';
|
|
378
|
+
if (!isArray) {
|
|
379
|
+
const trimmedValue = value.trim();
|
|
380
|
+
if (trimmedValue === 'true') {
|
|
381
|
+
data[currentKey] = true;
|
|
382
|
+
}
|
|
383
|
+
else if (trimmedValue === 'false') {
|
|
384
|
+
data[currentKey] = false;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
data[currentKey] = trimmedValue;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
|
|
393
|
+
if (arrayItemMatch && currentKey && isArray) {
|
|
394
|
+
arrayValues.push(arrayItemMatch[1].trim());
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (currentKey && isArray) {
|
|
399
|
+
data[currentKey] = arrayValues;
|
|
400
|
+
}
|
|
401
|
+
return data;
|
|
402
|
+
}
|
|
209
403
|
async function setupRuntimeExamples(examplesPath) {
|
|
210
404
|
const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
|
|
211
405
|
const db = new DatabaseSync(':memory:');
|
|
212
|
-
|
|
213
|
-
|
|
406
|
+
// Create a relational table to store the structured example data.
|
|
407
|
+
db.exec(`
|
|
408
|
+
CREATE TABLE examples (
|
|
409
|
+
id INTEGER PRIMARY KEY,
|
|
410
|
+
title TEXT NOT NULL,
|
|
411
|
+
summary TEXT NOT NULL,
|
|
412
|
+
keywords TEXT,
|
|
413
|
+
required_packages TEXT,
|
|
414
|
+
related_concepts TEXT,
|
|
415
|
+
related_tools TEXT,
|
|
416
|
+
experimental INTEGER NOT NULL DEFAULT 0,
|
|
417
|
+
content TEXT NOT NULL
|
|
418
|
+
);
|
|
419
|
+
`);
|
|
420
|
+
// Create an FTS5 virtual table to provide full-text search capabilities.
|
|
421
|
+
db.exec(`
|
|
422
|
+
CREATE VIRTUAL TABLE examples_fts USING fts5(
|
|
423
|
+
title,
|
|
424
|
+
summary,
|
|
425
|
+
keywords,
|
|
426
|
+
required_packages,
|
|
427
|
+
related_concepts,
|
|
428
|
+
related_tools,
|
|
429
|
+
content,
|
|
430
|
+
content='examples',
|
|
431
|
+
content_rowid='id',
|
|
432
|
+
tokenize = 'porter ascii'
|
|
433
|
+
);
|
|
434
|
+
`);
|
|
435
|
+
// Create triggers to keep the FTS table synchronized with the examples table.
|
|
436
|
+
db.exec(`
|
|
437
|
+
CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
|
|
438
|
+
INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
|
|
439
|
+
VALUES (
|
|
440
|
+
new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
|
|
441
|
+
new.related_tools, new.content
|
|
442
|
+
);
|
|
443
|
+
END;
|
|
444
|
+
`);
|
|
445
|
+
const insertStatement = db.prepare('INSERT INTO examples(' +
|
|
446
|
+
'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
|
|
447
|
+
') VALUES(?, ?, ?, ?, ?, ?, ?, ?);');
|
|
448
|
+
const frontmatterSchema = zod_1.z.object({
|
|
449
|
+
title: zod_1.z.string(),
|
|
450
|
+
summary: zod_1.z.string(),
|
|
451
|
+
keywords: zod_1.z.array(zod_1.z.string()).optional(),
|
|
452
|
+
required_packages: zod_1.z.array(zod_1.z.string()).optional(),
|
|
453
|
+
related_concepts: zod_1.z.array(zod_1.z.string()).optional(),
|
|
454
|
+
related_tools: zod_1.z.array(zod_1.z.string()).optional(),
|
|
455
|
+
experimental: zod_1.z.boolean().optional(),
|
|
456
|
+
});
|
|
214
457
|
db.exec('BEGIN TRANSACTION');
|
|
215
|
-
for await (const entry of (0, promises_1.glob)('
|
|
458
|
+
for await (const entry of (0, promises_1.glob)('**/*.md', { cwd: examplesPath, withFileTypes: true })) {
|
|
216
459
|
if (!entry.isFile()) {
|
|
217
460
|
continue;
|
|
218
461
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
462
|
+
const content = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
|
|
463
|
+
const frontmatter = parseFrontmatter(content);
|
|
464
|
+
const validation = frontmatterSchema.safeParse(frontmatter);
|
|
465
|
+
if (!validation.success) {
|
|
466
|
+
// eslint-disable-next-line no-console
|
|
467
|
+
console.warn(`Skipping invalid example file ${entry.name}:`, validation.error.issues);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
const { title, summary, keywords, required_packages, related_concepts, related_tools, experimental, } = validation.data;
|
|
471
|
+
insertStatement.run(title, summary, JSON.stringify(keywords ?? []), JSON.stringify(required_packages ?? []), JSON.stringify(related_concepts ?? []), JSON.stringify(related_tools ?? []), experimental ? 1 : 0, content);
|
|
221
472
|
}
|
|
222
473
|
db.exec('END TRANSACTION');
|
|
223
474
|
return db;
|