Skip to content

feat(mcp): passthrough invoke calls the tool directly, without a dynamic worker - #36

Merged
fforres merged 2 commits into
skywardfrom
direct-invoke
Oct 22, 2026
Merged

fforres merged 2 commits into
skywardfrom
direct-invoke

Conversation

@fforres

@fforres fforres commented Oct 22, 2026

Copy link
Copy Markdown

What changes for users

Passthrough invoke (the tool Skywardin uses for every integration call) now calls the tool directly in the host. It no longer starts a new Cloudflare Dynamic Worker for each call.

  • Same results: same content blocks, same isError, same error texts for blocked tools, invalid arguments, declined approvals and plugin defects.
  • Faster calls: no isolate start, no TypeScript strip, no two RPC hops.
  • Cheaper: each invoke created one billed Dynamic Worker (executor-<random UUID>). That was about 19% of our billed Dynamic Workers (measured Oct 15-21).

Why it is safe

Policy, approval, credentials and the real upstream call already ran in the host through the tool invoker. The Dynamic Worker only ran a server-generated one-line script (return await tools["x"](args)) with globalOutbound: null that called back into the host. It isolated nothing. Upstream v2 has the same design (a direct call tool).

How

  • ExecutionEngine.call(address, args, { onElicitation }): required member, runs makeExecutorToolInvoker in an mcp.execute span (mcp.execute.mode: "direct"), maps ExecutionToolError to the same tool_error result the sandbox returned.
  • tool-server.ts: invoke uses engine.call. Visibility check, schema validation, elicitation, toPassthroughResult unchanged. passthroughCallCode removed.
  • Decorators forward call (analytics, promise surface, execution-stack middleware, cloud gate/usage/rate limit) and 13 test stub engines.

Behaviour differences

  • The in-isolate 60-minute cap no longer applies to invoke.
  • tool_blocked details now serialize with address/pattern (structured clone used to drop them). Message text unchanged.
  • executor.code.exec spans no longer appear for invoke.

Tests

  • engine.call: success + toolPaths, block policy, declined approval, opaque plugin defect, binary result; each with a code executor that dies if called.
  • Passthrough invoke on a real createExecutionEngine with a throwing code executor: ToolFile renders the same JSON text and structuredContent.
  • Existing passthrough error/image/elicitation/approval/schema tests pass unchanged.
  • execution 103/103, mcp 257/257, analytics 11/11, api 16/16, cloudflare 56/56, cloud 17/17.

Verified against production with a before/after benchmark (13 read-only tools, error cases, concurrency stress); results in the Skyward PR.

…mic worker

invoke used to run generated one-line code through engine.execute, which
starts a new sandbox (a billed dynamic worker on Cloudflare) only to RPC back
into the host. ExecutionEngine now has a required call(address, args) member
that runs the same tool invoker in the host and returns the same
ExecuteResult shape, so policy, approval, credentials, error mapping and
result rendering stay the same. Decorators (gate, rate limit, usage,
analytics, promise facade, read-only platform engine) forward and count it
like execute. passthroughCallCode is removed.
@fforres
fforres merged commit c49dd1b into skyward Oct 22, 2026
3 of 39 checks passed
@fforres
fforres deleted the direct-invoke branch October 22, 2026 22:28
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.

1 participant