fix: route MCP production mode through JSON-RPC - #1169
Conversation
|
@burtenshaw, tagging you regarding the MCP production-mode fix. The latest head, The required CI workflows ( Thank you! |
|
@mugenkyou thanks for the ping. Running. We should be able to get this down today. |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
There was a problem hiding this comment.
Stale comment
Blocking for merge at exact head
9dc05e30: Bugbot’s high-severity finding is correct.Production
connect()now creates a persistent HTTP MCP session, but session/httpxcleanup lives only inMCPClientBase.close(). Sync teardown (SyncEnvClient.close(), sync__exit__, andEnvClient.close()via_dispatch) calls_close_async()directly, so those paths never close the MCP session. Idle timeout defaults to off, so leaked sessions can accumulate towardMAX_CONCURRENT_ENVS.Please move production session + HTTP client cleanup into an overridden
_close_async()(or otherwise ensure every sync/async close path hits it), keep the existing connect-failure cleanup, and add a regression that syncclose()/ context-manager exit actually invokes session close. Do not merge until that lands and exact-head CI is green.If you’d rather keep authorship off the fork path for today, say the word and I’ll open an on-repo replacement with the fix.
Sent by Cursor Automation: Release
…1169) (#1175) * fix(mcp): route production mode through JSON-RPC with sync-safe teardown Replacement for #1169: enable use_production_mode for MCPToolClient, use HTTP /mcp sessions for connect/list_tools/call_tool, and put session/httpx cleanup on _close_async so SyncEnvClient.close and sync context exit do not leak MCP sessions. Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * fix(mcp): keep WebSocket for gym lifecycle in production mode Bugbot correctly flagged that HTTP-only production connect broke reset/step/state. Open WebSocket for the Gym path and still create the HTTP MCP session for list_tools/call_tool; sync close teardown unchanged. Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co>
Co-authored-by: burtenshaw <burtenshaw@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
Conflict resolution
This fork is
CONFLICTINGwith currentmainafter #1175, and the release bot cannot push your branch (403).Opened on-repo resolution with your single-session design + Co-authored-by credit:
#1180That PR attaches
/ws?session_id=to the HTTP MCP session (and teaches the server to attach without destroying HTTP-owned sessions), which fixes the dual-session capacity hole left by #1175. Please close this PR as superseded once #1180 lands.Sent by Cursor Automation: Release
Co-authored-by: burtenshaw <burtenshaw@users.noreply.github.com>
Co-authored-by: burtenshaw <burtenshaw@users.noreply.github.com>
Co-authored-by: burtenshaw <burtenshaw@users.noreply.github.com>
There was a problem hiding this comment.
Holding merge per Bugbot high on 17006d52: production _close_async calls openenv/session/close while /ws is still attached; the attachment guard rejects it, the client still clears _production_session_id, then WS detach leaves the HTTP session alive → capacity leak under max_concurrent_envs=1.
Please either:
- Land the fix on this fork (disconnect WS before session/close; optionally make HTTP
session/closeauthoritative), or - Squash-merge on-repo #1182 which already has that fix + regressions.
I will not merge this tip as-is.
Sent by Cursor Automation: Release
Bugbot high on #1169: openenv/session/close was rejected while /ws was attached, so production close cleared the local id and leaked the server session. Disconnect first; make HTTP session/close authoritative if a socket is still attached. Co-authored-by: mugenkyou <sachinskyte@gmail.com>
There was a problem hiding this comment.
Clearing CHANGES_REQUESTED: tip 41cb4e58 detaches WebSocket before openenv/session/close, which addresses the Bugbot high close/attach leak for the normal client path.
Still watching exact-head CI. Note: server still rejects session/close while a socket is attached — fine if clients always disconnect first; on-repo #1182 additionally makes HTTP close authoritative for races.
Will squash-merge this tip when required checks are green on the exact head (per Ben), unless a new blocking finding appears.
Sent by Cursor Automation: Release
Sync production `_close_async` nesting from fork tip 41cb4e5 (disconnect before session/close; keep provider teardown in finally). Server-side authoritative HTTP close from prior commit remains for attach races. Co-authored-by: mugenkyou <sachinskyte@gmail.com>
There was a problem hiding this comment.
Follow-up: tip 41cb4e58 addresses the close/attach leak via disconnect-before-session-close. Remaining gate is fork workflow Approve-and-run on that exact head (Tests/Package/docs action_required). Will squash-merge when green.
Sent by Cursor Automation: Release
Co-authored-by: burtenshaw <burtenshaw@users.noreply.github.com>
There was a problem hiding this comment.
Holding again: Bugbot high on 41cb4e58 is real — _disconnect_async returning does not guarantee the server cleared _session_websocket_attachments before openenv/session/close, so the ownership guard can still reject close; the client ignores the JSON-RPC error and clears the id → leak.
Needed: make HTTP openenv/session/close authoritative (discard attachment + destroy) and/or retry close until success after detach. On-repo #1182 already has the authoritative server close + regressions.
Please squash-merge #1182 when green, or port that server change onto this fork. I will not merge this tip as-is.
Sent by Cursor Automation: Release
Sync on-repo #1182 with contributor tip 715d5af: HTTP session/close while attached marks closing and destroys on detach (pending_closes), instead of force-destroying under an active WebSocket. Add disconnect- before-close unit coverage. Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co>
There was a problem hiding this comment.
Clearing my earlier CHANGES_REQUESTED (that hold targeted the 41cb4e58 close/attach race).
Tip 715d5af6 addresses it correctly: while /ws is attached, HTTP openenv/session/close returns closing: true and queues _session_pending_closes; destroy runs on WebSocket detach (cancel-safe discard-before-await). Client still disconnects before session/close. Local focused suite 103 passed / 1 skipped.
Per Ben: will squash-merge #1169 when exact-head required CI is green. Gate right now is fork Approve-and-run on this head (Tests / Package CI / Docs are action_required; I get 403 approving those).
Sent by Cursor Automation: Release
There was a problem hiding this comment.
Approving tip 715d5af6: deferred openenv/session/close until WebSocket detach (closing + _session_pending_closes) clears the prior attach-race hold. Will squash-merge when exact-head Tests/Package/docs are green (fork Approve-and-run still required).
Sent by Cursor Automation: Release
The exact-head approval clears a still-reproducible connect-cancellation leak and unresolved RFC 003 capability boundary. CancelledError during super()._connect_async() bypasses except Exception after the HTTP session is created, leaving the session/client/provider allocated. Do not merge on CI alone.
There was a problem hiding this comment.
Stale comment
Blocking exact head
715d5af6: the close/attachment race is fixed, but production connect remains cancellation-unsafe. After_ensure_production_session()allocates the HTTP session, cancellation duringsuper()._connect_async()raisesCancelledError(aBaseException) past theexcept Exceptioncleanup, leaving the HTTP session/client/provider allocated. Add a cancelled-WebSocket-connect regression proving all resources and capacity are released. Also retain requestedDarktex/sergiopaniegoreview: the new bearersession_idlets an/mcpsession attach to orchestration/wsreset/step/state and requires an explicit RFC 003 security decision. Do not merge on generic CI alone.Sent by Cursor Automation: Release
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 715d5af. Configure here.
| # permanently exempt from idle reaping. | ||
| self._session_websocket_attachments.discard(session_id) | ||
| if session_id in self._session_pending_closes: | ||
| await asyncio.shield(self._destroy_session(session_id)) |
There was a problem hiding this comment.
Pending close can destroy reattached session
Medium Severity
Deferred session close drops the WebSocket attachment and then awaits _destroy_session without claiming exclusive ownership. A new WebSocket can attach to the same session_id in that gap because attach never consults _session_pending_closes, so the delayed destroy can close the environment out from under the new connection.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 715d5af. Configure here.
Co-authored-by: burtenshaw <burtenshaw@users.noreply.github.com>
There was a problem hiding this comment.
Confirmed hold on tip 715d5af6:
- Cancel leak (real): production
_connect_asyncusesexcept Exception, soCancelledErrorafter_ensure_production_session()skips cleanup → capacity leak. - Bugbot medium (real): attach does not consult
_session_pending_closes, so a reattach can race delayed destroy.
Both fixed on on-repo #1182 (d7c9a7e0) with regressions. Prefer merging #1182 (Co-authored-by mugenkyou) when its CI is green, or wait for the author to port onto this fork. Won’t squash-merge this tip as-is.
Sent by Cursor Automation: Release
There was a problem hiding this comment.
Tip 00173d47 clears the cancel-after-session-create leak (except BaseException + close). Resuming Ben’s merge-when-green for this exact head.
Residual Bugbot medium (reattach while _session_pending_closes) is still open on this tip; fixed on on-repo #1182. Treating that as follow-up unless Ben says hold — normal client close does not reattach the same session id in the destroy gap.
Will squash-merge when exact-head Tests/Package/docs are green.
Sent by Cursor Automation: Release
There was a problem hiding this comment.
Blocking exact head 00173d47: connect cancellation is improved, but Bugbot’s pending-close reattachment race remains. WebSocket finally drops the attachment marker before _destroy_session() acquires the lock; attach does not reject _session_pending_closes, so a new socket can attach in that gap and have its environment destroyed underneath it. Port the pending-close attach guard and regression from #1182, then retain current Darktex/sergiopaniego review for the RFC 003 bearer-capability boundary. Do not merge on generic CI alone.
Sent by Cursor Automation: Release
|
Thanks @mugenkyou . Cursor really had a hard time with this one! 🤣 If you're interested, we're in need of someone taking on MCP > v4. Think you could handle that? |
|
Yeah, definitely @burtenshaw. I’d love to take this on. Let me know what you have in mind for MCP > v4. |
* fix: clear 0.4.3 release blockers Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * style: format socket close scheduler Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * fix(discovery): fail closed on relative home cache Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * fix(client): preserve parent teardown on child cancellation Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * fix(discovery): reject Unicode line separators Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * fix(client): close every child despite cancellation Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * Revert "fix(mcp): production JSON-RPC routing with sync-safe teardown (from #1169) (#1175)" This reverts commit e3eb3fa. * fix(mcp): preserve sync-safe teardown after revert Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * style(mcp): document best-effort HTTP cleanup Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * fix(mcp): isolate explicit HTTP tools mode Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> * chore: separate shared MCP follow-up Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: benjamin.burtenshaw <benjamin.burtenshaw@huggingface.co>




Summary
Fixes
MCPToolClientproduction mode so it uses the HTTP/mcpJSON-RPC transport instead of falling back to the WebSocketstep()path.Type of Change
Alignment Checklist
Before submitting, verify:
.claude/docs/PRINCIPLES.mdand this PR aligns with our principles.claude/docs/INVARIANTS.mdand no invariants are violated/pre-submit-pr(orbash .claude/hooks/lint.shand tests) and addressed all issuesRFC Status
Test Plan
Updated the production-mode tests to verify that:
mode="production"setsuse_production_modetoTruelist_tools()usestools/listthrough the HTTP JSON-RPC pathcall_tool()usestools/callthrough the HTTP JSON-RPC pathstep()Tests run:
PYTHONPATH="src;envs" python -m pytest tests/core/test_mode_selection.py -q— 36 passedPYTHONPATH="src;envs" python -m pytest -p no:flask tests/core/test_mcp/ tests/core/test_production_mode_routes.py -q— 143 passed, 1 skippedPYTHONPATH="src;envs" python -m pytest -p no:flask tests/core/ -q— 601 passed, 2 skippedClaude Code Review
N/A
Closes #1168
Note
Medium Risk
Changes session ownership, idle reaping, and close ordering on the env server; production clients depend on the new attach/teardown contract, though behavior is covered by expanded integration tests.
Overview
Production MCP clients now share one server session across HTTP JSON-RPC and WebSocket, so
list_tools/call_toolandreset/step/statehit the same environment instance.On the server,
/wscan attach to an HTTP-created session via asession_idquery parameter (one WebSocket per session). Attached sessions are exempt from idle reaping;openenv/session/closewhile a socket is attached returnsclosing: trueand tears down only after the WebSocket disconnects. WebSocket-owned sessions still destroy on disconnect.MCPToolClientconnect creates the HTTP session first, opens the WebSocket with thatsession_id(preserving other query params), and on close disconnects the WebSocket before closing the MCP session. HTTP/mcpURL construction is normalized from the stablebase_url. Connect/cancel paths use locks and cleanup so orphaned sessions are not left behind.Reviewed by Cursor Bugbot for commit 00173d4. Bugbot is set up for automated code reviews on this repo. Configure here.