Skip to content

Commit 3db2d98

Browse files
fix: Flush cancelled notification before closing Actor-MCP client
Yield a macrotask in the abort catch so notifications/cancelled can leave the wire before finally close() aborts the transport controller. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent cd5889c commit 3db2d98

3 files changed

Lines changed: 9 additions & 8 deletions

File tree

src/mcp/tool_dispatch.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,6 @@ export async function dispatchToolCall(params: {
181181
CallToolResultSchema,
182182
{
183183
timeout: EXTERNAL_TOOL_CALL_TIMEOUT_MSEC,
184-
// Same abort source as ACTOR/INTERNAL branches: request signal for sync,
185-
// cancel-watcher signal for tasks. Without this, cancel/disconnect cannot
186-
// stop the remote call before EXTERNAL_TOOL_CALL_TIMEOUT_MSEC.
187184
signal,
188185
},
189186
);
@@ -202,6 +199,11 @@ export async function dispatchToolCall(params: {
202199

203200
result = { ...res };
204201
} catch (error) {
202+
if (signal.aborted) {
203+
// Yield a macrotask first: the SDK sends notifications/cancelled fire-and-forget on
204+
// the transport's AbortController, which the finally's close() would abort.
205+
await new Promise((resolve) => setImmediate(resolve));
206+
}
205207
({ toolStatus, callDiagnostics } = buildExecutionDiagnostics({
206208
error,
207209
isAborted: Boolean(signal.aborted),

src/tools/actors/call_actor.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,10 +352,6 @@ export async function handleMcpToolCall(params: {
352352
return respondServerError(`Failed to connect to MCP server ${mcpServerUrl}`);
353353
}
354354

355-
if (signal.aborted) {
356-
return respondAborted();
357-
}
358-
359355
const result = await client.callTool(
360356
{
361357
name: mcpToolName,
@@ -389,6 +385,9 @@ export async function handleMcpToolCall(params: {
389385
});
390386
} catch (error) {
391387
if (signal.aborted) {
388+
// Yield a macrotask first: the SDK sends notifications/cancelled fire-and-forget on the
389+
// transport's AbortController, which the finally's close() would abort before it flushes.
390+
await new Promise((resolve) => setImmediate(resolve));
392391
return respondAborted();
393392
}
394393
logHttpError(error, `Failed to call MCP tool '${mcpToolName}' on Actor '${baseActorName}'`, {

tests/unit/tools.call_actor_common.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ describe('call_actor_common', () => {
370370
});
371371

372372
it('returns aborted when the signal is already aborted before the remote call', async () => {
373-
const connectSpy = vi.spyOn(mcpClient, 'connectMCPClient');
373+
const connectSpy = vi.spyOn(mcpClient, 'connectMCPClient').mockResolvedValue(null);
374374
const controller = new AbortController();
375375
controller.abort();
376376

0 commit comments

Comments
 (0)