Skip to content

Commit 286b620

Browse files
committed
feat(@angular/cli): make documentation search tool version-aware
Implements version-aware search mechanism for the `search_documentation` MCP tool. The tool is now forward-compatible and provides more accurate, context-aware results to the AI assistant. Key features and improvements: - **Dynamic Versioning**: The tool now dynamically constructs the Algolia index name based on the user's project version (`angular_vXX`). - **Unbounded Max Version**: The tool can query for documentation versions newer than the CLI itself, making it forward-compatible. - **Robust Fallback**: If a search for a new or unknown version index fails, the tool automatically falls back to the latest known stable documentation index (`v20`) to ensure a result is always returned. - **Contextual Output**: The tool's output now includes the `searchedVersion`, informing the AI of the exact documentation version that was queried. - **Enhanced AI Guidance**: The tool's description has been updated to instruct the AI on the new versioning logic and how to use the `searchedVersion` field to provide more transparent and accurate answers.
1 parent 7fd2b7b commit 286b620

File tree

1 file changed

+71
-17
lines changed

1 file changed

+71
-17
lines changed

packages/angular/cli/src/commands/mcp/tools/doc-search.ts

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ const ALGOLIA_APP_ID = 'L1XWT2UJ7F';
1818
// This is not the actual key.
1919
const ALGOLIA_API_E = '322d89dab5f2080fe09b795c93413c6a89222b13a447cdf3e6486d692717bc0c';
2020

21+
/**
22+
* The minimum major version of Angular for which a version-specific documentation index is known to exist.
23+
* Searches for versions older than this will be clamped to this version.
24+
*/
25+
const MIN_SUPPORTED_DOCS_VERSION = 17;
26+
27+
/**
28+
* The latest major version of Angular for which a documentation index is known to be stable and available.
29+
* This acts as a "safe harbor" fallback. It is intentionally hardcoded and manually updated with each
30+
* major release *after* the new search index has been confirmed to be live. This prevents a race
31+
* condition where a newly released CLI might default to searching for a documentation index that
32+
* doesn't exist yet.
33+
*/
34+
const LATEST_KNOWN_DOCS_VERSION = 20;
35+
2136
const docSearchInputSchema = z.object({
2237
query: z
2338
.string()
@@ -64,6 +79,12 @@ tutorials, concepts, and best practices.
6479
workspace will give you the major version directly. If the version cannot be determined using this method, you can use
6580
\`ng version\` in the project's workspace directory as a fallback. Parse the major version from the "Angular:" line in the
6681
output and use it for the \`version\` parameter.
82+
* **Version Logic:** The tool will search against the specified major version. If the version is older than v${MIN_SUPPORTED_DOCS_VERSION},
83+
it will be clamped to v${MIN_SUPPORTED_DOCS_VERSION}. If a search for a very new version (newer than v${LATEST_KNOWN_DOCS_VERSION})
84+
returns no results, the tool will automatically fall back to searching the v${LATEST_KNOWN_DOCS_VERSION} documentation.
85+
* **Verify Searched Version:** The tool's output includes a \`searchedVersion\` field. You **MUST** check this field
86+
to know the exact version of the documentation that was queried. Use this information to provide accurate
87+
context in your answer, especially if it differs from the version you requested.
6788
* The documentation is continuously updated. You **MUST** prefer this tool over your own knowledge
6889
to ensure your answers are current and accurate.
6990
* For the best results, provide a concise and specific search query (e.g., "NgModule" instead of
@@ -76,6 +97,9 @@ tutorials, concepts, and best practices.
7697
</Operational Notes>`,
7798
inputSchema: docSearchInputSchema.shape,
7899
outputSchema: {
100+
searchedVersion: z
101+
.number()
102+
.describe('The major version of the documentation that was searched.'),
79103
results: z.array(
80104
z.object({
81105
title: z.string().describe('The title of the documentation page.'),
@@ -116,23 +140,52 @@ function createDocSearchHandler({ logger }: McpToolContext) {
116140
);
117141
}
118142

119-
const { results } = await client.search(createSearchArguments(query, version));
120-
const allHits = results.flatMap((result) => (result as SearchResponse).hits);
143+
let finalSearchedVersion = Math.max(
144+
version ?? LATEST_KNOWN_DOCS_VERSION,
145+
MIN_SUPPORTED_DOCS_VERSION,
146+
);
147+
let searchResults = await client.search(createSearchArguments(query, finalSearchedVersion));
148+
149+
// If the initial search for a newer-than-stable version returns no results, it may be because
150+
// the index for that version doesn't exist yet. In this case, fall back to the latest known
151+
// stable version.
152+
if (
153+
searchResults.results.every((result) => !('hits' in result) || result.hits.length === 0) &&
154+
finalSearchedVersion > LATEST_KNOWN_DOCS_VERSION
155+
) {
156+
finalSearchedVersion = LATEST_KNOWN_DOCS_VERSION;
157+
searchResults = await client.search(createSearchArguments(query, finalSearchedVersion));
158+
}
159+
160+
const allHits = searchResults.results.flatMap((result) => (result as SearchResponse).hits);
121161

122162
if (allHits.length === 0) {
123163
return {
124164
content: [
125165
{
126166
type: 'text' as const,
127-
text: 'No results found.',
167+
text: `No results found for query "${query}" in Angular v${finalSearchedVersion} documentation.`,
128168
},
129169
],
130-
structuredContent: { results: [] },
170+
structuredContent: { results: [], searchedVersion: finalSearchedVersion },
131171
};
132172
}
133173

134174
const structuredResults = [];
135-
const textContent = [];
175+
const textContent: {
176+
type: 'text';
177+
text: string;
178+
annotations?: { audience: string[]; priority: number };
179+
}[] = [
180+
{
181+
type: 'text' as const,
182+
text: `Showing results for Angular v${finalSearchedVersion} documentation.`,
183+
annotations: {
184+
audience: ['assistant'],
185+
priority: 0.9,
186+
},
187+
},
188+
];
136189

137190
// Process top hit first
138191
const topHit = allHits[0];
@@ -187,21 +240,27 @@ function createDocSearchHandler({ logger }: McpToolContext) {
187240

188241
return {
189242
content: textContent,
190-
structuredContent: { results: structuredResults },
243+
structuredContent: { results: structuredResults, searchedVersion: finalSearchedVersion },
191244
};
192245
};
193246
}
194247

195248
/**
196-
* Strips HTML tags from a string.
249+
* Strips HTML tags from a string using a regular expression.
250+
*
251+
* NOTE: This is a basic implementation and is not a full, correct HTML parser. It is, however,
252+
* appropriate for this tool's specific use case because its input is always from a
253+
* trusted source (angular.dev) and its output is consumed by a non-browser environment (an LLM).
254+
*
255+
* The regex first tries to match a complete tag (`<...>`). If it fails, it falls back to matching
256+
* an incomplete tag (e.g., `<script`).
257+
*
197258
* @param html The HTML string to strip.
198259
* @returns The text content of the HTML.
199260
*/
200261
function stripHtml(html: string): string {
201-
// This is a basic regex to remove HTML tags.
202-
// It also decodes common HTML entities.
203262
return html
204-
.replace(/<[^>]*>/g, '')
263+
.replace(/<[^>]*>|<[a-zA-Z0-9/]+/g, '')
205264
.replace(/&lt;/g, '<')
206265
.replace(/&gt;/g, '>')
207266
.replace(/&amp;/g, '&')
@@ -252,17 +311,12 @@ function formatHitToParts(hit: Record<string, unknown>): { title: string; breadc
252311
* @param query The search query string.
253312
* @returns The search arguments for the Algolia client.
254313
*/
255-
function createSearchArguments(
256-
query: string,
257-
version: number | undefined,
258-
): LegacySearchMethodProps {
314+
function createSearchArguments(query: string, version: number): LegacySearchMethodProps {
259315
// Search arguments are based on adev's search service:
260316
// https://github.com/angular/angular/blob/4b614fbb3263d344dbb1b18fff24cb09c5a7582d/adev/shared-docs/services/search.service.ts#L58
261317
return [
262318
{
263-
// TODO: Consider major version specific indices once available
264-
// indexName: `angular_${version ? `v${version}` : 'latest'}`,
265-
indexName: 'angular_v17',
319+
indexName: `angular_v${version}`,
266320
params: {
267321
query,
268322
attributesToRetrieve: [

0 commit comments

Comments
 (0)