Skip to content

Commit 6a1dddb

Browse files
Merge pull request #701 from corbitsdev/fix/web-fetch-result-parity-0.3.7
Restore provider-independent web fetch results
2 parents 165acc0 + 2b79d54 commit 6a1dddb

3 files changed

Lines changed: 64 additions & 16 deletions

File tree

‎src/agent/exa-web-fetch-alias.test.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,10 @@ describe("built-in Exa web_fetch alias", () => {
168168
await connect(toolset);
169169
const result = await runTool(toolset, "web_fetch", { url: "ftp://example.com/file" });
170170

171-
expect(result.isError).toBe(true);
172-
expect(result.content).toContain("http or https");
171+
expect(result).not.toHaveProperty("isError");
172+
expect(result.content).toBe(
173+
'Error: Unsupported protocol "ftp:"; only http and https are allowed.',
174+
);
173175
expect(calls).toHaveLength(0);
174176
} finally {
175177
await toolset.dispose();
@@ -182,7 +184,7 @@ describe("built-in Exa web_fetch alias", () => {
182184
try {
183185
await connect(toolset);
184186
const result = await runTool(toolset, "web_fetch", { url: "https://example.com" });
185-
expect(result.isError).toBe(true);
187+
expect(result).not.toHaveProperty("isError");
186188
expect(result.content).toContain("Exa MCP");
187189
expect(result.content).toContain("web_fetch_exa");
188190
expect(calls).toHaveLength(0);
@@ -195,7 +197,7 @@ describe("built-in Exa web_fetch alias", () => {
195197
try {
196198
await connect(failed);
197199
const result = await runTool(failed, "web_fetch", { url: "https://example.com" });
198-
expect(result.isError).toBe(true);
200+
expect(result).not.toHaveProperty("isError");
199201
expect(result.content).toContain("Exa MCP");
200202
expect(result.content).toContain("connection exploded");
201203
expect(calls).toHaveLength(0);

‎src/tools/web-fetch.test.ts‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
22
import { createServer, type Server } from "node:http";
33
import type { MCPClient } from "../mcp/client.js";
4-
import { createExaMCPWebFetchTool, runWebFetch, MAX_FETCH_BYTES } from "./web-fetch.js";
4+
import { createDynamicToolRunner } from "../tui/dynamic-tool-runner.js";
5+
import {
6+
createExaMCPWebFetchTool,
7+
createWebFetchTool,
8+
runWebFetch,
9+
MAX_FETCH_BYTES,
10+
} from "./web-fetch.js";
511

612
let server: Server;
713
let baseUrl: string;
@@ -154,10 +160,57 @@ describe("createExaMCPWebFetchTool", () => {
154160
callId: "timeout-call",
155161
content:
156162
"Error: Request to https://example.com timed out after 1s. Retry with a larger timeout parameter (up to 120s) if the site is slow.",
157-
isError: true,
158163
});
159164
});
160165

166+
test("matches the complete native dynamic-runner result for protocol failures", async () => {
167+
const exa = createTool(async () => "unused");
168+
const nativeRunner = createDynamicToolRunner([createWebFetchTool()]);
169+
const exaRunner = createDynamicToolRunner([exa]);
170+
const call = {
171+
id: "protocol-call",
172+
name: "web_fetch",
173+
arguments: { url: "ftp://example.com/file" },
174+
};
175+
176+
const nativeResult = await nativeRunner.run(call, new AbortController().signal);
177+
const exaResult = await exaRunner.run(call, new AbortController().signal);
178+
179+
expect(nativeResult).toEqual({
180+
callId: "protocol-call",
181+
content: 'Error: Unsupported protocol "ftp:"; only http and https are allowed.',
182+
});
183+
expect(exaResult).toEqual(nativeResult);
184+
});
185+
186+
test("matches the complete native dynamic-runner result for timeout failures", async () => {
187+
handler = (_req, _res) => undefined;
188+
const exa = createTool(
189+
async (_name, _args, signal) =>
190+
new Promise<string>((_resolve, reject) => {
191+
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
192+
}),
193+
);
194+
const nativeRunner = createDynamicToolRunner([createWebFetchTool()]);
195+
const exaRunner = createDynamicToolRunner([exa]);
196+
const call = {
197+
id: "timeout-call",
198+
name: "web_fetch",
199+
arguments: { url: `${baseUrl}/`, timeout: 1 },
200+
};
201+
202+
const [nativeResult, exaResult] = await Promise.all([
203+
nativeRunner.run(call, new AbortController().signal),
204+
exaRunner.run(call, new AbortController().signal),
205+
]);
206+
207+
expect(nativeResult).toEqual({
208+
callId: "timeout-call",
209+
content: `Error: Request to ${baseUrl}/ timed out after 1s. Retry with a larger timeout parameter (up to 120s) if the site is slow.`,
210+
});
211+
expect(exaResult).toEqual(nativeResult);
212+
});
213+
161214
test("returns distinct markdown, text, and html representations", async () => {
162215
handler = (_req, res) => {
163216
res.writeHead(200, { "content-type": "text/html" });

‎src/tools/web-fetch.ts‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ export function createExaMCPWebFetchTool(args: {
234234
callId: call.id,
235235
content:
236236
"Error: web_fetch requires a non-empty url (http/https); format and timeout are optional.",
237-
isError: true,
238237
};
239238
}
240239

@@ -244,15 +243,13 @@ export function createExaMCPWebFetchTool(args: {
244243
} catch {
245244
return {
246245
callId: call.id,
247-
content: "Error: web_fetch URL must use http or https.",
248-
isError: true,
246+
content: `Error: Invalid URL: ${parsed.url}`,
249247
};
250248
}
251249
if (url.protocol !== "http:" && url.protocol !== "https:") {
252250
return {
253251
callId: call.id,
254-
content: "Error: web_fetch URL must use http or https.",
255-
isError: true,
252+
content: `Error: Unsupported protocol "${url.protocol}"; only http and https are allowed.`,
256253
};
257254
}
258255

@@ -261,7 +258,7 @@ export function createExaMCPWebFetchTool(args: {
261258
if (format !== "markdown") {
262259
const outcome = await runWebFetch(parsed.url, format, timeout);
263260
if (!outcome.ok) {
264-
return { callId: call.id, content: `Error: ${outcome.error}`, isError: true };
261+
return { callId: call.id, content: `Error: ${outcome.error}` };
265262
}
266263
const suffix = outcome.truncated
267264
? `\n\n[content truncated at ${MAX_FETCH_BYTES} bytes]`
@@ -288,15 +285,13 @@ export function createExaMCPWebFetchTool(args: {
288285
return {
289286
callId: call.id,
290287
content: `Error: Exa MCP web_fetch unavailable: ${connection.error}`,
291-
isError: true,
292288
};
293289
}
294290
if (!connection.client.tools.some((tool) => tool.name === "web_fetch_exa")) {
295291
return {
296292
callId: call.id,
297293
content:
298294
"Error: Exa MCP web_fetch unavailable: connected Exa server did not advertise web_fetch_exa.",
299-
isError: true,
300295
};
301296
}
302297
const content = await connection.client.call(
@@ -313,13 +308,11 @@ export function createExaMCPWebFetchTool(args: {
313308
return {
314309
callId: call.id,
315310
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.`,
316-
isError: true,
317311
};
318312
}
319313
return {
320314
callId: call.id,
321315
content: `Error: Exa MCP web_fetch failed: ${err instanceof Error ? err.message : String(err)}`,
322-
isError: true,
323316
};
324317
} finally {
325318
if (timer !== undefined) clearTimeout(timer);

0 commit comments

Comments
 (0)