From 44ecfa39dff254486e713bd136ed4dbc833456dd Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 27 Aug 2026 12:42:48 -0700 Subject: [PATCH] Restore provider-independent web fetch results --- src/agent/exa-web-fetch-alias.test.ts | 10 +++-- src/tools/web-fetch.test.ts | 57 ++++++++++++++++++++++++++- src/tools/web-fetch.ts | 13 ++---- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/agent/exa-web-fetch-alias.test.ts b/src/agent/exa-web-fetch-alias.test.ts index 914777756..0b944688e 100644 --- a/src/agent/exa-web-fetch-alias.test.ts +++ b/src/agent/exa-web-fetch-alias.test.ts @@ -168,8 +168,10 @@ describe("built-in Exa web_fetch alias", () => { await connect(toolset); const result = await runTool(toolset, "web_fetch", { url: "ftp://example.com/file" }); - expect(result.isError).toBe(true); - expect(result.content).toContain("http or https"); + expect(result).not.toHaveProperty("isError"); + expect(result.content).toBe( + 'Error: Unsupported protocol "ftp:"; only http and https are allowed.', + ); expect(calls).toHaveLength(0); } finally { await toolset.dispose(); @@ -182,7 +184,7 @@ describe("built-in Exa web_fetch alias", () => { try { await connect(toolset); const result = await runTool(toolset, "web_fetch", { url: "https://example.com" }); - expect(result.isError).toBe(true); + expect(result).not.toHaveProperty("isError"); expect(result.content).toContain("Exa MCP"); expect(result.content).toContain("web_fetch_exa"); expect(calls).toHaveLength(0); @@ -195,7 +197,7 @@ describe("built-in Exa web_fetch alias", () => { try { await connect(failed); const result = await runTool(failed, "web_fetch", { url: "https://example.com" }); - expect(result.isError).toBe(true); + expect(result).not.toHaveProperty("isError"); expect(result.content).toContain("Exa MCP"); expect(result.content).toContain("connection exploded"); expect(calls).toHaveLength(0); diff --git a/src/tools/web-fetch.test.ts b/src/tools/web-fetch.test.ts index 0339424e5..f76702513 100644 --- a/src/tools/web-fetch.test.ts +++ b/src/tools/web-fetch.test.ts @@ -1,7 +1,13 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { createServer, type Server } from "node:http"; import type { MCPClient } from "../mcp/client.js"; -import { createExaMCPWebFetchTool, runWebFetch, MAX_FETCH_BYTES } from "./web-fetch.js"; +import { createDynamicToolRunner } from "../tui/dynamic-tool-runner.js"; +import { + createExaMCPWebFetchTool, + createWebFetchTool, + runWebFetch, + MAX_FETCH_BYTES, +} from "./web-fetch.js"; let server: Server; let baseUrl: string; @@ -154,10 +160,57 @@ describe("createExaMCPWebFetchTool", () => { callId: "timeout-call", content: "Error: Request to https://example.com timed out after 1s. Retry with a larger timeout parameter (up to 120s) if the site is slow.", - isError: true, }); }); + test("matches the complete native dynamic-runner result for protocol failures", async () => { + const exa = createTool(async () => "unused"); + const nativeRunner = createDynamicToolRunner([createWebFetchTool()]); + const exaRunner = createDynamicToolRunner([exa]); + const call = { + id: "protocol-call", + name: "web_fetch", + arguments: { url: "ftp://example.com/file" }, + }; + + const nativeResult = await nativeRunner.run(call, new AbortController().signal); + const exaResult = await exaRunner.run(call, new AbortController().signal); + + expect(nativeResult).toEqual({ + callId: "protocol-call", + content: 'Error: Unsupported protocol "ftp:"; only http and https are allowed.', + }); + expect(exaResult).toEqual(nativeResult); + }); + + test("matches the complete native dynamic-runner result for timeout failures", async () => { + handler = (_req, _res) => undefined; + const exa = createTool( + async (_name, _args, signal) => + new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }), + ); + const nativeRunner = createDynamicToolRunner([createWebFetchTool()]); + const exaRunner = createDynamicToolRunner([exa]); + const call = { + id: "timeout-call", + name: "web_fetch", + arguments: { url: `${baseUrl}/`, timeout: 1 }, + }; + + const [nativeResult, exaResult] = await Promise.all([ + nativeRunner.run(call, new AbortController().signal), + exaRunner.run(call, new AbortController().signal), + ]); + + expect(nativeResult).toEqual({ + callId: "timeout-call", + content: `Error: Request to ${baseUrl}/ timed out after 1s. Retry with a larger timeout parameter (up to 120s) if the site is slow.`, + }); + expect(exaResult).toEqual(nativeResult); + }); + test("returns distinct markdown, text, and html representations", async () => { handler = (_req, res) => { res.writeHead(200, { "content-type": "text/html" }); diff --git a/src/tools/web-fetch.ts b/src/tools/web-fetch.ts index 23a66fb21..6a85089ce 100644 --- a/src/tools/web-fetch.ts +++ b/src/tools/web-fetch.ts @@ -234,7 +234,6 @@ export function createExaMCPWebFetchTool(args: { callId: call.id, content: "Error: web_fetch requires a non-empty url (http/https); format and timeout are optional.", - isError: true, }; } @@ -244,15 +243,13 @@ export function createExaMCPWebFetchTool(args: { } catch { return { callId: call.id, - content: "Error: web_fetch URL must use http or https.", - isError: true, + content: `Error: Invalid URL: ${parsed.url}`, }; } if (url.protocol !== "http:" && url.protocol !== "https:") { return { callId: call.id, - content: "Error: web_fetch URL must use http or https.", - isError: true, + content: `Error: Unsupported protocol "${url.protocol}"; only http and https are allowed.`, }; } @@ -261,7 +258,7 @@ export function createExaMCPWebFetchTool(args: { if (format !== "markdown") { const outcome = await runWebFetch(parsed.url, format, timeout); if (!outcome.ok) { - return { callId: call.id, content: `Error: ${outcome.error}`, isError: true }; + return { callId: call.id, content: `Error: ${outcome.error}` }; } const suffix = outcome.truncated ? `\n\n[content truncated at ${MAX_FETCH_BYTES} bytes]` @@ -288,7 +285,6 @@ export function createExaMCPWebFetchTool(args: { return { callId: call.id, content: `Error: Exa MCP web_fetch unavailable: ${connection.error}`, - isError: true, }; } if (!connection.client.tools.some((tool) => tool.name === "web_fetch_exa")) { @@ -296,7 +292,6 @@ export function createExaMCPWebFetchTool(args: { callId: call.id, content: "Error: Exa MCP web_fetch unavailable: connected Exa server did not advertise web_fetch_exa.", - isError: true, }; } const content = await connection.client.call( @@ -313,13 +308,11 @@ export function createExaMCPWebFetchTool(args: { return { callId: call.id, 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.`, - isError: true, }; } return { callId: call.id, content: `Error: Exa MCP web_fetch failed: ${err instanceof Error ? err.message : String(err)}`, - isError: true, }; } finally { if (timer !== undefined) clearTimeout(timer);