Fix MCP request hangs caused by agent interruption

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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-11-29 11:41:11 +00:00
co-authored by Claude
parent 7c1517d830
commit 6b9fbb83c0
2 changed files with 38 additions and 33 deletions
@@ -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 }),
+34 -33
View File
@@ -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<T>(value: T, rootLabel: string = "root"): T {
const seen = new Set<object>();
export function ensureValidJson<T>(value: T): T {
const seen = new WeakSet<object>();
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<string, unknown>;
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<string, JsonValue> = {};
for (const [key, val] of Object.entries(current as Record<string, unknown>)) {
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;
}