Skip to content

fix(sdk): clean up sandbox when MCP gateway startup fails - #1548

Merged
mishushakov merged 3 commits into
mainfrom
mcp-gateway-startup-cleanup
Aug 7, 2026
Merged

mishushakov merged 3 commits into
mainfrom
mcp-gateway-startup-cleanup

Conversation

@mishushakov

@mishushakov mishushakov commented Jul 13, 2026 •

Copy link
Copy Markdown
Member

Problem

Fixes #1498.

Sandbox.create allocates a remote sandbox before starting mcp-gateway. If gateway startup fails, creation throws before the sandbox object is returned. As a result, the caller has no sandbox ID to clean up, and the orphaned sandbox continues consuming resources until it times out.

This state transition exists in synchronous Python, asynchronous Python, and JavaScript/TypeScript.

Changes

  • Add a rollback boundary around MCP gateway startup in all three SDK implementations: on failure, best-effort kill the newly allocated sandbox, then re-raise.
  • Surface gateway startup failure as SandboxError (JS) / SandboxException (Python) with a Failed to start MCP gateway: <stderr> message. Previously the intended message was unreachable dead code — foreground commands.run already throws on non-zero exit — so callers got a bare CommandExitError/CommandExitException.
  • In async Python, re-raise asyncio.CancelledError from the best-effort kill() so caller cancellation (e.g. asyncio.timeout) is honored; only ordinary cleanup failures are suppressed and never mask the original error.
  • Add integration coverage for synchronous Python, asynchronous Python, and TypeScript. The tests pin the sandbox to the base template (which has no mcp-gateway binary) so gateway startup genuinely fails after allocation.
  • Add a patch changeset for e2b and @e2b/python-sdk.

Usage Behavior

No API changes. A failed creation no longer leaves a sandbox behind, and the error is now descriptive:

try {
  const sandbox = await Sandbox.create({ mcp: { ... } })
} catch (err) {
  // err is SandboxError: "Failed to start MCP gateway: <stderr>"
  // the allocated sandbox has already been killed — no orphan is left running
}

Validation

All three integration tests verified against real infra: creation rejects with the documented error and no sandbox remains.

Notes

Supersedes #1547 by @hxaxd (squash-merged into this branch to preserve attribution).

🤖 Generated with Claude Code

## Problem

Fixes #1498.

`Sandbox.create` allocates a remote sandbox before starting
`mcp-gateway`. If gateway startup returns a non-zero exit code or throws
an exception, creation fails before the sandbox object is returned. As a
result, the caller has no sandbox ID to clean up, and the orphaned
sandbox continues consuming resources until it times out.

This state transition exists in synchronous Python, asynchronous Python,
and JavaScript/TypeScript.

## Changes

- Add a rollback boundary around MCP gateway startup in all three SDK
implementations.
- Attempt to kill the newly allocated sandbox when gateway startup
fails.
- Keep cleanup best-effort so a failed or cancelled `kill` does not
replace the original gateway error.
- Add real integration coverage for synchronous Python, asynchronous
Python, and TypeScript.
- Add a patch changeset for `e2b` and `@e2b/python-sdk`.

## Usage Behavior

No API changes are required. Existing code continues to receive the
original startup error, but a failed creation will normally no longer
leave a sandbox behind.

## Validation

Integration tests have been added and verified successfully.

## Notes

PR #1499 is a friendly competing implementation for the same issue. The
maintainer requested that its mocked tests be replaced with real
integration tests, but the PR received no further updates for several
days. The maintainer subsequently confirmed that an alternative
contribution could be submitted as long as it was implemented
independently from scratch, so this PR was prepared only after that
confirmation.
@changeset-bot

changeset-bot Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 987aeae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
e2b Patch
@e2b/python-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor

cursor Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

PR Summary

Low Risk
Scoped to MCP-enabled create failure paths; cleanup is best-effort and does not change successful create behavior.

Overview
When mcp-gateway fails during Sandbox.create / create with MCP enabled, the SDK now kills the sandbox that was already allocated instead of leaving it running until timeout.

JS, sync Python, and async Python share the same rollback: best-effort kill on any gateway startup failure, with SandboxError / SandboxException and Failed to start MCP gateway: <stderr> when the command exits non-zero (replacing a generic exit error or manual exit-code check). Patch changesets for e2b and @e2b/python-sdk. Integration tests assert no running sandbox remains after a forced gateway failure on the base template.

Reviewed by Cursor Bugbot for commit 987aeae. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from f704c5d. Download artifacts from this workflow run.

JS SDK ([email protected]):

npm install ./e2b-2.38.3-mcp-gateway-startup-cleanup.0.tgz

CLI (@e2b/[email protected]):

npm install ./e2b-cli-2.16.2-mcp-gateway-startup-cleanup.0.tgz

Python SDK (e2b==2.37.1+mcp.gateway.startup.cleanup):

pip install ./e2b-2.37.1+mcp.gateway.startup.cleanup-py3-none-any.whl

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16390cfd76

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Beyond the inline finding on the dead MCP gateway exit-code check, I looked at the exception-handling in the new rollback blocks themselves: whether the except BaseException wrapping kill() could swallow a second KeyboardInterrupt/SystemExit raised during cleanup, and whether the blocking kill() call in the async path could delay CancelledError propagation past a wait_for/timeout deadline. Both are inherent to the best-effort cleanup design here (the PR explicitly swallows cleanup failures so they don't mask the original error) rather than distinct bugs, so I'm not flagging them separately.

Extended reasoning...

Checked the sync and async except BaseException: ... finally raise rollback blocks in sandbox_sync/main.py and sandbox_async/main.py for interrupt-swallowing and cancellation-delay issues beyond the inline finding. The inner except BaseException: pass around kill() only suppresses exceptions from the cleanup call itself and still re-raises the original error afterward, and the blocking kill() during cleanup is bounded and best-effort by design, so these did not rise to separate bugs worth blocking on.

Comment thread packages/js-sdk/src/sandbox/index.ts Resolved
The tests relied on `mcp: {invalid_server: {}}` making mcp-gateway exit
non-zero, but the gateway accepts unknown server names and starts fine,
so Sandbox.create succeeded and the expected rejection never happened.

Pin the tests to the base template, which has no mcp-gateway binary, so
gateway startup genuinely fails after the sandbox is allocated. The
failure now surfaces as CommandExitError/CommandExitException rather
than the exit-code message, so match on the exception type instead.

Co-Authored-By: Claude Fable 5 <[email protected]>
@jakubno
jakubno removed their request for review July 29, 2026 07:02
@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

SDK-276

…preserve cancellation

Address review findings on the rollback block in all three SDKs:

- The manual exit-code check was dead code: foreground commands.run
  already throws CommandExitError/CommandExitException on non-zero
  exit, so the documented "Failed to start MCP gateway" message was
  never produced. Catch the command exit error and re-raise it as
  SandboxError/SandboxException with that message and the stderr.
- In async Python, re-raise asyncio.CancelledError from the
  best-effort kill() so caller cancellation (e.g. asyncio.timeout)
  is honored instead of being swallowed; suppress only ordinary
  cleanup failures in the sync path too.
- Tests assert the wrapped message again, which is now genuinely
  reachable.

Co-Authored-By: Claude Fable 5 <[email protected]>
@mishushakov
mishushakov merged commit cab27aa into main Aug 7, 2026
51 of 53 checks passed
@mishushakov
mishushakov deleted the mcp-gateway-startup-cleanup branch August 7, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP gateway startup failure leaves orphaned sandbox running

3 participants