66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9- import type { LegacySearchMethodProps , SearchResponse } from 'algoliasearch' ;
109import { createDecipheriv } from 'node:crypto' ;
1110import { Readable } from 'node:stream' ;
1211import { z } from 'zod' ;
@@ -32,7 +31,7 @@ const MIN_SUPPORTED_DOCS_VERSION = 17;
3231 * condition where a newly released CLI might default to searching for a documentation index that
3332 * doesn't exist yet.
3433 */
35- const LATEST_KNOWN_DOCS_VERSION = 20 ;
34+ const LATEST_KNOWN_DOCS_VERSION = 22 ;
3635
3736const docSearchInputSchema = z . object ( {
3837 query : z
@@ -99,41 +98,86 @@ Searches the official Angular documentation (angular.dev) to answer questions ab
9998} ) ;
10099
101100function createDocSearchHandler ( { logger } : McpToolContext ) {
102- let client : import ( 'algoliasearch' ) . SearchClient | undefined ;
101+ let apiKey : string | undefined ;
103102
104- return async ( { query, includeTopContent , version } : DocSearchInput ) => {
105- if ( ! client ) {
103+ async function performSearch ( query : string , version : number ) {
104+ if ( ! apiKey ) {
106105 const dcip = createDecipheriv (
107106 'aes-256-gcm' ,
108107 ( k1 + ALGOLIA_APP_ID ) . padEnd ( 32 , '^' ) ,
109108 iv ,
110109 ) . setAuthTag ( Buffer . from ( at , 'base64' ) ) ;
111- const { searchClient } = await import ( 'algoliasearch' ) ;
112- client = searchClient (
113- ALGOLIA_APP_ID ,
114- dcip . update ( ALGOLIA_API_E , 'hex' , 'utf-8' ) + dcip . final ( 'utf-8' ) ,
110+ apiKey = dcip . update ( ALGOLIA_API_E , 'hex' , 'utf-8' ) + dcip . final ( 'utf-8' ) ;
111+ }
112+
113+ const url = `https://${ ALGOLIA_APP_ID } -dsn.algolia.net/1/indexes/angular_v${ version } /query` ;
114+ const response = await fetch ( url , {
115+ method : 'POST' ,
116+ headers : {
117+ 'Content-Type' : 'application/json' ,
118+ 'X-Algolia-Application-Id' : ALGOLIA_APP_ID ,
119+ 'X-Algolia-API-Key' : apiKey ,
120+ } ,
121+ body : JSON . stringify ( {
122+ query,
123+ attributesToRetrieve : [
124+ 'hierarchy.lvl0' ,
125+ 'hierarchy.lvl1' ,
126+ 'hierarchy.lvl2' ,
127+ 'hierarchy.lvl3' ,
128+ 'hierarchy.lvl4' ,
129+ 'hierarchy.lvl5' ,
130+ 'hierarchy.lvl6' ,
131+ 'content' ,
132+ 'type' ,
133+ 'url' ,
134+ ] ,
135+ hitsPerPage : 10 ,
136+ } ) ,
137+ signal : AbortSignal . timeout ( 5000 ) , // Timeout after 5 seconds
138+ } ) ;
139+
140+ if ( ! response . ok ) {
141+ throw new Error (
142+ `Search request failed with status ${ response . status } (${ response . statusText } )` ,
115143 ) ;
116144 }
117145
146+ const data = ( await response . json ( ) ) as { hits : Record < string , unknown > [ ] } ;
147+
148+ return data . hits ;
149+ }
150+
151+ return async ( { query, includeTopContent, version } : DocSearchInput ) => {
118152 let finalSearchedVersion = Math . max (
119153 version ?? LATEST_KNOWN_DOCS_VERSION ,
120154 MIN_SUPPORTED_DOCS_VERSION ,
121155 ) ;
122- let searchResults ;
156+
157+ let allHits : Record < string , unknown > [ ] | undefined ;
123158 try {
124- searchResults = await client . search ( createSearchArguments ( query , finalSearchedVersion ) ) ;
125- } catch { }
159+ allHits = await performSearch ( query , finalSearchedVersion ) ;
160+ } catch ( error ) {
161+ logger . warn ( `Error searching Angular v${ finalSearchedVersion } documentation: ${ error } ` ) ;
162+ }
126163
127164 // If the initial search for a newer-than-stable version returns no results, it may be because
128165 // the index for that version doesn't exist yet. In this case, fall back to the latest known
129166 // stable version.
130- if ( ! searchResults && finalSearchedVersion > LATEST_KNOWN_DOCS_VERSION ) {
167+ if ( ( ! allHits || allHits . length === 0 ) && finalSearchedVersion > LATEST_KNOWN_DOCS_VERSION ) {
168+ logger . warn (
169+ `Documentation index for v${ finalSearchedVersion } not found or empty. Falling back to v${ LATEST_KNOWN_DOCS_VERSION } .` ,
170+ ) ;
131171 finalSearchedVersion = LATEST_KNOWN_DOCS_VERSION ;
132- searchResults = await client . search ( createSearchArguments ( query , finalSearchedVersion ) ) ;
172+ try {
173+ allHits = await performSearch ( query , finalSearchedVersion ) ;
174+ } catch ( error ) {
175+ logger . warn (
176+ `Error searching fallback Angular v${ finalSearchedVersion } documentation: ${ error } ` ,
177+ ) ;
178+ }
133179 }
134180
135- const allHits = searchResults ?. results . flatMap ( ( result ) => ( result as SearchResponse ) . hits ) ;
136-
137181 if ( ! allHits ?. length ) {
138182 return {
139183 content : [
@@ -282,38 +326,3 @@ function formatHitToParts(hit: Record<string, unknown>): { title: string; breadc
282326
283327 return { title, breadcrumb } ;
284328}
285-
286- /**
287- * Creates the search arguments for an Algolia search.
288- *
289- * The arguments are based on the search implementation in `adev`.
290- *
291- * @param query The search query string.
292- * @returns The search arguments for the Algolia client.
293- */
294- function createSearchArguments ( query : string , version : number ) : LegacySearchMethodProps {
295- // Search arguments are based on adev's search service:
296- // https://github.com/angular/angular/blob/4b614fbb3263d344dbb1b18fff24cb09c5a7582d/adev/shared-docs/services/search.service.ts#L58
297- return [
298- {
299- indexName : `angular_v${ version } ` ,
300- params : {
301- query,
302- attributesToRetrieve : [
303- 'hierarchy.lvl0' ,
304- 'hierarchy.lvl1' ,
305- 'hierarchy.lvl2' ,
306- 'hierarchy.lvl3' ,
307- 'hierarchy.lvl4' ,
308- 'hierarchy.lvl5' ,
309- 'hierarchy.lvl6' ,
310- 'content' ,
311- 'type' ,
312- 'url' ,
313- ] ,
314- hitsPerPage : 10 ,
315- } ,
316- type : 'default' ,
317- } ,
318- ] ;
319- }
0 commit comments