@@ -25,6 +25,24 @@ import {
2525 agents ,
2626} from '../db/schema.js' ;
2727import { embed } from './embeddings.js' ;
28+ import { createLogger } from '../lib/logger.js' ;
29+
30+ const embedLog = createLogger ( { service : 'mcp-registry' } ) ;
31+
32+ /**
33+ * embed() wrapper that swallows failures. The nomic model downloads on first
34+ * use; if the download is broken (no cache, no network, etc.) we fall back to
35+ * the keyword-only path rather than failing the whole request. Existing
36+ * routes (tasks.ts, agents.ts) use the same graceful-degradation pattern.
37+ */
38+ async function tryEmbed ( text : string , type : 'document' | 'query' ) : Promise < number [ ] | null > {
39+ try {
40+ return await embed ( text , type ) ;
41+ } catch ( err ) {
42+ embedLog . warn ( `embed failed, falling back to lexical path: ${ String ( err ) } ` ) ;
43+ return null ;
44+ }
45+ }
2846import {
2947 canonicalizeAttestationPayload ,
3048 type McpServer ,
@@ -90,10 +108,18 @@ export async function listServers(
90108 let orderExpr = sql `${ mcpServers . qualityScore } DESC, ${ mcpServers . verifiedUsageCount } DESC` ;
91109
92110 if ( query . q && query . q . trim ( ) . length > 0 ) {
93- const queryEmbedding = await embed ( query . q , 'query' ) ;
94- const embedLiteral = `[${ queryEmbedding . join ( ',' ) } ]` ;
95- predicates . push ( sql `${ mcpServers . descriptionEmbedding } IS NOT NULL` ) ;
96- orderExpr = sql `${ mcpServers . descriptionEmbedding } <=> ${ embedLiteral } ::vector` ;
111+ const queryEmbedding = await tryEmbed ( query . q , 'query' ) ;
112+ if ( queryEmbedding ) {
113+ const embedLiteral = `[${ queryEmbedding . join ( ',' ) } ]` ;
114+ predicates . push ( sql `${ mcpServers . descriptionEmbedding } IS NOT NULL` ) ;
115+ orderExpr = sql `${ mcpServers . descriptionEmbedding } <=> ${ embedLiteral } ::vector` ;
116+ } else {
117+ // Lexical fallback — case-insensitive contains over name + description
118+ const needle = `%${ query . q . trim ( ) } %` ;
119+ predicates . push (
120+ sql `(${ mcpServers . name } ILIKE ${ needle } OR ${ mcpServers . description } ILIKE ${ needle } )` ,
121+ ) ;
122+ }
97123 }
98124
99125 const rows = await db
@@ -161,7 +187,7 @@ export async function submitServer(
161187 throw Errors . conflict ( 'An MCP server with that slug already exists' ) ;
162188 }
163189
164- const descriptionEmbedding = await embed ( `${ input . name } \n\n${ input . description } ` , 'document' ) ;
190+ const descriptionEmbedding = await tryEmbed ( `${ input . name } \n\n${ input . description } ` , 'document' ) ;
165191
166192 return db . transaction ( async ( tx ) => {
167193 const [ server ] = await tx . insert ( mcpServers ) . values ( {
@@ -199,7 +225,7 @@ export async function submitServer(
199225
200226 if ( input . tools . length > 0 ) {
201227 const toolEmbeddings = await Promise . all (
202- input . tools . map ( ( t ) => embed ( `${ t . name } \n\n${ t . description ?? '' } ` , 'document' ) ) ,
228+ input . tools . map ( ( t ) => tryEmbed ( `${ t . name } \n\n${ t . description ?? '' } ` , 'document' ) ) ,
203229 ) ;
204230 await tx . insert ( mcpServerTools ) . values (
205231 input . tools . map ( ( t , i ) => ( {
@@ -246,7 +272,8 @@ export async function updateServer(
246272 if ( input . name !== undefined || input . description !== undefined ) {
247273 const name = input . name ?? existing . name ;
248274 const description = input . description ?? existing . description ;
249- patch . descriptionEmbedding = await embed ( `${ name } \n\n${ description } ` , 'document' ) ;
275+ const emb = await tryEmbed ( `${ name } \n\n${ description } ` , 'document' ) ;
276+ if ( emb ) patch . descriptionEmbedding = emb ;
250277 }
251278
252279 await db . update ( mcpServers ) . set ( patch ) . where ( eq ( mcpServers . id , existing . id ) ) ;
@@ -424,7 +451,7 @@ export async function upsertIngestedServer(
424451 } ,
425452) : Promise < { serverId : string ; created : boolean } > {
426453 const existing = await db . select ( ) . from ( mcpServers ) . where ( eq ( mcpServers . slug , upstream . slug ) ) . limit ( 1 ) ;
427- const descriptionEmbedding = await embed ( `${ upstream . name } \n\n${ upstream . description } ` , 'document' ) ;
454+ const descriptionEmbedding = await tryEmbed ( `${ upstream . name } \n\n${ upstream . description } ` , 'document' ) ;
428455
429456 if ( existing . length === 0 ) {
430457 const [ server ] = await db . insert ( mcpServers ) . values ( {
@@ -459,7 +486,7 @@ export async function upsertIngestedServer(
459486
460487 if ( upstream . tools ?. length ) {
461488 const toolEmbeddings = await Promise . all (
462- upstream . tools . map ( ( t ) => embed ( `${ t . name } \n\n${ t . description ?? '' } ` , 'document' ) ) ,
489+ upstream . tools . map ( ( t ) => tryEmbed ( `${ t . name } \n\n${ t . description ?? '' } ` , 'document' ) ) ,
463490 ) ;
464491 await db . insert ( mcpServerTools ) . values (
465492 upstream . tools . map ( ( t , i ) => ( {
@@ -540,20 +567,34 @@ export async function recommendForTask(params: {
540567 limit ?: number ;
541568} ) : Promise < Array < McpServer & { similarity : number } > > {
542569 const limit = params . limit ?? 10 ;
543- const queryEmbedding = await embed ( params . description , 'query' ) ;
544- const embedLiteral = `[${ queryEmbedding . join ( ',' ) } ]` ;
570+ const queryEmbedding = await tryEmbed ( params . description , 'query' ) ;
545571
546- const predicates = [
547- sql `${ mcpServers . archivedAt } IS NULL` ,
548- sql `${ mcpServers . descriptionEmbedding } IS NOT NULL` ,
549- ] ;
572+ const predicates = [ sql `${ mcpServers . archivedAt } IS NULL` ] ;
550573 if ( params . transport ) predicates . push ( eq ( mcpServers . transport , params . transport ) ) ;
551574 if ( params . maxPriceMicroUsdc !== undefined ) {
552575 predicates . push (
553576 sql `(${ mcpServers . paidTier } = false OR ${ mcpServers . priceMicroUsdc } <= ${ params . maxPriceMicroUsdc } )` ,
554577 ) ;
555578 }
556579
580+ if ( ! queryEmbedding ) {
581+ // Lexical fallback — quality-ranked results with ILIKE match on the
582+ // description. Semantic similarity is set to 0 so callers can detect
583+ // the degraded path.
584+ const needle = `%${ params . description . slice ( 0 , 200 ) . trim ( ) } %` ;
585+ predicates . push ( sql `(${ mcpServers . name } ILIKE ${ needle } OR ${ mcpServers . description } ILIKE ${ needle } )` ) ;
586+ const rows = await db
587+ . select ( )
588+ . from ( mcpServers )
589+ . where ( and ( ...predicates ) )
590+ . orderBy ( desc ( mcpServers . qualityScore ) , desc ( mcpServers . verifiedUsageCount ) )
591+ . limit ( limit ) ;
592+ return rows . map ( ( row ) => ( { ...rowToServer ( row ) , similarity : 0 } ) ) ;
593+ }
594+
595+ const embedLiteral = `[${ queryEmbedding . join ( ',' ) } ]` ;
596+ predicates . push ( sql `${ mcpServers . descriptionEmbedding } IS NOT NULL` ) ;
597+
557598 const rows = await db
558599 . select ( {
559600 row : mcpServers ,
0 commit comments