fix(mcp): resolve managed Windows Node launchers (npx.cmd/npm.cmd) for stdio MCP servers

On Windows a stdio MCP server configured with `command: npx|npm|node` failed
with WinError 2 whenever the desktop/gateway PATH lacked the managed Node dir:
`_node_fallback` probed only the POSIX shape `<HERMES_HOME>/node/bin/<cmd>`
with no extension, while `scripts/install.ps1` unpacks Node directly into
`<HERMES_HOME>\node` as `npx.cmd`/`npm.cmd`/`node.exe`. It also derived the
home from raw `os.getenv("HERMES_HOME")`, so a context-local profile home
(multiplexed gateway) was ignored.

Reuse the platform-aware helpers instead of a second hand-rolled layout:
`hermes_constants.iter_hermes_node_dirs(get_hermes_home())` supplies both
managed shapes in the right order, and the module's own `_npx_bin_candidates`
supplies the `.cmd` -> `.exe` precedence (same injectable `windows=` seam the
npx-cache shortcut already uses, so the branch is testable on Linux CI).
POSIX candidates (`node/bin`, `~/.local/bin`, `/usr/local/bin`) are unchanged.

Slimmer redo of #111941 by @KoNit-K, which re-derived the Windows shape
in-place and kept the raw env read.

Fixes #111937

Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
This commit is contained in:
teknium1
2026-09-15 11:49:50 -07:00
committed by Teknium
parent a2837ec088
commit 8e11666726
2 changed files with 50 additions and 6 deletions
+10 -5
View File
@@ -149,14 +149,19 @@ def _which_with_config_pathext(command: str, path_arg, env: dict):
os.environ["PATHEXT"] = saved
def _node_fallback(command: str) -> str:
"""Well-known Node install locations for bare ``npx``/``npm``/``node``; *command* unchanged when none exists."""
def _node_fallback(command: str, *, windows: Optional[bool] = None) -> str:
"""Well-known Node install locations for bare ``npx``/``npm``/``node``; *command* unchanged when none exists.
The managed tree comes from ``iter_hermes_node_dirs`` (Windows unpacks into ``<home>\\node``, POSIX into
``<home>/node/bin``) under the active profile's ``get_hermes_home()``; on Windows the real files are
``npx.cmd``/``node.exe`` (``windows`` injectable, as for ``_npx_bin_candidates``)."""
from hermes_constants import get_hermes_home, iter_hermes_node_dirs
home = os.path.expanduser("~")
hermes_home = os.path.expanduser(os.getenv("HERMES_HOME", os.path.join(home, ".hermes")))
# /usr/local/bin: canonical Node location (from-source Linux, Hermes Docker image, Intel Homebrew),
# needed when a hand-authored env.PATH omits it — npx's shebang re-execs /usr/bin/env node.
candidates = (os.path.join(hermes_home, "node", "bin", command), os.path.join(home, ".local", "bin", command),
os.path.join(os.sep, "usr", "local", "bin", command))
directories = [*map(str, iter_hermes_node_dirs(get_hermes_home())), os.path.join(home, ".local", "bin"),
os.path.join(os.sep, "usr", "local", "bin")]
candidates = (c for d in directories for c in _npx_bin_candidates(d, command, windows=windows))
return next((c for c in candidates if os.path.isfile(c) and os.access(c, os.X_OK)), command)