Skip to content

fix(mcp-client): the real-child stdio tests spawn on a subprocess-capable loop (#692) - #951

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/692-mcp-client-spawn-windows
Nov 9, 2026
Merged

github-actions[bot] merged 1 commit into
mainfrom
fix/692-mcp-client-spawn-windows

Conversation

@cdeust

@cdeust cdeust commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Summary

Class 9 slice of #692, "mcp client spawn (2)". In Windows run 38008915302 (job 114084097957), TestStdioDeadlockRegression::test_large_response_over_pipe_buffer_is_received and ::test_silent_child_raises_loud_timeout failed with McpConnectionError: Failed to spawn: , and nothing came after the colon.

Root cause, from the traceback in the log. tests_py/conftest.py:89-90 installs WindowsSelectorEventLoopPolicy on Windows. A Windows SelectorEventLoop has no subprocess support, so base_events._make_subprocess_transport raised a bare NotImplementedError. Its str() is "", so the message printed by mcp_server/infrastructure/mcp_client.py:151 hid the cause.

Fix.

  • The two real-child tests run through _run_with_subprocesses. On Windows it uses asyncio.ProactorEventLoop(), which is the Windows default and supports subprocesses. Elsewhere it uses asyncio.new_event_loop(). The global selector policy stays as it is, because the conftest documents why it is there.
  • _real_child_client spawns sys.executable instead of a bare python3, since the python.org Windows installer ships only python.exe. The allowlist gets both the full path and the basename, because _spawn_process compares the last / component.
  • MCPClient formats the spawn error with repr(), so a bare exception still names its type (Failed to spawn: NotImplementedError()).

Refs #692

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Test plan

  • pytest tests_py/infrastructure/test_mcp_client.py: 65 passed on Linux, Python 3.11.
  • New test test_spawn_failure_with_an_empty_message_still_names_the_cause: it fails when {e!r} is reverted to {e} (checked locally: 1 failed) and passes with the fix.
  • ruff check and ruff format --check (ruff 0.17.3, the uv.lock version) on the changed files: OK. python scripts/check_craftsmanship.py: OK.

Audit notes

  • Not verified: no Windows run. The two tests are expected to pass on Windows with this change, but the scratch measurement workflow has not been re-run. The Proactor loop's GC-time teardown warning, which the conftest comment cites, should not appear because _close_and_reap closes the transport explicitly. That has not been observed on Windows either.
  • The full suite was not run locally, because this environment cannot download torch.

Coding-standards compliance

  • §2.2 Layer dependency direction preserved.
  • §8 The added helper cites its source (Python docs, asyncio "Platform Support").
  • §9 No dead code.

Reviewer checklist

  • CHANGELOG.md updated under Fixed.
  • No secrets / credentials / PII in the diff.

Generated by Claude Code

…able loop (#692)

Windows run 38008915302 (job 114084097957) failed
test_large_response_over_pipe_buffer_is_received and
test_silent_child_raises_loud_timeout with "Failed to spawn: " and nothing
after the colon. tests_py/conftest.py installs WindowsSelectorEventLoopPolicy
on Windows, and a Windows SelectorEventLoop has no subprocess support:
create_subprocess_exec raised a bare NotImplementedError, whose str() is "".

The two tests now run on the Proactor loop on Windows (asyncio.new_event_loop
elsewhere) and spawn sys.executable rather than a bare python3, which the
python.org Windows installer does not provide. MCPClient formats the spawn
error with repr(), so an exception with an empty message still names its type.

Refs #692

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYtzRWPJiuHWAatwLCK
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T20:33:10.822166Z c40adf6 PR opened
🔒 Security Review ✅ Completed 2026-11-09T20:34:30.071889Z c40adf6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 2m 35s —— View job


PR #951 review complete

  • Read the diff and context
  • Checked CI status: the Pipeline run was still in progress when I checked
  • Ran the five reviewer agents: code quality, documentation, performance, security and test coverage
  • Posted findings and the verdict: review comment

There are no Blockers and three Notes, none needed before merge. I could not run pytest, ruff or the craftsmanship gate here because those commands needed approval, so the review comes from reading the code. Nobody ran the tests on Windows.

Verdict: approve

@claude

claude Bot commented Nov 9, 2026

Copy link
Copy Markdown

Review 1 of 3: PR #951

I found no Blockers. Five reviewers looked at the diff: code quality, documentation accuracy, performance, security and test coverage.

What the reviewers confirmed by reading the code:

  • The allowlist works on both platforms. It compares the last / component (mcp_client.py:122). On Windows, sys.executable is a backslash path with no /, so the whole path is compared, and the whole path is in the allowed set. On POSIX, Path(sys.executable).name covers names like python3.12. The production allowlist and its callers are unchanged.
  • The new test catches a revert. mcp_client.py calls asyncio.create_subprocess_exec through the module, so the patch reaches the real call. The bare NotImplementedError lands in except Exception. With {e} the assertion would fail.
  • {e!r} leaks nothing new. For OSError it shows (errno, strerror), which is less than str() showed, since str() also printed the filename. Environment values never enter the message.
  • No other test needs the subprocess loop. No other test in tests_py spawns a real child on an asyncio loop; every other create_subprocess_exec is patched or monkeypatched.

Not verified: the reviewers only read the code. I could not run pytest, ruff or the craftsmanship gate in this environment. When I checked, the Pipeline CI run was still in progress. Nobody exercised the Windows Proactor path.

Notes, no action before merge

  1. In test_mcp_client.py:1344-1346 and :1323, transport.close() only schedules the pipe close for the next loop pass, and loop.close() runs before that pass happens. The stderr reader task (mcp_client.py:79) is not cancelled either, so ResourceWarnings or "Task was destroyed" can still show up. Using asyncio.Runner(loop_factory=...) (3.11+) would drain both.
  2. In test_mcp_client.py:1365 and :1383, await client.connect() is outside the try, so a failed handshake skips _close_and_reap and leaves the child unreaped.
  3. _run_with_subprocesses copies _run (test_mcp_client.py:22-27) except for the loop factory.

Verdict: approve

cdeust commented Nov 9, 2026

Copy link
Copy Markdown
Owner Author

Réponse aux trois notes non bloquantes de la revue 1. Aucune ne justifie à elle seule un push, qui relancerait la CI et la revue.

  1. et 3. asyncio.Runner(loop_factory=...) réglerait d'un coup le drainage des pipes et du lecteur stderr, ainsi que la copie de _run. Je l'intègre au prochain push de code sur cette PR, s'il y en a un. Sinon, je le garde pour la mesure Windows qui doit confirmer cette tranche, car c'est elle qui montrera si des ResourceWarnings apparaissent réellement sous Proactor.
  2. connect() hors du try existait déjà avant cette PR. Il ne concerne pas la cause Windows corrigée ici, donc je le laisse en l'état.

Generated by Claude Code

@github-actions
github-actions Bot merged commit 8b1af39 into main Nov 9, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants