Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) }));
Expand All @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions test/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
});
});

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down
Loading