@@ -43,6 +43,37 @@ const WebFetchArgs = type({
4343
4444type WebFetchFormat = "text" | "markdown" | "html" ;
4545
46+ type ParsedWebFetchArgs =
47+ | { ok : true ; url : string ; format : WebFetchFormat ; timeout : number }
48+ | { ok : false ; error : string } ;
49+
50+ function parseWebFetchArgs (
51+ rawArgs : Record < string , unknown > ,
52+ ) : ParsedWebFetchArgs {
53+ const parsed = WebFetchArgs ( rawArgs ) ;
54+ if ( parsed instanceof type . errors ) {
55+ return {
56+ ok : false ,
57+ error :
58+ "Error: web_fetch requires a non-empty url (http/https); format and timeout are optional." ,
59+ } ;
60+ }
61+ return {
62+ ok : true ,
63+ url : parsed . url ,
64+ format : parsed . format ?? "markdown" ,
65+ timeout : parsed . timeout ?? DEFAULT_TIMEOUT_S ,
66+ } ;
67+ }
68+
69+ function truncationSuffix ( truncated : boolean ) : string {
70+ return truncated ? `\n\n[content truncated at ${ MAX_FETCH_BYTES } bytes]` : "" ;
71+ }
72+
73+ function fetchTimeoutMessage ( url : string , seconds : number ) : string {
74+ return `Request to ${ url } timed out after ${ seconds } s. Retry with a larger timeout parameter (up to 120s) if the site is slow.` ;
75+ }
76+
4677export const webFetchDefinition : ToolDefinition = {
4778 name : "web_fetch" ,
4879 description :
@@ -166,7 +197,7 @@ export async function runWebFetch(
166197 if ( err instanceof Error && err . name === "AbortError" ) {
167198 return {
168199 ok : false ,
169- error : `Request to ${ currentUrl } timed out after ${ clampedSeconds } s. Retry with a larger timeout parameter (up to 120s) if the site is slow.` ,
200+ error : fetchTimeoutMessage ( currentUrl , clampedSeconds ) ,
170201 } ;
171202 }
172203 return {
@@ -233,18 +264,16 @@ export function createWebFetchTool(options?: WebFetchTimingOptions): AgentTool {
233264 return stringTool ( {
234265 definition : webFetchDefinition ,
235266 handler : async ( rawArgs : Record < string , unknown > ) : Promise < string > => {
236- const parsed = WebFetchArgs ( rawArgs ) ;
237- if ( parsed instanceof type . errors ) {
238- return "Error: web_fetch requires a non-empty url (http/https); format and timeout are optional." ;
239- }
240- const format = parsed . format ?? "markdown" ;
241- const timeout = parsed . timeout ?? DEFAULT_TIMEOUT_S ;
242- const outcome = await runWebFetch ( parsed . url , format , timeout , options ) ;
267+ const parsed = parseWebFetchArgs ( rawArgs ) ;
268+ if ( ! parsed . ok ) return parsed . error ;
269+ const outcome = await runWebFetch (
270+ parsed . url ,
271+ parsed . format ,
272+ parsed . timeout ,
273+ options ,
274+ ) ;
243275 if ( ! outcome . ok ) return `Error: ${ outcome . error } ` ;
244- const suffix = outcome . truncated
245- ? `\n\n[content truncated at ${ MAX_FETCH_BYTES } bytes]`
246- : "" ;
247- return outcome . content + suffix ;
276+ return outcome . content + truncationSuffix ( outcome . truncated ) ;
248277 } ,
249278 } ) ;
250279}
@@ -266,13 +295,9 @@ export function createExaMCPWebFetchTool(
266295 call : ToolCall ,
267296 signal : AbortSignal ,
268297 ) : Promise < ToolResult > => {
269- const parsed = WebFetchArgs ( call . arguments ) ;
270- if ( parsed instanceof type . errors ) {
271- return {
272- callId : call . id ,
273- content :
274- "Error: web_fetch requires a non-empty url (http/https); format and timeout are optional." ,
275- } ;
298+ const parsed = parseWebFetchArgs ( call . arguments ) ;
299+ if ( ! parsed . ok ) {
300+ return { callId : call . id , content : parsed . error } ;
276301 }
277302
278303 let url : URL ;
@@ -291,17 +316,17 @@ export function createExaMCPWebFetchTool(
291316 } ;
292317 }
293318
294- const format = parsed . format ?? "markdown" ;
295- const timeout = parsed . timeout ?? DEFAULT_TIMEOUT_S ;
319+ const format = parsed . format ;
320+ const timeout = parsed . timeout ;
296321 if ( format !== "markdown" ) {
297322 const outcome = await runWebFetch ( parsed . url , format , timeout ) ;
298323 if ( ! outcome . ok ) {
299324 return { callId : call . id , content : `Error: ${ outcome . error } ` } ;
300325 }
301- const suffix = outcome . truncated
302- ? `\n\n[content truncated at ${ MAX_FETCH_BYTES } bytes]`
303- : "" ;
304- return { callId : call . id , content : outcome . content + suffix } ;
326+ return {
327+ callId : call . id ,
328+ content : outcome . content + truncationSuffix ( outcome . truncated ) ,
329+ } ;
305330 }
306331
307332 const timeoutSeconds = Math . min ( Math . max ( timeout , 1 ) , MAX_TIMEOUT_S ) ;
@@ -352,7 +377,10 @@ export function createExaMCPWebFetchTool(
352377 if ( err === timeoutError ) {
353378 return {
354379 callId : call . id ,
355- content : `Error: Request to ${ parsed . url } timed out after ${ timeoutSeconds } s. Retry with a larger timeout parameter (up to 120s) if the site is slow.` ,
380+ content : `Error: ${ fetchTimeoutMessage (
381+ parsed . url ,
382+ timeoutSeconds ,
383+ ) } `,
356384 } ;
357385 }
358386 return {
0 commit comments