Summary
In SHTTP (--port) mode, when an MCP client disconnects without calling end, the underlying Browserbase session, its SessionManager, and the Server instance are never cleaned up. SessionManager.closeAllSessions() exists but has no callers anywhere, so the only path that actually closes a Browserbase session today is the end tool.
How I found it
Reading the session lifecycle (server.ts → transport.ts → context.ts → sessionManager.ts) to understand who owns and tears down a Browserbase session — the teardown chain doesn't connect.
Details
server.close() doesn't reach the SessionManager. The MCP SDK's Server.close() tears down the protocol, but nothing wires it to Context's SessionManager. So the exit watchdog's serverList.closeAll() (program.ts:85) closes the protocol but not the browsers. (stdio masks this: the process exits, the CDP socket drops, and Browserbase reaps the session.)
- HTTP
onclose only removes the routing entry. transport.onclose (transport.ts:56) does sessions.delete(...) — no server.close(), no serverList.close(server). So for a long-running --port server, each disconnect leaks:
- a
Server in ServerList._servers (grows unbounded),
- a
SessionManager holding a live Stagehand instance,
- the Browserbase cloud session itself (its CDP link is independent of the HTTP transport, so it stays open until Browserbase's inactivity timeout — or indefinitely with
keepAlive: true).
SessionManager.closeAllSessions() (sessionManager.ts:447) looks purpose-built for this but has no callers, which suggests the cleanup was intended and just not connected.
Impact
- Memory growth in long-running self-hosted HTTP servers (
_servers and SessionManagers accumulate).
- Browserbase session-minutes consumed by orphaned sessions until server-side timeout; unbounded with
keepAlive.
- Mostly affects self-hosted SHTTP mode; stdio is largely masked by process exit.
Proposed fix (small)
- In
index.ts (where Context is in scope), wire the server's onclose → context.getSessionManager().closeAllSessions(), so any server.close() frees the browser sessions (also fixes the stdio exit path).
- In
transport.ts onclose, call await serverList.close(server) (triggers the above + removes the entry from _servers).
Happy to open a PR for this. One coordination note: the in-flight stg-1927 branch rewrites index.ts/transport.ts, so I wanted to check first — fold this into that work, or a standalone PR against main? (Not a dup of #149, which is a connection failure rather than a teardown leak.)
cc @Kylejeong2
Summary
In SHTTP (
--port) mode, when an MCP client disconnects without callingend, the underlying Browserbase session, itsSessionManager, and theServerinstance are never cleaned up.SessionManager.closeAllSessions()exists but has no callers anywhere, so the only path that actually closes a Browserbase session today is theendtool.How I found it
Reading the session lifecycle (
server.ts→transport.ts→context.ts→sessionManager.ts) to understand who owns and tears down a Browserbase session — the teardown chain doesn't connect.Details
server.close()doesn't reach the SessionManager. The MCP SDK'sServer.close()tears down the protocol, but nothing wires it toContext'sSessionManager. So the exit watchdog'sserverList.closeAll()(program.ts:85) closes the protocol but not the browsers. (stdio masks this: the process exits, the CDP socket drops, and Browserbase reaps the session.)oncloseonly removes the routing entry.transport.onclose(transport.ts:56) doessessions.delete(...)— noserver.close(), noserverList.close(server). So for a long-running--portserver, each disconnect leaks:ServerinServerList._servers(grows unbounded),SessionManagerholding a live Stagehand instance,keepAlive: true).SessionManager.closeAllSessions()(sessionManager.ts:447) looks purpose-built for this but has no callers, which suggests the cleanup was intended and just not connected.Impact
_serversand SessionManagers accumulate).keepAlive.Proposed fix (small)
index.ts(whereContextis in scope), wire the server'sonclose→context.getSessionManager().closeAllSessions(), so anyserver.close()frees the browser sessions (also fixes the stdio exit path).transport.tsonclose, callawait serverList.close(server)(triggers the above + removes the entry from_servers).Happy to open a PR for this. One coordination note: the in-flight
stg-1927branch rewritesindex.ts/transport.ts, so I wanted to check first — fold this into that work, or a standalone PR againstmain? (Not a dup of #149, which is a connection failure rather than a teardown leak.)cc @Kylejeong2