Conversation
Motivation: ClientSessionGroup.connect_to_server opens a transport (subprocess or HTTP session) and initializes a ClientSession before validating the server's prompts/resources/tools against names already registered in the group. When that validation rejects the server for a duplicate name, connect_with_session raises MCPError before the session is recorded in self._sessions, so the caller has no session object to close and the group's own tracking never lists it. The already-opened transport stays alive until the whole group tears down: a leaked child process for stdio, or an initialized session counted against a server's max_sessions for streamable HTTP. A long-lived host that retries a failed connect leaks one more resource per attempt. Approach: connect_to_server is the only caller of connect_with_session that owns the transport it opened (via _establish_session); the other caller, connect_with_session used directly, is handed an already-owned session by its caller and must not have it closed out from under them. So the fix is scoped to connect_to_server: wrap the call to connect_with_session in try/except, and on any exception pop the session's entry out of self._session_exit_stacks and close it before re-raising, mirroring the cleanup disconnect_from_server already does for a session the group decides to drop. Validation: Added test_client_session_group_connect_to_server_closes_transport_on_duplicate to tests/client/test_session_group.py, which mocks _establish_session to register a mock transport stack the way the real implementation does, triggers the existing duplicate-tool-name rejection path, and asserts the mock stack's aclose is awaited and the session is removed from _session_exit_stacks. Confirmed the test fails on the pre-fix code with "Expected aclose to have been awaited once. Awaited 0 times." and passes after the fix. Ran the full suite: `uv run --frozen pytest` and `./scripts/test` both pass (5969 passed, 10 skipped, 1 xfailed), coverage stays at 100.00% with no new pragma: no cover, and `ruff format --check`, `ruff check`, and `pyright` are clean on both changed files. Report: modelcontextprotocol#3490 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-sonnet-5 (via Claude Code)
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3490. If a maintainer assigns you to #3490, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Fixes #3490
Motivation and Context
ClientSessionGroup.connect_to_serveropens a transport (subprocess or HTTPsession) and initializes a
ClientSessionbefore validating the server'sprompts/resources/tools against names already registered in the group. When
that validation rejects the server for a duplicate name,
connect_with_sessionraises
MCPErrorbefore the session is recorded inself._sessions, so thecaller has no session object to close and the group's own tracking never lists
it. The already-opened transport stays alive until the whole group tears down:
a leaked child process for stdio, or an initialized session counted against a
server's
max_sessionsfor streamable HTTP. A long-lived host that retries afailed connect leaks one more resource per attempt.
How Has This Been Tested?
Added
test_client_session_group_connect_to_server_closes_transport_on_duplicateto
tests/client/test_session_group.py, which mocks_establish_sessiontoregister a mock transport stack the way the real implementation does, triggers
the existing duplicate-tool-name rejection path, and asserts the mock stack's
acloseis awaited and the session is removed from_session_exit_stacks.Confirmed the test fails on the pre-fix code with "Expected aclose to have
been awaited once. Awaited 0 times." and passes after the fix.
Ran the full suite:
uv run --frozen pytestand./scripts/testboth pass(5969 passed, 10 skipped, 1 xfailed), coverage stays at 100.00% with no new
pragma: no cover, andruff format --check,ruff check, andpyrightareclean on both changed files.
Breaking Changes
None.
connect_to_server's signature, return type, and raised exception areunchanged; the only observable difference is that the transport it opened is
now closed before the exception propagates, instead of leaking until the
whole group is torn down.
Types of changes
Checklist
help wanted, or I'm a maintainer)Additional context
Two prior PRs for this same issue (#3491, #3502) were auto-closed by this
repo's
require-issue-linkbot because their authors weren't assigned to#3490 — neither was rejected on technical grounds. This PR reimplements the
same fix (close the transport in
connect_to_server, the only caller thatowns it, so
connect_with_session's "the group never closes a session itdidn't open" contract for its own direct callers is preserved).
AI assistance: this change was drafted with Claude Code.