@@ -661,6 +661,96 @@ const MCP_TOOL_ROUTING = {
661661 biorxiv_get_categories : { server : 'biorxiv' , mcpName : 'get_categories' }
662662}
663663
664+ const VFB_CACHED_TERM_INFO_URL = 'https://v3-cached.virtualflybrain.org/get_term_info'
665+ const VFB_CACHED_RUN_QUERY_URL = 'https://v3-cached.virtualflybrain.org/run_query'
666+ const VFB_CACHED_TERM_INFO_TIMEOUT_MS = 12000
667+
668+ function isRetryableMcpError ( error ) {
669+ const message = `${ error ?. name || '' } ${ error ?. message || '' } ` . toLowerCase ( )
670+ return (
671+ message . includes ( 'timeout' ) ||
672+ message . includes ( 'timed out' ) ||
673+ message . includes ( 'abort' ) ||
674+ message . includes ( 'network' ) ||
675+ message . includes ( 'fetch failed' ) ||
676+ message . includes ( 'econnreset' ) ||
677+ message . includes ( 'econnrefused' ) ||
678+ message . includes ( 'enotfound' ) ||
679+ message . includes ( 'etimedout' ) ||
680+ message . includes ( 'eai_again' ) ||
681+ message . includes ( 'connectivity' )
682+ )
683+ }
684+
685+ async function fetchCachedVfbTermInfo ( id ) {
686+ const safeId = String ( id || '' ) . trim ( )
687+ if ( ! safeId ) throw new Error ( 'Missing id for cached VFB get_term_info fallback.' )
688+
689+ const controller = new AbortController ( )
690+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , VFB_CACHED_TERM_INFO_TIMEOUT_MS )
691+
692+ try {
693+ const cacheUrl = `${ VFB_CACHED_TERM_INFO_URL } ?id=${ encodeURIComponent ( safeId ) } `
694+ const response = await fetch ( cacheUrl , {
695+ method : 'GET' ,
696+ headers : { Accept : 'application/json' } ,
697+ signal : controller . signal
698+ } )
699+
700+ if ( ! response . ok ) {
701+ const responseText = await response . text ( )
702+ throw new Error ( `Cached VFB get_term_info failed: HTTP ${ response . status } ${ responseText . slice ( 0 , 200 ) } ` . trim ( ) )
703+ }
704+
705+ const responseText = await response . text ( )
706+ try {
707+ JSON . parse ( responseText )
708+ } catch {
709+ throw new Error ( 'Cached VFB get_term_info returned non-JSON payload.' )
710+ }
711+
712+ return responseText
713+ } finally {
714+ clearTimeout ( timeoutId )
715+ }
716+ }
717+
718+ async function fetchCachedVfbRunQuery ( id , queryType ) {
719+ const safeId = String ( id || '' ) . trim ( )
720+ const safeQueryType = String ( queryType || '' ) . trim ( )
721+ if ( ! safeId || ! safeQueryType ) {
722+ throw new Error ( 'Missing id or query_type for cached VFB run_query fallback.' )
723+ }
724+
725+ const controller = new AbortController ( )
726+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , VFB_CACHED_TERM_INFO_TIMEOUT_MS )
727+
728+ try {
729+ const cacheUrl = `${ VFB_CACHED_RUN_QUERY_URL } ?id=${ encodeURIComponent ( safeId ) } &query_type=${ encodeURIComponent ( safeQueryType ) } `
730+ const response = await fetch ( cacheUrl , {
731+ method : 'GET' ,
732+ headers : { Accept : 'application/json' } ,
733+ signal : controller . signal
734+ } )
735+
736+ if ( ! response . ok ) {
737+ const responseText = await response . text ( )
738+ throw new Error ( `Cached VFB run_query failed: HTTP ${ response . status } ${ responseText . slice ( 0 , 200 ) } ` . trim ( ) )
739+ }
740+
741+ const responseText = await response . text ( )
742+ try {
743+ JSON . parse ( responseText )
744+ } catch {
745+ throw new Error ( 'Cached VFB run_query returned non-JSON payload.' )
746+ }
747+
748+ return responseText
749+ } finally {
750+ clearTimeout ( timeoutId )
751+ }
752+ }
753+
664754async function executeFunctionTool ( name , args ) {
665755 if ( name === 'search_pubmed' ) {
666756 return searchPubmed ( args . query , args . max_results , args . sort )
@@ -689,15 +779,55 @@ async function executeFunctionTool(name, args) {
689779 if ( value !== undefined && value !== null ) cleanArgs [ key ] = value
690780 }
691781
692- const result = await client . callTool ( { name : routing . mcpName , arguments : cleanArgs } )
693- if ( result ?. content ) {
694- const texts = result . content
695- . filter ( item => item . type === 'text' )
696- . map ( item => item . text )
697- return texts . join ( '\n' ) || JSON . stringify ( result . content )
698- }
782+ try {
783+ const result = await client . callTool ( { name : routing . mcpName , arguments : cleanArgs } )
784+ if ( result ?. content ) {
785+ const texts = result . content
786+ . filter ( item => item . type === 'text' )
787+ . map ( item => item . text )
788+ return texts . join ( '\n' ) || JSON . stringify ( result . content )
789+ }
790+
791+ return JSON . stringify ( result )
792+ } catch ( error ) {
793+ const shouldUseCachedTermInfoFallback =
794+ name === 'vfb_get_term_info' &&
795+ routing . server === 'vfb' &&
796+ typeof cleanArgs . id === 'string' &&
797+ cleanArgs . id . trim ( ) . length > 0 &&
798+ isRetryableMcpError ( error )
799+
800+ if ( shouldUseCachedTermInfoFallback ) {
801+ try {
802+ return await fetchCachedVfbTermInfo ( cleanArgs . id )
803+ } catch ( fallbackError ) {
804+ throw new Error (
805+ `VFB MCP get_term_info failed (${ error ?. message || 'unknown error' } ); cached fallback failed (${ fallbackError ?. message || 'unknown error' } ).`
806+ )
807+ }
808+ }
699809
700- return JSON . stringify ( result )
810+ const shouldUseCachedRunQueryFallback =
811+ name === 'vfb_run_query' &&
812+ routing . server === 'vfb' &&
813+ typeof cleanArgs . id === 'string' &&
814+ cleanArgs . id . trim ( ) . length > 0 &&
815+ typeof cleanArgs . query_type === 'string' &&
816+ cleanArgs . query_type . trim ( ) . length > 0 &&
817+ isRetryableMcpError ( error )
818+
819+ if ( shouldUseCachedRunQueryFallback ) {
820+ try {
821+ return await fetchCachedVfbRunQuery ( cleanArgs . id , cleanArgs . query_type )
822+ } catch ( fallbackError ) {
823+ throw new Error (
824+ `VFB MCP run_query failed (${ error ?. message || 'unknown error' } ); cached fallback failed (${ fallbackError ?. message || 'unknown error' } ).`
825+ )
826+ }
827+ }
828+
829+ throw error
830+ }
701831 }
702832
703833 throw new Error ( `Unknown function tool: ${ name } ` )
0 commit comments