Skip to content

fix: Honor AbortSignal on Actor-MCP calls and hash-dedupe proxy names - #1185

Merged
vojtechj-apify merged 2 commits into
apify:masterfrom
Ayush7614:fix/actor-mcp-abort-and-proxy-names
Aug 4, 2026
Merged

fix: Honor AbortSignal on Actor-MCP calls and hash-dedupe proxy names#1185
vojtechj-apify merged 2 commits into
apify:masterfrom
Ayush7614:fix/actor-mcp-abort-and-proxy-names

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Forward the request/cancel AbortSignal into remote Actor-MCP client.callTool (dispatch + call-actor passthrough) so cancel/disconnect can stop remote work before the 120s timeout.
  • Yield a macrotask in the abort catch before client.close() so the SDK's fire-and-forget notifications/cancelled can flush (otherwise close() aborts the same transport AbortController and the remote never sees cancel).
  • Hash-suffix over-length proxied MCP tool names (same pattern as Actor tool names) so bare truncation cannot collide two origin tools into one exposed name.

Notes

  • handleMcpToolCall now passes EXTERNAL_TOOL_CALL_TIMEOUT_MSEC (120s), matching the dispatch path. That raises this route from the SDK default 60s to 120s for parity.

Why

Actor and internal tools already honor abort. Actor-MCP only checked signal.aborted in the catch after failure — the SDK call never received signal. Proxy naming documented the collision risk and violated the capped + hash-deduped tool-name invariant in src/mcp/AGENTS.md.

Test plan

  • pnpm run type-check
  • pnpm run lint
  • pnpm run test:unit (new proxy naming tests + AbortSignal forwarding assertions)
  • pnpm run format / pnpm run check:agents
  • Optional: mcpc cancel against a long Actor-MCP tool call and confirm remote work stops early (and that notifications/cancelled reaches the remote)

Remote Actor-MCP callTool ignored cancel/disconnect, and bare truncation could collide over-length proxied tool names.
@Ayush7614
Ayush7614 force-pushed the fix/actor-mcp-abort-and-proxy-names branch 2 times, most recently from fc78876 to cd5889c Compare July 30, 2026 15:48
@jirispilka
jirispilka requested review from jirispilka and vojtechj-apify and removed request for jirispilka July 31, 2026 21:47
@vojtechj-apify
vojtechj-apify requested a review from Copilot August 3, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Ready to approve

The changes are minimal, match stated invariants, and are backed by focused unit/contract tests for both abort propagation and proxy name deduping.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR fixes two correctness issues in the MCP proxying path: (1) Actor-MCP tool calls now receive the request/task AbortSignal so cancellations can stop remote work before the 120s timeout, and (2) proxied Actor-MCP tool names now follow the repo’s “cap + hash-suffix” invariant to avoid truncation collisions.

Changes:

  • Forward AbortSignal (and timeout) into remote client.callTool(...) options for Actor-MCP calls (both dispatchToolCall and call-actor passthrough).
  • Hash-suffix over-length proxied MCP tool names to prevent collisions caused by bare truncation.
  • Add/extend unit tests to assert signal forwarding behavior and proxy name hashing/deduping.
File summaries
File Description
tests/unit/tools.call_actor_common.test.ts Adds unit coverage for handleMcpToolCall() signal/timeout forwarding and abort handling.
tests/unit/mcp.server.tool_call_contracts.test.ts Adds contract test ensuring tools/call forwards abort signal into Actor-MCP callTool options.
tests/unit/mcp.proxy.test.ts New tests for stable server IDs and hash-suffixed proxy tool naming under the length cap.
src/tools/actors/call_actor.ts Passes signal + timeout into remote Actor-MCP callTool and returns aborted early when applicable.
src/mcp/tool_dispatch.ts Forwards signal into Actor-MCP callTool options (sync + task mode).
src/mcp/proxy.ts Implements hash-suffixing for over-length proxy tool names and exports getProxyMCPServerToolName.
src/mcp/AGENTS.md Updates docs to reflect proxy tool naming invariant and new helper export.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@vojtechj-apify vojtechj-apify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @Ayush7614 - this found two real problems, and the tool-name fix is straightforwardly correct.

The abort forwarding is right too, and it's a real improvement: cancel now returns immediately instead of holding the call for up to 120s. One piece is missing, and it isn't visible from the client side - the explicit notifications/cancelled never reaches the remote, because client.close() in the finally aborts the same AbortController the notification rides on. I caught it by standing up a live remote and logging what arrived. The SDK only documents signal as raising an AbortError locally, so you got exactly what's specified.

Two comments below with the fix - one macrotask yield in each catch. Verified against a real remote: cancellation delivered, remote handler aborts, no added latency, unit suite green. This same race silently breaks the pre-existing 120s timeout path too, so your PR plus this patch fixes something older than your change.

One thing to note in the description: passing EXTERNAL_TOOL_CALL_TIMEOUT_MSEC in handleMcpToolCall moves that call from the SDK default 60s to 120s. Parity with the dispatch path makes sense, but it doubles worst-case latency on that route, so it's worth stating.

Two optional nits as suggestions, plus one on a test. Take or leave them.

Process note, and the timing is on us: we published new contribution guidelines a few days after you opened this - issue-first, AI disclosure, evidence. Not applying them retroactively. Worth a read before the next one: CONTRIBUTING.md

Comment thread src/tools/actors/call_actor.ts
Comment thread src/mcp/tool_dispatch.ts
Comment thread src/tools/actors/call_actor.ts Outdated
Comment thread src/mcp/tool_dispatch.ts Outdated
Comment thread tests/unit/tools.call_actor_common.test.ts
Yield a macrotask in the abort catch so notifications/cancelled can leave the wire before finally close() aborts the transport controller.
@Ayush7614
Ayush7614 force-pushed the fix/actor-mcp-abort-and-proxy-names branch from 3db2d98 to a0a108c Compare August 4, 2026 10:53
@Ayush7614

Copy link
Copy Markdown
Contributor Author

Thanks @vojtechj-apify — addressed in a0a108c.

Required fix: macrotask yield (setImmediate) in both abort catches (handleMcpToolCall + dispatchToolCall ACTOR_MCP branch) so notifications/cancelled can flush before finally client.close() aborts the transport controller.

Nits taken:

  • Dropped the redundant post-connect abort check
  • Removed the stale signal comment in tool_dispatch
  • Stubbed connectMCPClient with .mockResolvedValue(null) in the already-aborted test

PR description: noted that handleMcpToolCall now uses EXTERNAL_TOOL_CALL_TIMEOUT_MSEC (120s) for parity with dispatch (was SDK default 60s).

Unit suite still green.

@vojtechj-apify vojtechj-apify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix, looks all good now!

@vojtechj-apify
vojtechj-apify merged commit 7b24c9e into apify:master Aug 4, 2026
15 checks passed
vojtechj-apify added a commit that referenced this pull request Aug 4, 2026
On abort the ACTOR_MCP branch fell through its generic catch, producing an
error result body, failure_category INTERNAL_ERROR and failure_detail, and
a failure log — at error level when the abort landed while connecting, where
callTool throws a bare DOMException that no logHttpError branch matches.

Return result = {} and break instead, matching the ACTOR branch. tool_status
was already ABORTED via buildExecutionDiagnostics; what changes is the body,
the failure_* fields and the log. Reachable since #1185 started passing the
abort signal into client.callTool().
vojtechj-apify added a commit that referenced this pull request Aug 5, 2026
## Why

Cancelling an Actor-MCP tool call is reported as a failure. Probed
against a local MCP server: an error result body and `failure_category:
INTERNAL_ERROR`, plus a failure log. (`tool_status` was already
`ABORTED` — `buildExecutionDiagnostics` derives it from `isAborted` — so
the status was never the problem.)

An abort mid-call throws `McpError(-32001)`, which is in
`SOFT_MCP_ERROR_CODES`, so `log.softFail`. An abort that lands while
`connectMCPClient` is in flight is worse: `connectMCPClient` itself
never throws on abort, but the subsequent `client.callTool` calls
`signal.throwIfAborted()` and raises a bare `DOMException`
(`AbortError`, `code: 20`) that no `logHttpError` branch matches — 20 is
outside 100–600 and −32768..−32000, and "This operation was aborted"
fits neither transient pattern — so it reaches the final `log.error`.
Alert level, on a user pressing cancel.

Reachable since #1185 started passing the abort signal into
`client.callTool()`. In `@apify/actors-mcp-server 0.14.2-beta.2`;
`latest` is still `0.14.1`.

## What changed

Before: an aborted `ACTOR_MCP` call fell through the generic catch to
`buildExecutionDiagnostics` and `logHttpError`.

Now: it returns `result = {}` and breaks, matching the `ACTOR` branch.
`tool_status` stays `ABORTED`; the error body, the `failure_*` fields
and the log go away.

Also updated two comments in the same file that described the catch as
always producing a soft-fail `isError` body.

## Notes for reviewers

The one decision here is logging nothing on a cancel. That matches the
`ACTOR` branch, but if you'd rather a cancellation left a debug trace,
that's a one-line change.

Known trade-off: the guard keys on `signal.aborted`, not on the error
being abort-caused. So a genuine remote failure that races with a cancel
is now dropped with no log, where before it was misfiled as
`INTERNAL_ERROR` but at least logged. Keying on the signal is the
existing convention here (`handleMcpToolCall` and
`buildExecutionDiagnostics` both do it), so I left it alone — say the
word if you'd rather narrow it to abort-shaped errors.

Telemetry shifts: `failure_category` and `failure_detail` are no longer
emitted for these. Anything grouping cancelled Actor-MCP calls under
`INTERNAL_ERROR` will see that bucket empty out — intended, but worth
telling whoever owns those charts. There is no cancel/abort value in
`FAILURE_CATEGORY`, and the three `ABORTED` sites in `task_execution.ts`
already emit actor fields only, so unset is the existing convention.

Task mode is unaffected: the cancel-watcher signal only aborts after the
store shows the task cancelled, so `skipIfTaskCancelled` short-circuits
before the `{}` could be stored.

No `apify-mcp-server-internal` surface touched — no `internals.js`
exports, `_meta`, `structuredContent`, or `?ui=`/`?payment=` parsing.

## Proof it works

Probe against a real SDK `Server` with a hanging tool, cancelled
mid-call:

```
before:  toolStatus=ABORTED  errorBody=true   failure_category=INTERNAL_ERROR
after:   toolStatus=ABORTED  errorBody=false  failure_category=<unset>
```

New unit test asserts the aborted result carries no error body and that
`logHttpError` is not called; both assertions fail independently when
the guard is removed. Local gates: `type-check`, `lint`, `format`,
`check:agents`, 1268 unit tests.

Found and fixed with Claude Code while reviewing #1185; a second model
did an adversarial pass over the diff and description, which is where
the title correction and the stale comments came from.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants