Skip to content

fix: resolve Windows command shims for stdio MCP servers - #1691

Open
pinqionghongbai wants to merge 1 commit into
google:mainfrom
pinqionghongbai:fix/windows-npm-shim-resolution
Open

pinqionghongbai wants to merge 1 commit into
google:mainfrom
pinqionghongbai:fix/windows-npm-shim-resolution

Conversation

@pinqionghongbai

Copy link
Copy Markdown

Fixes #1686.

Problem

On Windows, McpToolset configured with an npm-shim command (command("npx")) never starts a
subprocess. ProcessBuilder delegates to Win32 CreateProcess, which appends only .exe to an
extension-less command and never consults PATHEXT. npm installs its CLI tools as batch shims
(npx.cmd, npm.cmd), so CreateProcess fails with error=2 even though npx works fine in a
shell — the shell does the PATHEXT lookup, CreateProcess does not.

The failure is invisible to callers: no process is ever spawned, so the MCP client waits out
initializationTimeout (default 5 min) and retries (MAX_RETRIES = 3, i.e. 3 total attempts).
Measured on the reporter's machine with adk-java 1.13.0: 900,857 ms of silence, then
McpToolLoadingException, with node.exe never started.

Fix

Resolve the command against PATH / PATHEXT before handing ServerParameters to
StdioClientTransport, mirroring shutil.which as used by the Python MCP SDK — which is where
get_windows_executable_command already does this for the Python transports. This keeps the Java
side aligned with the other SDKs, per CONTRIBUTING.

New package-private helper WindowsExecutableResolver:

  • a command that already ends with a PATHEXT extension is tried as-is;
  • any other command is tried with each PATHEXT extension appended, in PATHEXT order;
  • the match is returned with its on-disk spelling (npx.cmd, not the npx.CMD that
    concatenating PATHEXT would produce);
  • non-Windows, explicit paths (containing / or \), and lookup misses all return the command
    unchanged, so ProcessBuilder still surfaces its own, more useful error.

DefaultMcpTransportBuilder.build() applies the resolver only for ServerParameters and rebuilds
the parameters with the caller's args and env intact.

Measured result

Same machine, same repro, bare command("npx"):

Version Command Result
1.13.0 npx silent 900,857 ms → McpToolLoadingException, node.exe never started
this PR npx 3.5–4.1 s (two independent runs), 14 tools loaded
both npx.cmd ~3.5 s, 14 tools

Tests

  • WindowsExecutableResolverTest — 14 cases: non-Windows passthrough, explicit paths, shim
    resolution, PATHEXT ordering, multiple PATH entries, not-found passthrough, missing
    PATHEXT fallback, custom PATHEXT, case-insensitive OS name.
  • DefaultMcpTransportBuilderTest — 4 new cases: args/env survive the command substitution (runs
    on every platform), the resolved command reaches the transport, and a Windows+Node-gated
    end-to-end shim assertion.
  • Full core suite: 2609 tests, 0 failures, 0 errors, 30 skipped (pre-existing assumption-based
    skips). With -Dos.name=Linux the Windows-only case is skipped and the rest still pass.
  • mvn -Prelease clean package -pl core -am — the command CI runs — passes, including the javadoc
    jar (doclint=all,-missing).
  • mvn fmt:check (all modules): 0 non-complying.

Note on placement

The resolution arguably belongs in the upstream Java MCP SDK's StdioClientTransport (that is
where the Python SDK does it). It is placed in DefaultMcpTransportBuilder so ADK users get the
fix without waiting on an upstream release; if you would rather move it upstream, it can be lifted
as-is.

On Windows, npm installs its command-line tools as batch shims
(npx.cmd, npm.cmd) rather than PE executables. ProcessBuilder delegates
to Win32 CreateProcess, which appends only .exe to an extension-less
command and never consults PATHEXT. A stdio server configured with
command("npx") therefore fails with CreateProcess error=2 even though
the same command works when typed into a shell -- the shell does the
PATHEXT lookup, and CreateProcess does not.

That failure never reaches the caller. No subprocess is ever spawned,
so the MCP client waits out its initialization timeout instead of
reporting anything. Measured on Windows 11 with the mcpfilesystem
sample: 900 s (3 attempts x 5-minute timeout) of silence before a
McpToolLoadingException.

DefaultMcpTransportBuilder now resolves the command against PATH and
PATHEXT before handing it to StdioClientTransport, mirroring
shutil.which as used by the Python MCP SDK: a command that already
carries a PATHEXT extension is tried as-is, anything else is tried with
each extension appended, in PATHEXT order. Non-Windows platforms, and
commands that are already paths, are untouched.

The lookup matches case-insensitively and returns the name as spelled
on disk, so the result does not depend on the case sensitivity of the
filesystem it runs on.

Fixes google#1686.
@hemasekhar-p

Copy link
Copy Markdown
Contributor

Hi @pinqionghongbai, We appreciate your contribution and the effort you put into this pull request. It is currently under review by our team. We will notify you if any additional details are needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

McpToolset hangs silently for ~5 minutes per retry (3 retries) when stdio command is an npm shim ("npx") on Windows

2 participants