diff --git a/src/bridge.ts b/src/bridge.ts index a76401b..e565521 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -228,6 +228,16 @@ function getToolContent(result: unknown): BridgeContentBlock[] { return result.content as BridgeContentBlock[]; } +/** MCP CallToolResult.isError — set when the tool itself failed. */ +function isErrorResult(result: unknown): boolean { + return ( + !!result && + typeof result === "object" && + "isError" in result && + result.isError === true + ); +} + export function parseBridgeCallPayload(body: string): BridgeCallPayload { let payload: { name?: unknown; args?: unknown }; try { @@ -477,8 +487,13 @@ async function handleCallRequest( try { const result = await callPromise; const text = extractToolText(getToolContent(result)); - res.statusCode = 200; - res.end(JSON.stringify({ result: text })); + if (isErrorResult(result)) { + res.statusCode = 500; + res.end(JSON.stringify({ error: text || "Tool call failed" })); + } else { + res.statusCode = 200; + res.end(JSON.stringify({ result: text })); + } } catch (error) { res.statusCode = 500; res.end(JSON.stringify({ error: getErrorMessage(error) })); @@ -496,6 +511,11 @@ async function handleCallRequest( undefined, ); const text = extractToolText(getToolContent(result)); + if (isErrorResult(result)) { + res.statusCode = 500; + res.end(JSON.stringify({ error: text || "Tool call failed" })); + return; + } if (payload.name === "take_snapshot") { lastSnapshot = { raw: text, diff --git a/test/bridge.test.ts b/test/bridge.test.ts index 7204879..3377329 100644 --- a/test/bridge.test.ts +++ b/test/bridge.test.ts @@ -599,6 +599,26 @@ describe("handleBridgeRequest access control", () => { await handleBridgeRequest(okClient, bad, badMock.res, undefined, "secret"); expect(badMock.res.statusCode).toBe(403); }); + + it("returns a 500 when the tool result carries isError, even without a thrown error", async () => { + const failingClient: BridgeClient = { + ...okClient, + callTool: async () => ({ + isError: true, + content: [{ type: "text", text: "element uid not found" }], + }), + }; + const req = makeMockRequest( + "POST", + "/call", + JSON.stringify({ name: "click", args: { uid: "1_0" } }), + { host: "127.0.0.1:9225", authorization: "Bearer secret" }, + ); + const mock = makeMockResponse(); + await handleBridgeRequest(failingClient, req, mock.res, undefined, "secret"); + expect(mock.res.statusCode).toBe(500); + expect(JSON.parse(mock.endPayload)).toEqual({ error: "element uid not found" }); + }); }); // --------------------------------------------------------------------------- @@ -639,6 +659,26 @@ describe("handleBridgeRequest streaming", () => { expect(JSON.parse(mock.endPayload)).toEqual({ error: "browser crashed" }); }); + it("returns a 500 when a streaming tool result carries isError", async () => { + const client: BridgeClient = { + listTools: async () => ({ tools: [] }), + callTool: async () => ({ + isError: true, + content: [{ type: "text", text: "AI access is blocked on this site" }], + }), + close: async () => {}, + }; + + const captureNextId = () => Promise.resolve("req-isError"); + const req = makeMockRequest("POST", "/call", JSON.stringify({ name: "opera_do", args: { prompt: "fail" } })); + const mock = makeMockResponse(); + + await handleBridgeRequest(client, req, mock.res, captureNextId); + + expect(mock.res.statusCode).toBe(500); + expect(JSON.parse(mock.endPayload)).toEqual({ error: "AI access is blocked on this site" }); + }); + it("routes concurrent streaming calls to their respective responses", async () => { let resolveA!: (v: unknown) => void; let resolveB!: (v: unknown) => void;