Skip to content

Commit b0268d7

Browse files
committed
fix(#863): use pythonw.exe + drop DETACHED_PROCESS to actually suppress Windows console
Real-world Claude Desktop test on Windows showed the previous fix was a no-op: a cmd window still popped and closing it killed the sidecar. Root cause in Python's subprocess docs: "CREATE_NO_WINDOW... is ignored if you specify CREATE_NEW_CONSOLE or DETACHED_PROCESS." DETACHED_PROCESS on a CUI binary (python.exe) auto-allocates a fresh visible console — and that's the cmd window the user saw. The closed- window killing the server is because the auto-allocated console's process group sends CTRL_CLOSE_EVENT to the child when X is clicked. Drop DETACHED_PROCESS entirely; prefer pythonw.exe (GUI subsystem, never allocates a console) over python.exe; keep CREATE_NEW_PROCESS_GROUP (blocks CTRL_C / CTRL_CLOSE propagation from any console that might attach during the python.exe fallback path) and CREATE_NO_WINDOW (belt-and-suspenders for the fallback, harmless when pythonw is in use). pythonw.exe ships alongside python.exe in every standard CPython install including uv-managed ones.
1 parent 75adaae commit b0268d7

1 file changed

Lines changed: 27 additions & 12 deletions

File tree

src/ha_mcp/stdio_settings_sidecar.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -318,19 +318,35 @@ def _do_spawn() -> None:
318318
"close_fds": True,
319319
}
320320
if sys.platform == "win32":
321-
# Detach from the parent console so closing the parent doesn't
322-
# take the child down with it. CREATE_NEW_PROCESS_GROUP also
323-
# prevents the parent's CTRL_C_EVENT (issued by the console
324-
# window) from reaching the child process group.
325-
# CREATE_NO_WINDOW suppresses the empty console window that
326-
# ``python.exe`` (a console app) would otherwise pop up under
327-
# Claude Desktop — DETACHED_PROCESS by itself doesn't reuse
328-
# the parent's console, it just creates a fresh one.
321+
# Prefer ``pythonw.exe`` over ``python.exe``. pythonw is the
322+
# GUI-subsystem Python interpreter — Windows never allocates a
323+
# console window for it. Falls back to python.exe if pythonw
324+
# isn't alongside (rare; most CPython distributions ship both,
325+
# including the uv-installed Python that uvx uses).
326+
#
327+
# Why not DETACHED_PROCESS: the prior attempt used
328+
# ``DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW``
329+
# and the cmd window still popped under Claude Desktop. Python's
330+
# subprocess docs spell out that ``CREATE_NO_WINDOW`` is
331+
# IGNORED when ``DETACHED_PROCESS`` is set, and
332+
# ``DETACHED_PROCESS`` on a CUI binary auto-allocates a fresh
333+
# visible console. pythonw avoids the whole console-allocation
334+
# path.
335+
#
336+
# CREATE_NEW_PROCESS_GROUP is still useful: closing any console
337+
# that happens to be attached (if pythonw isn't found and we
338+
# fall back to python.exe) sends ``CTRL_CLOSE_EVENT`` to the
339+
# console's process group. Putting the child in its own group
340+
# prevents that event from killing the sidecar.
341+
# CREATE_NO_WINDOW is belt-and-suspenders for the python.exe
342+
# fallback path — harmless when pythonw is in use.
343+
pythonw = Path(sys.executable).with_name("pythonw.exe")
344+
cmd_python = str(pythonw) if pythonw.exists() else sys.executable
329345
popen_kwargs["creationflags"] = (
330-
subprocess.DETACHED_PROCESS # type: ignore[attr-defined]
331-
| subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
346+
subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
332347
| subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
333348
)
349+
cmd = [cmd_python, "-m", "ha_mcp.stdio_settings_sidecar"]
334350
else:
335351
# New session leader → parent SIGTERM / shell exit doesn't
336352
# cascade. Detaches from the parent's session so SIGHUP /
@@ -339,8 +355,7 @@ def _do_spawn() -> None:
339355
# isn't needed here since the parent stdio process already has
340356
# no TTY to inherit.
341357
popen_kwargs["start_new_session"] = True
342-
343-
cmd = [sys.executable, "-m", "ha_mcp.stdio_settings_sidecar"]
358+
cmd = [sys.executable, "-m", "ha_mcp.stdio_settings_sidecar"]
344359
try:
345360
proc = subprocess.Popen(cmd, **popen_kwargs)
346361
except OSError:

0 commit comments

Comments
 (0)