From f92a9955ec4fc74e86bef12d356b7d8eb6312d26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:23:37 +0000 Subject: [PATCH 1/6] Initial plan From d77017af8e2daaf03ef324a4f3ae0ff05aa60769 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:40:58 +0000 Subject: [PATCH 2/6] Raise MCP logs bridge timeout beyond 120s cap Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 29 +++++++++++++++++++++++- actions/setup/js/mcp_cli_bridge.test.cjs | 14 +++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 9d56d00f17c..24eed01e669 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -45,6 +45,10 @@ const DEFAULT_HTTP_TIMEOUT_MS = 15000; /** Timeout (ms) for tool invocation calls (may be long-running) */ const TOOL_CALL_TIMEOUT_MS = 120000; +/** Default timeout (minutes) for logs MCP calls when timeout is not provided */ +const LOGS_TOOL_DEFAULT_TIMEOUT_MINUTES = 5; +/** Extra time (ms) to allow response marshalling/transport after tool execution */ +const TOOL_CALL_TIMEOUT_BUFFER_MS = 15000; /** Timeout (ms) for the notifications/initialized handshake step */ const NOTIFY_TIMEOUT_MS = 10000; @@ -331,6 +335,7 @@ async function mcpToolsCall(serverUrl, apiKey, sessionId, toolName, toolArgs, se headers["Mcp-Session-Id"] = sessionId; } + const callTimeoutMs = getToolCallTimeoutMs(toolName, toolArgs); const resp = await httpPostJSON( serverUrl, headers, @@ -340,7 +345,7 @@ async function mcpToolsCall(serverUrl, apiKey, sessionId, toolName, toolArgs, se method: "tools/call", params: { name: toolName, arguments: toolArgs }, }, - TOOL_CALL_TIMEOUT_MS + callTimeoutMs ); const elapsedMs = Date.now() - startMs; @@ -357,6 +362,27 @@ async function mcpToolsCall(serverUrl, apiKey, sessionId, toolName, toolArgs, se return resp; } +/** + * Resolve MCP bridge timeout for a tool call. + * + * The logs tool may legitimately run for multiple minutes. Match the caller's + * requested timeout when provided, with a small response buffer. + * + * @param {string} toolName + * @param {Record} toolArgs + * @returns {number} + */ +function getToolCallTimeoutMs(toolName, toolArgs) { + if (toolName !== "logs") { + return TOOL_CALL_TIMEOUT_MS; + } + + const timeoutCandidate = Number(toolArgs?.timeout); + const timeoutMinutes = Number.isFinite(timeoutCandidate) && timeoutCandidate > 0 ? timeoutCandidate : LOGS_TOOL_DEFAULT_TIMEOUT_MINUTES; + const resolvedTimeoutMs = Math.ceil(timeoutMinutes * 60 * 1000) + TOOL_CALL_TIMEOUT_BUFFER_MS; + return Math.max(TOOL_CALL_TIMEOUT_MS, resolvedTimeoutMs); +} + /** * Start periodic MCP ping requests to keep a session alive while a tool call runs. * @@ -1341,5 +1367,6 @@ module.exports = { hasStdinJsonPayload, readStdinSync, ensureSafeOutputsTools, + getToolCallTimeoutMs, main, }; diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index edfddc2fd69..2465896ab2b 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -3,7 +3,7 @@ import fs from "fs"; import os from "os"; import path from "path"; -import { ensureSafeOutputsTools, formatResponse, hasStdinJsonPayload, parseToolArgs, readStdinSync, shouldShowToolHelpForEmptyArgs, showHelp, showToolHelp, writeStdoutAndFlush } from "./mcp_cli_bridge.cjs"; +import { ensureSafeOutputsTools, formatResponse, getToolCallTimeoutMs, hasStdinJsonPayload, parseToolArgs, readStdinSync, shouldShowToolHelpForEmptyArgs, showHelp, showToolHelp, writeStdoutAndFlush } from "./mcp_cli_bridge.cjs"; describe("mcp_cli_bridge.cjs", () => { let originalCore; @@ -214,6 +214,18 @@ describe("mcp_cli_bridge.cjs", () => { }); }); + it("uses default 120s timeout for non-logs tools", () => { + expect(getToolCallTimeoutMs("audit", {})).toBe(120000); + }); + + it("uses a longer timeout for logs calls without explicit timeout", () => { + expect(getToolCallTimeoutMs("logs", {})).toBe(315000); + }); + + it("uses logs timeout argument with bridge buffer when provided", () => { + expect(getToolCallTimeoutMs("logs", { timeout: 10 })).toBe(615000); + }); + it("treats MCP result envelopes with isError=true as errors", async () => { await formatResponse( { From 213f745b10177c1424c13c4cf31d93ec57c29bda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:46:54 +0000 Subject: [PATCH 3/6] Add edge-case tests for MCP logs timeout resolution Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.test.cjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index 2465896ab2b..35f58ec0126 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -226,6 +226,16 @@ describe("mcp_cli_bridge.cjs", () => { expect(getToolCallTimeoutMs("logs", { timeout: 10 })).toBe(615000); }); + it("rounds logs timeout up to the next millisecond", () => { + expect(getToolCallTimeoutMs("logs", { timeout: 2.5 })).toBe(165000); + }); + + it("falls back to logs default timeout for invalid timeout values", () => { + expect(getToolCallTimeoutMs("logs", { timeout: 0 })).toBe(315000); + expect(getToolCallTimeoutMs("logs", { timeout: -5 })).toBe(315000); + expect(getToolCallTimeoutMs("logs", { timeout: "invalid" })).toBe(315000); + }); + it("treats MCP result envelopes with isError=true as errors", async () => { await formatResponse( { From 234d337fea78de6c2e1f679aae760d0f35ecfbaa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:15:15 +0000 Subject: [PATCH 4/6] Mirror server count/workflow_name timeout logic in bridge fallback Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 31 ++++++++++++++++++++---- actions/setup/js/mcp_cli_bridge.test.cjs | 26 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 24eed01e669..020c799a924 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -45,8 +45,12 @@ const DEFAULT_HTTP_TIMEOUT_MS = 15000; /** Timeout (ms) for tool invocation calls (may be long-running) */ const TOOL_CALL_TIMEOUT_MS = 120000; -/** Default timeout (minutes) for logs MCP calls when timeout is not provided */ -const LOGS_TOOL_DEFAULT_TIMEOUT_MINUTES = 5; +/** Default run count for logs MCP calls when count is not provided (mirrors server default) */ +const LOGS_TOOL_DEFAULT_COUNT = 100; +/** Number of runs per timeout minute for logs auto-scaling (mirrors server: ceil(count/40)) */ +const LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE = 40; +/** Minimum fallback timeout (minutes) for unfiltered logs calls (no workflow_name) */ +const LOGS_TOOL_MIN_TIMEOUT_MINUTES_NO_FILTER = 5; /** Extra time (ms) to allow response marshalling/transport after tool execution */ const TOOL_CALL_TIMEOUT_BUFFER_MS = 15000; @@ -365,8 +369,12 @@ async function mcpToolsCall(serverUrl, apiKey, sessionId, toolName, toolArgs, se /** * Resolve MCP bridge timeout for a tool call. * - * The logs tool may legitimately run for multiple minutes. Match the caller's - * requested timeout when provided, with a small response buffer. + * The logs tool may legitimately run for multiple minutes. When an explicit + * `timeout` argument is provided, the bridge derives its deadline from that + * value plus a response buffer. When `timeout` is omitted, the bridge mirrors + * the server's own auto-scaling rule: `ceil(count / 40)` minutes, with a + * minimum of 5 minutes when no `workflow_name` filter is supplied (matching + * `effectiveMCPLogsToolTimeoutMinutes` in `mcp_tools_privileged.go`). * * @param {string} toolName * @param {Record} toolArgs @@ -378,7 +386,20 @@ function getToolCallTimeoutMs(toolName, toolArgs) { } const timeoutCandidate = Number(toolArgs?.timeout); - const timeoutMinutes = Number.isFinite(timeoutCandidate) && timeoutCandidate > 0 ? timeoutCandidate : LOGS_TOOL_DEFAULT_TIMEOUT_MINUTES; + let timeoutMinutes; + if (Number.isFinite(timeoutCandidate) && timeoutCandidate > 0) { + // Explicit timeout provided — use it directly. + timeoutMinutes = timeoutCandidate; + } else { + // No explicit timeout: mirror the server's auto-scaling from count. + const countCandidate = Number(toolArgs?.count); + const effectiveCount = Number.isFinite(countCandidate) && countCandidate > 0 ? countCandidate : LOGS_TOOL_DEFAULT_COUNT; + const base = Math.max(1, Math.ceil(effectiveCount / LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE)); + // Without a workflow filter the GitHub API scans all runs and is substantially + // slower for large repositories — apply the same 5-minute floor as the server. + timeoutMinutes = toolArgs?.workflow_name ? base : Math.max(LOGS_TOOL_MIN_TIMEOUT_MINUTES_NO_FILTER, base); + } + const resolvedTimeoutMs = Math.ceil(timeoutMinutes * 60 * 1000) + TOOL_CALL_TIMEOUT_BUFFER_MS; return Math.max(TOOL_CALL_TIMEOUT_MS, resolvedTimeoutMs); } diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index 35f58ec0126..d34940ce341 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -218,10 +218,31 @@ describe("mcp_cli_bridge.cjs", () => { expect(getToolCallTimeoutMs("audit", {})).toBe(120000); }); - it("uses a longer timeout for logs calls without explicit timeout", () => { + it("uses a longer timeout for logs calls without explicit timeout (default count=100, no filter)", () => { + // effectiveCount=100, base=ceil(100/40)=3, no workflow_name → max(5,3)=5 minutes expect(getToolCallTimeoutMs("logs", {})).toBe(315000); }); + it("scales logs timeout from count when no explicit timeout is set (count=250, no filter)", () => { + // effectiveCount=250, base=ceil(250/40)=7, no workflow_name → max(5,7)=7 minutes + expect(getToolCallTimeoutMs("logs", { count: 250 })).toBe(435000); + }); + + it("scales logs timeout from count with workflow_name filter (count=250, filtered)", () => { + // effectiveCount=250, base=ceil(250/40)=7, workflow_name present → 7 minutes (no min floor applied) + expect(getToolCallTimeoutMs("logs", { count: 250, workflow_name: "ci" })).toBe(435000); + }); + + it("clamps count-based timeout to global minimum for small filtered counts", () => { + // effectiveCount=40, base=ceil(40/40)=1, workflow_name present → 1 minute → 75000ms < 120000ms → clamped + expect(getToolCallTimeoutMs("logs", { count: 40, workflow_name: "ci" })).toBe(120000); + }); + + it("applies 5-minute no-filter floor for small unfiltered counts", () => { + // effectiveCount=40, base=1, no workflow_name → max(5,1)=5 minutes + expect(getToolCallTimeoutMs("logs", { count: 40 })).toBe(315000); + }); + it("uses logs timeout argument with bridge buffer when provided", () => { expect(getToolCallTimeoutMs("logs", { timeout: 10 })).toBe(615000); }); @@ -230,7 +251,8 @@ describe("mcp_cli_bridge.cjs", () => { expect(getToolCallTimeoutMs("logs", { timeout: 2.5 })).toBe(165000); }); - it("falls back to logs default timeout for invalid timeout values", () => { + it("falls back to count-derived timeout for invalid timeout values", () => { + // Invalid timeouts fall back to count-based auto-scaling (default count=100 → 5 minutes) expect(getToolCallTimeoutMs("logs", { timeout: 0 })).toBe(315000); expect(getToolCallTimeoutMs("logs", { timeout: -5 })).toBe(315000); expect(getToolCallTimeoutMs("logs", { timeout: "invalid" })).toBe(315000); From 57a74f236b7ec7588cb9d2947506e60625899232 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:16:23 +0000 Subject: [PATCH 5/6] Simplify: remove redundant Math.max(1) floor in base computation Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index 020c799a924..f37b2bd8600 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -394,7 +394,7 @@ function getToolCallTimeoutMs(toolName, toolArgs) { // No explicit timeout: mirror the server's auto-scaling from count. const countCandidate = Number(toolArgs?.count); const effectiveCount = Number.isFinite(countCandidate) && countCandidate > 0 ? countCandidate : LOGS_TOOL_DEFAULT_COUNT; - const base = Math.max(1, Math.ceil(effectiveCount / LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE)); + const base = Math.ceil(effectiveCount / LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE); // Without a workflow filter the GitHub API scans all runs and is substantially // slower for large repositories — apply the same 5-minute floor as the server. timeoutMinutes = toolArgs?.workflow_name ? base : Math.max(LOGS_TOOL_MIN_TIMEOUT_MINUTES_NO_FILTER, base); From ef177f790a15ecb3cbdf225c62964423ad3a9158 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:13:38 +0000 Subject: [PATCH 6/6] Apply count-floor to explicit timeout, typeof guard, and max cap for logs Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/mcp_cli_bridge.cjs | 56 +++++++++++++++--------- actions/setup/js/mcp_cli_bridge.test.cjs | 17 +++++-- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/actions/setup/js/mcp_cli_bridge.cjs b/actions/setup/js/mcp_cli_bridge.cjs index f37b2bd8600..668938e7e19 100644 --- a/actions/setup/js/mcp_cli_bridge.cjs +++ b/actions/setup/js/mcp_cli_bridge.cjs @@ -51,6 +51,8 @@ const LOGS_TOOL_DEFAULT_COUNT = 100; const LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE = 40; /** Minimum fallback timeout (minutes) for unfiltered logs calls (no workflow_name) */ const LOGS_TOOL_MIN_TIMEOUT_MINUTES_NO_FILTER = 5; +/** Maximum allowed explicit timeout (minutes) for logs calls to prevent bridge-resource exhaustion */ +const LOGS_TOOL_MAX_EXPLICIT_TIMEOUT_MINUTES = 60; /** Extra time (ms) to allow response marshalling/transport after tool execution */ const TOOL_CALL_TIMEOUT_BUFFER_MS = 15000; @@ -369,39 +371,51 @@ async function mcpToolsCall(serverUrl, apiKey, sessionId, toolName, toolArgs, se /** * Resolve MCP bridge timeout for a tool call. * - * The logs tool may legitimately run for multiple minutes. When an explicit - * `timeout` argument is provided, the bridge derives its deadline from that - * value plus a response buffer. When `timeout` is omitted, the bridge mirrors - * the server's own auto-scaling rule: `ceil(count / 40)` minutes, with a - * minimum of 5 minutes when no `workflow_name` filter is supplied (matching - * `effectiveMCPLogsToolTimeoutMinutes` in `mcp_tools_privileged.go`). + * The logs tool may legitimately run for multiple minutes. The bridge always + * computes a count-derived floor that mirrors the server's own auto-scaling + * (`ceil(count / 40)` minutes, with a 5-minute minimum when no `workflow_name` + * filter is provided — matching `effectiveMCPLogsToolTimeoutMinutes` in + * `mcp_tools_privileged.go`). This floor applies to both the implicit path and + * any explicit `timeout` argument, so callers that pass a small positive value + * still receive a bridge deadline that is at least as long as the server's own + * expected runtime. Explicit timeouts are capped at + * `LOGS_TOOL_MAX_EXPLICIT_TIMEOUT_MINUTES` to prevent bridge-resource + * exhaustion from adversarial or misconfigured calls. The result is always + * at least `TOOL_CALL_TIMEOUT_MS` (120 s). * * @param {string} toolName * @param {Record} toolArgs - * @returns {number} + * @returns {number} Timeout in ms. Always at least TOOL_CALL_TIMEOUT_MS (120 s). */ function getToolCallTimeoutMs(toolName, toolArgs) { if (toolName !== "logs") { return TOOL_CALL_TIMEOUT_MS; } - const timeoutCandidate = Number(toolArgs?.timeout); - let timeoutMinutes; + // Compute the count-derived floor that mirrors the server's auto-scaling. + // Reject non-numeric types to avoid implicit coercion of booleans/strings. + const countCandidate = typeof toolArgs?.count === "number" ? toolArgs.count : NaN; + const effectiveCount = Number.isFinite(countCandidate) && countCandidate > 0 ? countCandidate : LOGS_TOOL_DEFAULT_COUNT; + const baseMinutes = Math.ceil(effectiveCount / LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE); + // Without a workflow filter the GitHub API scans all runs and is substantially + // slower for large repositories — apply the same 5-minute floor as the server. + const floorMinutes = toolArgs?.workflow_name ? baseMinutes : Math.max(LOGS_TOOL_MIN_TIMEOUT_MINUTES_NO_FILTER, baseMinutes); + const floorMs = Math.ceil(floorMinutes * 60 * 1000) + TOOL_CALL_TIMEOUT_BUFFER_MS; + + // Honor an explicit timeout when present. Reject non-numeric types (e.g. + // booleans, strings) so only real numeric values are accepted, and cap at + // LOGS_TOOL_MAX_EXPLICIT_TIMEOUT_MINUTES to bound bridge-resource use. + const timeoutCandidate = typeof toolArgs?.timeout === "number" ? toolArgs.timeout : NaN; if (Number.isFinite(timeoutCandidate) && timeoutCandidate > 0) { - // Explicit timeout provided — use it directly. - timeoutMinutes = timeoutCandidate; - } else { - // No explicit timeout: mirror the server's auto-scaling from count. - const countCandidate = Number(toolArgs?.count); - const effectiveCount = Number.isFinite(countCandidate) && countCandidate > 0 ? countCandidate : LOGS_TOOL_DEFAULT_COUNT; - const base = Math.ceil(effectiveCount / LOGS_TOOL_RUNS_PER_TIMEOUT_MINUTE); - // Without a workflow filter the GitHub API scans all runs and is substantially - // slower for large repositories — apply the same 5-minute floor as the server. - timeoutMinutes = toolArgs?.workflow_name ? base : Math.max(LOGS_TOOL_MIN_TIMEOUT_MINUTES_NO_FILTER, base); + const clampedMinutes = Math.min(timeoutCandidate, LOGS_TOOL_MAX_EXPLICIT_TIMEOUT_MINUTES); + const explicitMs = Math.ceil(clampedMinutes * 60 * 1000) + TOOL_CALL_TIMEOUT_BUFFER_MS; + // Floor to the count-derived minimum so tiny explicit values do not produce + // a misleadingly short bridge deadline; always at least TOOL_CALL_TIMEOUT_MS. + return Math.max(TOOL_CALL_TIMEOUT_MS, floorMs, explicitMs); } - const resolvedTimeoutMs = Math.ceil(timeoutMinutes * 60 * 1000) + TOOL_CALL_TIMEOUT_BUFFER_MS; - return Math.max(TOOL_CALL_TIMEOUT_MS, resolvedTimeoutMs); + // No valid explicit timeout: use the count-derived floor directly. + return Math.max(TOOL_CALL_TIMEOUT_MS, floorMs); } /** diff --git a/actions/setup/js/mcp_cli_bridge.test.cjs b/actions/setup/js/mcp_cli_bridge.test.cjs index d34940ce341..1478efe4b6f 100644 --- a/actions/setup/js/mcp_cli_bridge.test.cjs +++ b/actions/setup/js/mcp_cli_bridge.test.cjs @@ -244,18 +244,27 @@ describe("mcp_cli_bridge.cjs", () => { }); it("uses logs timeout argument with bridge buffer when provided", () => { + // timeout=10min, floor=5min (default count=100, no filter) → max(120000, 315000, 615000) = 615000 expect(getToolCallTimeoutMs("logs", { timeout: 10 })).toBe(615000); }); - it("rounds logs timeout up to the next millisecond", () => { - expect(getToolCallTimeoutMs("logs", { timeout: 2.5 })).toBe(165000); + it("floors small explicit timeout to the count-derived minimum", () => { + // timeout=2.5min → explicit=165000ms; floor=5min → 315000ms; floor wins + expect(getToolCallTimeoutMs("logs", { timeout: 2.5 })).toBe(315000); }); - it("falls back to count-derived timeout for invalid timeout values", () => { - // Invalid timeouts fall back to count-based auto-scaling (default count=100 → 5 minutes) + it("caps explicit timeout at LOGS_TOOL_MAX_EXPLICIT_TIMEOUT_MINUTES (60)", () => { + // timeout=999min → clamped to 60min → 3615000ms; floor=315000ms; capped value wins + expect(getToolCallTimeoutMs("logs", { timeout: 999 })).toBe(3615000); + }); + + it("rejects non-numeric timeout types and falls back to count-derived timeout", () => { + // typeof-check rejects strings and booleans even when Number() would accept them expect(getToolCallTimeoutMs("logs", { timeout: 0 })).toBe(315000); expect(getToolCallTimeoutMs("logs", { timeout: -5 })).toBe(315000); expect(getToolCallTimeoutMs("logs", { timeout: "invalid" })).toBe(315000); + expect(getToolCallTimeoutMs("logs", { timeout: "5" })).toBe(315000); + expect(getToolCallTimeoutMs("logs", { timeout: true })).toBe(315000); }); it("treats MCP result envelopes with isError=true as errors", async () => {