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:
@@ -429,6 +429,9 @@ export async function createAgentMcpServer(
|
|||||||
},
|
},
|
||||||
async ({ agentId }) => {
|
async ({ agentId }) => {
|
||||||
const success = await agentManager.cancelAgentRun(agentId);
|
const success = await agentManager.cancelAgentRun(agentId);
|
||||||
|
if (success) {
|
||||||
|
waitTracker.cancel(agentId, "Agent run cancelled");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({ success }),
|
structuredContent: ensureValidJson({ success }),
|
||||||
@@ -450,6 +453,7 @@ export async function createAgentMcpServer(
|
|||||||
},
|
},
|
||||||
async ({ agentId }) => {
|
async ({ agentId }) => {
|
||||||
await agentManager.closeAgent(agentId);
|
await agentManager.closeAgent(agentId);
|
||||||
|
waitTracker.cancel(agentId, "Agent terminated");
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({ success: true }),
|
structuredContent: ensureValidJson({ success: true }),
|
||||||
|
|||||||
@@ -7,54 +7,55 @@ export type JsonValue =
|
|||||||
| { [key: string]: JsonValue };
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the provided value only contains JSON-safe primitives.
|
* Coerce arbitrary values into JSON-safe structures.
|
||||||
* Throws when undefined, functions, symbols, BigInts, etc. are found.
|
* 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 {
|
export function ensureValidJson<T>(value: T): T {
|
||||||
const seen = new Set<object>();
|
const seen = new WeakSet<object>();
|
||||||
|
|
||||||
const validate = (current: unknown, path: string): void => {
|
const sanitize = (current: unknown): JsonValue => {
|
||||||
if (current === undefined) {
|
if (current === null || current === undefined) {
|
||||||
throw new Error(`Invalid JSON value at ${path}: undefined`);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current === null) {
|
if (typeof current === "string" || typeof current === "number") {
|
||||||
return;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = typeof current;
|
if (typeof current === "boolean") {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "string" || type === "number" || type === "boolean") {
|
if (typeof current === "bigint") {
|
||||||
return;
|
return current.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current instanceof Date) {
|
||||||
|
return current.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
current.forEach((item, index) =>
|
return current.map((item) => sanitize(item));
|
||||||
validate(item, `${path}[${index}]`)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "object") {
|
if (typeof current === "object") {
|
||||||
const asObject = current as Record<string, unknown>;
|
if (seen.has(current as object)) {
|
||||||
if (!seen.has(asObject)) {
|
throw new Error("Cannot serialize circular structure to JSON");
|
||||||
seen.add(asObject);
|
|
||||||
for (const [key, val] of Object.entries(asObject)) {
|
|
||||||
const nextPath = path === rootLabel ? key : `${path}.${key}`;
|
|
||||||
validate(val, nextPath);
|
|
||||||
}
|
|
||||||
seen.delete(asObject);
|
|
||||||
}
|
}
|
||||||
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(
|
// functions, symbols, undefined, etc.
|
||||||
`Invalid JSON value at ${path}: ${
|
return null;
|
||||||
type === "symbol" ? "symbol" : String(current)
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
validate(value, rootLabel);
|
return sanitize(value) as T;
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user