Skip to content

fix(python-sdk): map GitHub MCP server keys to gateway wire names - #1901

Draft
Kylinny wants to merge 1 commit into
e2b-dev:mainfrom
Kylinny:fix/mcp-server-key-mapping-1899
Draft

Kylinny wants to merge 1 commit into
e2b-dev:mainfrom
Kylinny:fix/mcp-server-key-mapping-1899

Conversation

@Kylinny

@Kylinny Kylinny commented Sep 26, 2026 •

Copy link
Copy Markdown

Thank you @aimable100 for the clear report and repro — the gateway log line made the root cause easy to confirm.

Root cause

GitHubMcpServerConfig in the Python SDK documents snake_case keys (run_cmd / install_cmd), but the sandbox's mcp-gateway only recognizes camelCase (runCmd / installCmd). Sandbox.create / AsyncSandbox.create serialized the dict verbatim into mcp-gateway --config <json>, so the documented Python form was silently ignored and the server never started (the gateway logged GitHub server missing run command).

The JS SDK was already consistent (its GitHubMcpServer type uses runCmd / installCmd), so this only affected the Python SDK.

Fix (option 1 from the issue)

Map the keys to the wire form once at the top of create() in both the sync and async implementations, via a new build_mcp_wire_config() helper in e2b/sandbox/sandbox_api.py — matching how other snake_case fields are mapped to wire names elsewhere in the SDK. The documented Python form keeps working; both the API payload and the gateway --config JSON now agree on the wire names.

Invariants of the mapping:

  • Only entries keyed "github/owner/repo" are touched; base MCP server entries (already camelCase) pass through unchanged.
  • Unknown keys (e.g. envs, future gateway keys) pass through verbatim.
  • An explicitly spelled wire key (e.g. runCmd) always wins over its snake_case alias.
  • The caller's dict is never mutated.

Usage example

from e2b import Sandbox

sbx = Sandbox.create(
    mcp={
        "github/tenuo-ai/tenuo-demos": {
            "install_cmd": "npm install",   # now correctly reaches the gateway
            "run_cmd": "npx start",         # as installCmd / runCmd
        }
    }
)

The docs' "Custom MCP servers" Python example (snake_case) is now accurate as written, so no docs change is needed.

Tests

  • New packages/python-sdk/tests/shared/sandbox/test_mcp_wire_names.py (9 tests): key mapping, partial configs, envs/unknown keys pass-through, base-server entries untouched, explicit wire key precedence, no input mutation, plus end-to-end sync/async create() tests asserting both the API request body and the actual mcp-gateway --config command carry runCmd/installCmd.
  • Full tests/shared/ suite: 223 passed, 32 skipped (pre-existing skips), no regressions.
  • ruff check, ruff format --check, and ty check all pass.
  • Added a patch changeset for @e2b/python-sdk.

Fixes #1899.


Devin Review

@cla-bot

cla-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have @Kylinny on file. You can sign our CLA at https://e2b.dev/docs/cla . Once you've signed, post a comment here that says '@cla-bot check'

@changeset-bot

changeset-bot Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c7313bb

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

This PR includes changesets to release 1 package
Name Type
@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

@devin-ai-integration devin-ai-integration Bot 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.

TASTE.md review of #1901 (checked T-1/T-1a parity & naming, T-40 wire-format casing, T-43 shell escaping, T-52 client-side validation, T-54 public surface, T-71 docstrings).

The fix itself follows T-40 as intended: Python keeps the documented snake_case run_cmd/install_cmd in GitHubMcpServerConfig and converts to the camelCase wire form on the way in, and the gateway command still goes through shlex.quote (T-43). Applying the mapping once in create() so both the API payload and the mcp-gateway --config JSON agree is the right place.

2 violations, both inline in packages/python-sdk/e2b/sandbox/sandbox_api.py:

  1. The "explicit wire key wins over its snake_case alias" branch turns runCmd/installCmd into an accepted (undocumented) Python spelling — T-1a / T-40.
  2. build_mcp_wire_config is an internal helper exposed under a public name without being in __all__ — T-54.

Not tied to a line: since the tests import the helper, they'd import the underscored name after the rename.

Comment on lines +130 to +137
for key, value in config.items():
wire_key = _github_mcp_server_wire_names.get(key, key)
# An explicitly spelled wire key wins over its snake_case alias,
# regardless of dict order.
if wire_key == key or wire_key not in config:
mapped[wire_key] = value
wire[server] = mapped
else:

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.

T-1a / T-40 — the Python surface is snake_case only; camelCase is confined to the serialized shape and the SDK converts on the way in. Making an explicitly spelled runCmd/installCmd "win" over run_cmd/install_cmd quietly turns the wire spelling into a second accepted Python spelling (one that GitHubMcpServerConfig doesn't document), and adds a client-side precedence rule the gateway never defined (T-52). Just rename and let the gateway validate:

Suggested change
for key, value in config.items():
wire_key = _github_mcp_server_wire_names.get(key, key)
# An explicitly spelled wire key wins over its snake_case alias,
# regardless of dict order.
if wire_key == key or wire_key not in config:
mapped[wire_key] = value
wire[server] = mapped
else:
wire[server] = {
_github_mcp_server_wire_names.get(key, key): value
for key, value in config.items()
}

}


def build_mcp_wire_config(mcp: McpServer) -> Dict[str, Any]:

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.

T-54 — everything public is listed in __init__.py's __all__; this helper is internal wiring for create() and isn't (and shouldn't be) exported, so give it the module-private prefix like the _github_mcp_server_wire_names table above it. The tests can import the underscored name.

Suggested change
def build_mcp_wire_config(mcp: McpServer) -> Dict[str, Any]:
def _build_mcp_wire_config(mcp: McpServer) -> Dict[str, Any]:

@Kylinny

Kylinny commented Oct 3, 2026

Copy link
Copy Markdown
Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Oct 3, 2026
@cla-bot

cla-bot Bot commented Oct 3, 2026

Copy link
Copy Markdown

The cla-bot has been summoned, and re-checked this pull request!

@devin-ai-integration devin-ai-integration Bot 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@Kylinny

Kylinny commented Oct 7, 2026

Copy link
Copy Markdown
Author

Hi @mishushakov — a gentle follow-up on this fix: it maps the Python SDK's snake_case GitHub MCP server keys (run_cmd / install_cmd) to the camelCase names the mcp-gateway actually recognizes, so the documented config works as written. CLA is signed and checks are green — would appreciate a review when you have a moment. Thank you!

@Kylinny
Kylinny force-pushed the fix/mcp-server-key-mapping-1899 branch from 4f30399 to cdcf6f5 Compare October 15, 2026 01:30
The Python SDK documents snake_case keys (run_cmd/install_cmd) for
GitHubMcpServerConfig, but the mcp-gateway only recognizes camelCase
(runCmd/installCmd). The SDK passed the dict through verbatim, so the
documented form was silently ignored and the server never started
(e2b-dev#1899).

Map the keys to the wire form in Sandbox.create/AsyncSandbox.create
via build_mcp_wire_config() before the config reaches the API payload
and the `mcp-gateway --config` JSON. Base server entries and unknown
keys pass through unchanged; an explicitly spelled wire key wins
over its snake_case alias; the caller's dict is never mutated.
@Kylinny
Kylinny force-pushed the fix/mcp-server-key-mapping-1899 branch from cdcf6f5 to c7313bb Compare October 15, 2026 01:31
@mishushakov

Copy link
Copy Markdown
Member

This is not a priority for us atm. I will come back to it once I have more headroom

@mishushakov
mishushakov marked this pull request as draft October 16, 2026 11:50

This branch has not been deployed

No deployments
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.

Python SDK/docs spell custom MCP server keys install_cmd/run_cmd but the gateway only accepts installCmd/runCmd

2 participants