From 6b9fbb83c0c1ac13b0a2af1e6e42e82601d14e59 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 29 Nov 2025 11:41:11 +0000 Subject: [PATCH] Fix MCP request hangs caused by agent interruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes two root causes of MCP tool calls hanging indefinitely: 1. Transform ensureValidJson from validator to transformer - Previously threw errors on undefined values, causing handlers to crash - Now converts undefined→null, Date→ISO string, bigint→string, etc. - Ensures all MCP handlers always return valid JSON responses 2. Cancel waitTracker when interrupting agents - Added waitTracker.cancel() in cancel_agent handler - Added waitTracker.cancel() in kill_agent handler - Resolves waiting wait_for_agent promises when agents are interrupted - Prevents indefinite hangs requiring server restart 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../server/src/server/agent/mcp-server.ts | 4 ++ packages/server/src/server/json-utils.ts | 67 ++++++++++--------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index aa157252a..d8da5ceee 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -429,6 +429,9 @@ export async function createAgentMcpServer( }, async ({ agentId }) => { const success = await agentManager.cancelAgentRun(agentId); + if (success) { + waitTracker.cancel(agentId, "Agent run cancelled"); + } return { content: [], structuredContent: ensureValidJson({ success }), @@ -450,6 +453,7 @@ export async function createAgentMcpServer( }, async ({ agentId }) => { await agentManager.closeAgent(agentId); + waitTracker.cancel(agentId, "Agent terminated"); return { content: [], structuredContent: ensureValidJson({ success: true }), diff --git a/packages/server/src/server/json-utils.ts b/packages/server/src/server/json-utils.ts index 3e82e3e3e..0f9b2d8c0 100644 --- a/packages/server/src/server/json-utils.ts +++ b/packages/server/src/server/json-utils.ts @@ -7,54 +7,55 @@ export type JsonValue = | { [key: string]: JsonValue }; /** - * Ensure the provided value only contains JSON-safe primitives. - * Throws when undefined, functions, symbols, BigInts, etc. are found. + * Coerce arbitrary values into JSON-safe structures. + * Unlike the previous implementation, this never throws on undefined— + * it replaces unsupported values (undefined, functions, symbols) with null + * so responses always make it back to the client. */ -export function ensureValidJson(value: T, rootLabel: string = "root"): T { - const seen = new Set(); +export function ensureValidJson(value: T): T { + const seen = new WeakSet(); - const validate = (current: unknown, path: string): void => { - if (current === undefined) { - throw new Error(`Invalid JSON value at ${path}: undefined`); + const sanitize = (current: unknown): JsonValue => { + if (current === null || current === undefined) { + return null; } - if (current === null) { - return; + if (typeof current === "string" || typeof current === "number") { + return current; } - const type = typeof current; + if (typeof current === "boolean") { + return current; + } - if (type === "string" || type === "number" || type === "boolean") { - return; + if (typeof current === "bigint") { + return current.toString(); + } + + if (current instanceof Date) { + return current.toISOString(); } if (Array.isArray(current)) { - current.forEach((item, index) => - validate(item, `${path}[${index}]`) - ); - return; + return current.map((item) => sanitize(item)); } - if (type === "object") { - const asObject = current as Record; - if (!seen.has(asObject)) { - seen.add(asObject); - for (const [key, val] of Object.entries(asObject)) { - const nextPath = path === rootLabel ? key : `${path}.${key}`; - validate(val, nextPath); - } - seen.delete(asObject); + if (typeof current === "object") { + if (seen.has(current as object)) { + throw new Error("Cannot serialize circular structure to JSON"); } - return; + seen.add(current as object); + const obj: Record = {}; + for (const [key, val] of Object.entries(current as Record)) { + obj[key] = sanitize(val); + } + seen.delete(current as object); + return obj; } - throw new Error( - `Invalid JSON value at ${path}: ${ - type === "symbol" ? "symbol" : String(current) - }` - ); + // functions, symbols, undefined, etc. + return null; }; - validate(value, rootLabel); - return value; + return sanitize(value) as T; }