Route MCP transports through the hosted HttpClient#1102
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | db1ee2c | Commit Preview URL Branch Preview URL |
Jun 23 2026, 06:14 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | db1ee2c | Jun 23 2026, 06:17 PM |
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
586ae3b to
fe019f2
Compare
f3967a6 to
16eb505
Compare
fe019f2 to
02ee1b1
Compare
16eb505 to
75ec6f1
Compare
02ee1b1 to
26b8f48
Compare
75ec6f1 to
9726c66
Compare
Greptile SummaryThis PR routes all remote MCP transport traffic (SSE and streamable HTTP) through the executor's
Confidence Score: 4/5Safe to merge with the floating-rejection fix in connection.ts; all other changes are clean plumbing. The fetchFromHttpClientLayer abort path has a real defect: when the Effect request completes before abort() fires (the normal SSE teardown sequence), the floating aborted promise rejects without a handler, which can trigger unhandledRejection in production runtimes configured to treat that as fatal. packages/plugins/mcp/src/sdk/connection.ts — the Promise.race abort pattern at line 155 needs the floating-rejection fix before the SSE teardown path is safe in production. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Executor
participant McpPlugin
participant fetchFromHttpClientLayer
participant HttpClientLayer
participant McpSDKTransport
Executor->>McpPlugin: "resolveTools({ httpClientLayer })"
McpPlugin->>McpPlugin: buildConnectorInput(..., httpClientLayer)
McpPlugin->>McpSDKTransport: "createMcpConnector({ httpClientLayer })"
Note over McpSDKTransport: SSEClientTransport / StreamableHTTPClientTransport now hold a custom fetch
McpSDKTransport->>fetchFromHttpClientLayer: fetch(url, init)
fetchFromHttpClientLayer->>HttpClientLayer: HttpClient.execute(request)
HttpClientLayer-->>fetchFromHttpClientLayer: HttpClientResponse
fetchFromHttpClientLayer-->>McpSDKTransport: Web Response
Executor->>McpPlugin: "invokeTool({ ctx })"
McpPlugin->>McpPlugin: options?.httpClientLayer ?? ctx.httpClientLayer
McpPlugin->>McpSDKTransport: "createMcpConnector({ httpClientLayer })"
McpSDKTransport->>fetchFromHttpClientLayer: fetch(url, init)
fetchFromHttpClientLayer->>HttpClientLayer: HttpClient.execute(request)
HttpClientLayer-->>fetchFromHttpClientLayer: HttpClientResponse
fetchFromHttpClientLayer-->>McpSDKTransport: Web Response
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Executor
participant McpPlugin
participant fetchFromHttpClientLayer
participant HttpClientLayer
participant McpSDKTransport
Executor->>McpPlugin: "resolveTools({ httpClientLayer })"
McpPlugin->>McpPlugin: buildConnectorInput(..., httpClientLayer)
McpPlugin->>McpSDKTransport: "createMcpConnector({ httpClientLayer })"
Note over McpSDKTransport: SSEClientTransport / StreamableHTTPClientTransport now hold a custom fetch
McpSDKTransport->>fetchFromHttpClientLayer: fetch(url, init)
fetchFromHttpClientLayer->>HttpClientLayer: HttpClient.execute(request)
HttpClientLayer-->>fetchFromHttpClientLayer: HttpClientResponse
fetchFromHttpClientLayer-->>McpSDKTransport: Web Response
Executor->>McpPlugin: "invokeTool({ ctx })"
McpPlugin->>McpPlugin: options?.httpClientLayer ?? ctx.httpClientLayer
McpPlugin->>McpSDKTransport: "createMcpConnector({ httpClientLayer })"
McpSDKTransport->>fetchFromHttpClientLayer: fetch(url, init)
fetchFromHttpClientLayer->>HttpClientLayer: HttpClient.execute(request)
HttpClientLayer-->>fetchFromHttpClientLayer: HttpClientResponse
fetchFromHttpClientLayer-->>McpSDKTransport: Web Response
Reviews (2): Last reviewed commit: "Route MCP transports through HttpClient ..." | Re-trigger Greptile |
| const promise = Effect.runPromise(effect); | ||
| if (!init?.signal) return promise; | ||
| // oxlint-disable-next-line executor/no-promise-reject -- boundary: Fetch-compatible adapter mirrors abort rejection semantics | ||
| if (init.signal.aborted) return Promise.reject(abortError(init.signal)); | ||
| const aborted = new Promise<never>((_, reject) => { | ||
| // oxlint-disable-next-line executor/no-promise-reject -- boundary: Fetch-compatible adapter races the Effect request against AbortSignal | ||
| init.signal?.addEventListener("abort", () => reject(abortError(init.signal!)), { | ||
| once: true, | ||
| }); | ||
| }); | ||
| return Promise.race([promise, aborted]); |
There was a problem hiding this comment.
AbortSignal races the promise but does not cancel the underlying Effect fiber
When signal aborts and aborted wins Promise.race, the function rejects correctly for the caller — but Effect.runPromise(effect) continues executing on its internal fiber until the request completes or fails. For SSE connections, which hold a long-lived stream, this means the stream is still consumed and the HTTP connection is still held open even after the MCP transport has declared the connection aborted. The fix requires spawning a fiber explicitly so it can be interrupted on abort:
const fiber = Effect.runFork(effect);
init.signal?.addEventListener("abort", () => Fiber.interrupt(fiber), { once: true });
return Effect.runPromise(Fiber.join(fiber));Without interruption, every aborted MCP connection leaks an open HTTP request for its remaining lifetime.
26b8f48 to
f5906e3
Compare
9726c66 to
db1ee2c
Compare
|
Superseded by batch merge #1106. |
| once: true, | ||
| }); | ||
| }); | ||
| return Promise.race([promise, aborted]); |
There was a problem hiding this comment.
Floating promise rejection when the request wins the race and the signal is later aborted. When
promise resolves first, aborted is left pending. If init.signal.abort() is subsequently called (which is the normal SSE teardown path — SSEClientTransport calls controller.abort() via transport.close()), the listener fires, aborted rejects, and there is no handler on it, triggering an unhandledRejection event. Attaching a no-op .catch silences it without affecting the race's already-settled outcome.
| return Promise.race([promise, aborted]); | |
| aborted.catch(() => {}); | |
| return Promise.race([promise, aborted]); |
What changed
HttpClientlayer.Why
Remote MCP setup and invocation created upstream MCP transports directly, bypassing hosted outbound egress policy.
Validation
bun --bun vitest run src/sdk/plugin.test.ts src/sdk/invoke.test.ts src/sdk/probe-shape.test.tsfrompackages/plugins/mcpbun run --cwd packages/plugins/mcp typecheckbun run --cwd packages/core/sdk typecheckStack
Base:
fix/oauth-hosted-egress-guardPrevious: #1101
Next:
fix/graphql-hosted-egress-guard