Skip to content

Route MCP transports through the hosted HttpClient#1102

Closed
RhysSullivan wants to merge 1 commit into
fix/oauth-hosted-egress-guardfrom
fix/mcp-hosted-egress-guard
Closed

Route MCP transports through the hosted HttpClient#1102
RhysSullivan wants to merge 1 commit into
fix/oauth-hosted-egress-guardfrom
fix/mcp-hosted-egress-guard

Conversation

@RhysSullivan

Copy link
Copy Markdown
Owner

What changed

  • Add an internal fetch adapter for MCP SSE and streamable HTTP transports that is backed by the executor HttpClient layer.
  • Thread the executor HTTP layer through MCP probe, detect, resolveTools, and invoke paths.
  • Extend the SDK resolve-tools input so plugins can receive the guarded layer consistently.

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.ts from packages/plugins/mcp
  • bun run --cwd packages/plugins/mcp typecheck
  • bun run --cwd packages/core/sdk typecheck

Stack

Base: fix/oauth-hosted-egress-guard

Previous: #1101

Next: fix/graphql-hosted-egress-guard

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud db1ee2c Jun 23 2026, 06:17 PM

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Jun 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1102

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1102

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1102

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1102

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1102

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1102

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1102

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1102

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1102

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1102

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1102

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1102

executor

npm i https://pkg.pr.new/executor@1102

commit: 9726c66

@RhysSullivan RhysSullivan force-pushed the fix/oauth-hosted-egress-guard branch from 586ae3b to fe019f2 Compare June 23, 2026 16:59
@RhysSullivan RhysSullivan force-pushed the fix/mcp-hosted-egress-guard branch 2 times, most recently from f3967a6 to 16eb505 Compare June 23, 2026 17:06
@RhysSullivan RhysSullivan force-pushed the fix/oauth-hosted-egress-guard branch from fe019f2 to 02ee1b1 Compare June 23, 2026 17:35
@RhysSullivan RhysSullivan force-pushed the fix/mcp-hosted-egress-guard branch from 16eb505 to 75ec6f1 Compare June 23, 2026 17:35
@RhysSullivan RhysSullivan force-pushed the fix/oauth-hosted-egress-guard branch from 02ee1b1 to 26b8f48 Compare June 23, 2026 18:00
@RhysSullivan RhysSullivan force-pushed the fix/mcp-hosted-egress-guard branch from 75ec6f1 to 9726c66 Compare June 23, 2026 18:01
@RhysSullivan RhysSullivan marked this pull request as ready for review June 23, 2026 18:02
@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown

Greptile Summary

This PR routes all remote MCP transport traffic (SSE and streamable HTTP) through the executor's HttpClient layer, ensuring hosted outbound egress policy is enforced for MCP connections that previously bypassed it by creating transports directly. The core mechanism is a FetchLike adapter (fetchFromHttpClientLayer in connection.ts) that wraps the Effect-based HttpClient into the standard fetch interface expected by the MCP SDK, and httpClientLayer is threaded through ResolveToolsInput, buildConnectorInput, probe, resolveTools, and invokeTool.

  • connection.ts: New fetchFromHttpClientLayer converts an Effect HttpClient layer to a FetchLike, handling body serialisation, header conversion, and abort signalling; passed into both StreamableHTTPClientTransport and SSEClientTransport.
  • plugin.ts / executor.ts / plugin.ts (core SDK): ResolveToolsInput gains an httpClientLayer field; the executor supplies runtime.ctx.httpClientLayer and MCP plugin plumbs it through all call sites.
  • Tests: New test in plugin.test.ts verifies the custom layer intercepts traffic; spec-blob.test.ts updated for the now-required httpClientLayer field in ResolveToolsInput.

Confidence Score: 4/5

Safe 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

Filename Overview
packages/plugins/mcp/src/sdk/connection.ts Adds fetchFromHttpClientLayer — a FetchLike adapter that wraps the Effect HttpClient — and threads it into both StreamableHTTPClientTransport and SSEClientTransport. The Promise.race abort pattern has a floating-rejection bug when the Effect completes before the signal fires.
packages/core/sdk/src/plugin.ts Adds httpClientLayer to ResolveToolsInput so plugins can receive the guarded HTTP layer during tool discovery. Straightforward interface extension.
packages/core/sdk/src/executor.ts Passes runtime.ctx.httpClientLayer into resolveTools call — minimal, correct plumbing.
packages/plugins/mcp/src/sdk/plugin.ts Threads httpClientLayer through probe, resolveTools, and invokeTool paths. invokeTool correctly applies options?.httpClientLayer override; resolveTools uses the layer injected from ResolveToolsInput directly without applying the plugin-level override (noted in a prior review).
packages/plugins/mcp/src/sdk/plugin.test.ts Adds a test verifying that createMcpConnector routes traffic through the provided HttpClient layer by intercepting a streamable-HTTP connection with a 403-returning client and asserting the URL was seen.
packages/plugins/openapi/src/sdk/spec-blob.test.ts Fixes the test to supply the now-required httpClientLayer field in ResolveToolsInput. Mechanical update only.

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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "Route MCP transports through HttpClient ..." | Re-trigger Greptile

Comment on lines +145 to +155
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

@RhysSullivan RhysSullivan force-pushed the fix/oauth-hosted-egress-guard branch from 26b8f48 to f5906e3 Compare June 23, 2026 18:09
@RhysSullivan

Copy link
Copy Markdown
Owner Author

Superseded by batch merge #1106.

once: true,
});
});
return Promise.race([promise, aborted]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Suggested change
return Promise.race([promise, aborted]);
aborted.catch(() => {});
return Promise.race([promise, aborted]);

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