@@ -13,6 +13,7 @@ import { hideBin } from 'yargs/helpers';
1313dotenv . config ( ) ;
1414
1515const API_KEY = process . env . TAVILY_API_KEY ;
16+ const IS_KEYLESS = ! API_KEY ;
1617const HUMAN_ID = process . env . TAVILY_HUMAN_ID ;
1718const SESSION_ID = randomUUID ( ) ;
1819
@@ -84,7 +85,7 @@ class TavilyClient {
8485 this . server = new Server (
8586 {
8687 name : "tavily-mcp" ,
87- version : "0.2.19 " ,
88+ version : "0.2.20 " ,
8889 } ,
8990 {
9091 capabilities : {
@@ -97,13 +98,18 @@ class TavilyClient {
9798 headers : {
9899 'accept' : 'application/json' ,
99100 'content-type' : 'application/json' ,
100- 'Authorization' : `Bearer ${ API_KEY } ` ,
101- 'X-Client-Source' : 'MCP' ,
101+ ...( IS_KEYLESS
102+ ? { 'X-Tavily-Access-Mode' : 'keyless' , 'X-Client-Source' : 'tavily-mcp-keyless' }
103+ : { 'Authorization' : `Bearer ${ API_KEY } ` , 'X-Client-Source' : 'MCP' } ) ,
102104 'X-Session-Id' : SESSION_ID ,
103105 ...( HUMAN_ID ? { 'X-Human-Id' : HUMAN_ID } : { } ) ,
104106 }
105107 } ) ;
106108
109+ if ( IS_KEYLESS ) {
110+ console . error ( '[tavily-mcp] no TAVILY_API_KEY set; running in keyless mode. Search and extract are available; other tools will return a message explaining that an API key is required.' ) ;
111+ }
112+
107113 this . setupHandlers ( ) ;
108114 this . setupErrorHandling ( ) ;
109115 }
@@ -437,14 +443,6 @@ class TavilyClient {
437443 } ) ;
438444
439445 this . server . setRequestHandler ( CallToolRequestSchema , async ( request : any ) => {
440- // Check for API key at request time and return proper JSON-RPC error
441- if ( ! API_KEY ) {
442- throw new McpError (
443- ErrorCode . InvalidRequest ,
444- "TAVILY_API_KEY environment variable is required. Please set it before using this MCP server."
445- ) ;
446- }
447-
448446 try {
449447 let response : TavilyResponse ;
450448 const args = request . params . arguments ?? { } ;
@@ -553,6 +551,14 @@ class TavilyClient {
553551 } ;
554552 } catch ( error : any ) {
555553 if ( axios . isAxiosError ( error ) ) {
554+ if ( isKeylessEnvelope ( error . response ?. data ) ) {
555+ return {
556+ content : [ {
557+ type : "text" ,
558+ text : formatKeylessEnvelope ( error . response ! . data )
559+ } ]
560+ } ;
561+ }
556562 const toolName = request . params . name ?. replace ( 'tavily_' , '' ) || '' ;
557563 const docsUrl = this . docsURLs [ toolName ] || '' ;
558564 const responseData = error . response ?. data ;
@@ -582,9 +588,8 @@ class TavilyClient {
582588 }
583589
584590 async search ( params : any ) : Promise < TavilyResponse > {
585- try {
586591 const endpoint = this . baseURLs . search ;
587-
592+
588593 const defaults = this . getDefaultParameters ( ) ;
589594
590595 // Prepare the request payload
@@ -604,7 +609,7 @@ class TavilyClient {
604609 start_date : params . start_date ,
605610 end_date : params . end_date ,
606611 exact_match : params . exact_match ,
607- api_key : API_KEY ,
612+ ... ( IS_KEYLESS ? { } : { api_key : API_KEY } ) ,
608613 } ;
609614
610615 // Apply default parameters
@@ -634,65 +639,30 @@ class TavilyClient {
634639
635640 const response = await this . axiosInstance . post ( endpoint , cleanedParams ) ;
636641 return response . data ;
637- } catch ( error : any ) {
638- if ( error . response ?. status === 401 ) {
639- throw new Error ( `Invalid API key. Documentation: ${ this . docsURLs . search } ` ) ;
640- } else if ( error . response ?. status === 429 ) {
641- throw new Error ( `Usage limit exceeded. Documentation: ${ this . docsURLs . search } ` ) ;
642- }
643- throw error ;
644- }
645642 }
646643
647644 async extract ( params : any ) : Promise < TavilyResponse > {
648- try {
649- const response = await this . axiosInstance . post ( this . baseURLs . extract , {
650- ...params ,
651- api_key : API_KEY
652- } ) ;
653- return response . data ;
654- } catch ( error : any ) {
655- if ( error . response ?. status === 401 ) {
656- throw new Error ( `Invalid API key. Documentation: ${ this . docsURLs . extract } ` ) ;
657- } else if ( error . response ?. status === 429 ) {
658- throw new Error ( `Usage limit exceeded. Documentation: ${ this . docsURLs . extract } ` ) ;
659- }
660- throw error ;
661- }
645+ const response = await this . axiosInstance . post ( this . baseURLs . extract , {
646+ ...params ,
647+ ...( IS_KEYLESS ? { } : { api_key : API_KEY } )
648+ } ) ;
649+ return response . data ;
662650 }
663651
664652 async crawl ( params : any ) : Promise < TavilyCrawlResponse > {
665- try {
666- const response = await this . axiosInstance . post ( this . baseURLs . crawl , {
667- ...params ,
668- api_key : API_KEY
669- } ) ;
670- return response . data ;
671- } catch ( error : any ) {
672- if ( error . response ?. status === 401 ) {
673- throw new Error ( `Invalid API key. Documentation: ${ this . docsURLs . crawl } ` ) ;
674- } else if ( error . response ?. status === 429 ) {
675- throw new Error ( `Usage limit exceeded. Documentation: ${ this . docsURLs . crawl } ` ) ;
676- }
677- throw error ;
678- }
653+ const response = await this . axiosInstance . post ( this . baseURLs . crawl , {
654+ ...params ,
655+ ...( IS_KEYLESS ? { } : { api_key : API_KEY } )
656+ } ) ;
657+ return response . data ;
679658 }
680659
681660 async map ( params : any ) : Promise < TavilyMapResponse > {
682- try {
683- const response = await this . axiosInstance . post ( this . baseURLs . map , {
684- ...params ,
685- api_key : API_KEY
686- } ) ;
687- return response . data ;
688- } catch ( error : any ) {
689- if ( error . response ?. status === 401 ) {
690- throw new Error ( `Invalid API key. Documentation: ${ this . docsURLs . map } ` ) ;
691- } else if ( error . response ?. status === 429 ) {
692- throw new Error ( `Usage limit exceeded. Documentation: ${ this . docsURLs . map } ` ) ;
693- }
694- throw error ;
695- }
661+ const response = await this . axiosInstance . post ( this . baseURLs . map , {
662+ ...params ,
663+ ...( IS_KEYLESS ? { } : { api_key : API_KEY } )
664+ } ) ;
665+ return response . data ;
696666 }
697667
698668 async research ( params : any ) : Promise < TavilyResearchResponse > {
@@ -706,7 +676,7 @@ class TavilyClient {
706676 const response = await this . axiosInstance . post ( this . baseURLs . research , {
707677 input : params . input ,
708678 model : params . model || 'auto' ,
709- api_key : API_KEY
679+ ... ( IS_KEYLESS ? { } : { api_key : API_KEY } )
710680 } ) ;
711681
712682 const requestId = response . data . request_id ;
@@ -766,6 +736,40 @@ class TavilyClient {
766736 }
767737}
768738
739+ function isKeylessEnvelope ( data : any ) : boolean {
740+ // Recognises the Tavily API's recoverable-error envelope shape.
741+ // Used for keyless rate-limit caps and endpoints that require an API key.
742+ return ! ! ( data && typeof data === 'object'
743+ && data . error && typeof data . error === 'object'
744+ && typeof data . error . code === 'string' ) ;
745+ }
746+
747+ function formatKeylessEnvelope ( data : any ) : string {
748+ // Render the Tavily API's recoverable-error envelope as plain text:
749+ // the natural-language message, followed by retry-after (when present).
750+ const err = data . error ;
751+ const lines : string [ ] = [ String ( err . message ?? '' ) ] ;
752+ if ( err . retry_after_seconds != null ) {
753+ lines . push ( `Retry after: ${ err . retry_after_seconds } s` ) ;
754+ }
755+ if ( Array . isArray ( err . next_actions ) && err . next_actions . length > 0 ) {
756+ lines . push ( '' , 'Continuation options:' ) ;
757+ for ( const a of err . next_actions ) {
758+ if ( a ?. type === 'agentic_payment' ) {
759+ lines . push ( `- Agentic payment (${ a . scheme ?? 'x402' } ): ${ a . details ?? '' } ` ) ;
760+ } else if ( a ?. type === 'signup' ) {
761+ lines . push ( `- Sign up for a Tavily API key: ${ a . url ?? '' } ` ) ;
762+ } else if ( a ?. type === 'bonus_credits' && a . eligible ) {
763+ lines . push ( `- Earn ${ a . credits_on_completion ?? '' } bonus credits by POSTing answers to ${ a . endpoint ?? '' } ` ) ;
764+ if ( Array . isArray ( a . questions ) ) {
765+ a . questions . forEach ( ( q : string , i : number ) => lines . push ( ` ${ i + 1 } . ${ q } ` ) ) ;
766+ }
767+ }
768+ }
769+ }
770+ return lines . filter ( Boolean ) . join ( '\n' ) ;
771+ }
772+
769773function formatResults ( response : TavilyResponse ) : string {
770774 // Format API response into human-readable text
771775 const output : string [ ] = [ ] ;
0 commit comments