Skip to content

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

Description

@pinqionghongbai

Description

The examples/mcpfilesystem sample — and any McpToolset built with
command("npx") — appears to hang indefinitely on Windows. No error is
printed and no node.exe process is ever spawned; the caller just blocks
inside McpToolset.getTools().

After digging into it, the "hang" is actually a very slow failure loop:
each initialization attempt waits out the full default
initializationTimeout (5 minutes), and the sync retry logic re-runs the
same doomed initialization, so the program stays silent for a measured
900 seconds (~15 minutes: 3 attempts x 5-minute timeout) before a
McpToolLoadingException (wrapped
Client failed to initialize by explicit API call) finally surfaces —
and even that is invisible unless an SLF4J provider is on the classpath,
which the samples don't include by default.

Environment

  • OS: Windows 11 Home (10.0.26200)
  • JDK: Oracle 21.0.5
  • google-adk: 1.13.0
  • Maven: 3.9.8
  • Node.js: 24.15.0 (npx 11.12.1, standard npm-for-Windows install with
    npx.cmd shim)

Root cause

On Windows, npm installs command shims as batch scripts (npx.cmd), not
executables. Java's ProcessBuilder cannot execute a bare npx command
(it is not a PE executable and is not resolved through PATHEXT), so the
stdio subprocess never starts.

A plain new ProcessBuilder("npx", "-v").start() on the same machine
fails immediately with CreateProcess error=2, which confirms the
JVM/OS-level cause. Inside ADK, however, the failure is masked by:

  1. McpSessionManager.initializeSession(...) defaults the
    initializationTimeout to 5 minutes
    (core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java,
    the Duration.ofMinutes(5) fallback), so client.initialize() waits
    for a response from a process that never started.
  2. The sync McpToolset.getTools() retry logic (MAX_RETRIES = 3,
    100 ms delay) then re-runs the same doomed initialization.
  3. Without an SLF4J provider (the default for the samples), even the
    retry ERROR logs are swallowed, leaving zero output on the console.

Observed log once an SLF4J provider is added (full run, timestamps from
the measured repro):

ERROR com.google.adk.tools.mcp.McpToolset - Unexpected error during tool loading, retry attempt 1
java.lang.RuntimeException: Client failed to initialize by explicit API call
    at io.modelcontextprotocol.client.LifecycleInitializer...
ERROR ... retry attempt 2
ERROR ... retry attempt 3
ERROR com.google.adk.tools.mcp.McpToolset - Failed to load tools after multiple retries due to unexpected error.
com.google.adk.tools.mcp.McpToolsetException$McpToolLoadingException
    (total elapsed: 900,857 ms)

Minimal reproduction (public API only)

StdioServerParameters params = StdioServerParameters.builder()
    .command("npx")   // hangs; "npx.cmd" works
    .args(ImmutableList.of(
        "-y", "@modelcontextprotocol/server-filesystem", "<some-dir>"))
    .build();

McpToolset toolset = new McpToolset(params.toServerParameters());
List<BaseTool> tools = toolset.getTools(null).toList().blockingGet();
// blocks: ~5 min per attempt, up to 3 retries, then throws

Observed behavior

command result
npx silent block, measured 900 s (~15 min) total, then McpToolLoadingException; no node.exe ever spawned
npx.cmd OK in ~3.5 s, 14 tools loaded

Workaround

Use the .cmd extension explicitly on Windows:

.command("npx.cmd")

Suggestions

  1. Add a Windows note to examples/mcpfilesystem/README.md (lowest
    effort, immediate relief for new users).
  2. Optionally, fail fast with a clear error when the configured command
    cannot be executed at all (e.g. probe with ProcessBuilder.start()
    before the 5-minute timeout), or auto-map well-known npm shims
    (npx / npm / node) to their .cmd variants on Windows.

ReproToolset.java

Happy to send a PR for either suggestion. Thanks!

Activity

  1. pinqionghongbai commented on Oct 22, 2026

    @pinqionghongbai
    Author

    Follow-up with two clarifications and one piece of cross-ecosystem
    evidence after further digging:

    1. Precise retry semantics. The title says "3 retries", but to be
    exact: MAX_RETRIES = 3 in McpToolset corresponds to 3 total
    attempts
    (1 initial + 2 retries), because the sync retry logic zips
    errors with Flowable.range(1, MAX_RETRIES). The measured 900,857 ms
    matches exactly 3 × 300 s.

    2. Repro completeness. Full runnable version is attached as
    ReproToolset.java (compiles against `com.g

    • Guava; replace <some-dir> with any existing absolute path). This is
      Windows-only — on Linux/macOS npm installs npx as a real shebang
      script, so command("npx") works there unchanged.

    3. Alignment with adk-python (per CONTRIBUTING). adk-python doesn't
    hit this because the Python MCP SDK handle

    # mcp/client/stdio.py
    if sys.platform == "win32":
        return await anyio.to_thread.run_sync(get_windows_executable_command, command, ...)
    
    # mcp/os/win32/utilities.py — resolves "npx" → "npx.cmd" via shutil.which
    for ext in [".cmd", ".bat", ".exe", ".ps1"]:
        ext_version = f"{command}{ext}"
        if ext_path := shutil.which(ext_version):
            return ext_path

    The Java MCP SDK (io.modelcontextprotocol.sdk 2.0.0,
    StdioClientTransport) has no equivalent, so adk-java inherits the gap.
    This suggests suggestion 2 (auto-mapping shims) could live in
    DefaultMcpTransportBuilder for adk-java, and/or upstream in the Java
    MCP SDK for ecosystem-wide parity with the Python SDK. Happy to pursue
    either path (CLA-ready).

  2. self-assigned this
    on Oct 22, 2026
  3. added theissue type on Oct 22, 2026
  4. hemasekhar-p commented on Oct 23, 2026

    @hemasekhar-p
    Contributor

    Hi @pinqionghongbai, thank you for reporting this issue. I have been able to reproduce the behavior with your inputs, We truly appreciate and welcomes your contribution. Would you be able to provide a pull request to address this issue in a robust way?

  5. added
    waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.
    on Oct 23, 2026
  6. added 3 commits that reference this issue on Oct 23, 2026
    18fcfe5
    a412c59
    52b539e
  7. pinqionghongbai commented on Oct 23, 2026

    @pinqionghongbai
    Author

    @hemasekhar-p Yes — opened #1691: #1691

    Before ServerParameters reaches StdioClientTransport, the command is
    resolved against PATH / PATHEXT the way shutil.which does — the same lookup the Python
    MCP SDK's get_windows_executable_command already performs. A bare command("npx") now reaches
    ProcessBuilder as npx.cmd.

    **Measured on the same machine, same repro,

    Result
    1.13.0 silent 900,857 ms → McpToolLoadingException, node.exe never started
    with the fix ~3.5–4.1 s, 14 tools loaded

    Covered by 14 resolver tests and 4 builder tests; full core suite is 2609 tests, 0 failures.

    One open question. This resolution arguably belongs in the upstream Java MCP SDK's
    StdioClientTransport rather than in ADK — ython SDK does it. I
    placed it in DefaultMcpTransportBuilder so ADK users get the fix without waiting on an upstream
    release, but the class can be lifted upstream as-is if you'd prefer that. Happy to rework either
    way.

    Thanks again for the quick turnaround on triage.

  8. hemasekhar-p commented on Oct 23, 2026

    @hemasekhar-p
    Contributor

    @pinqionghongbai, Thanks for quick fix, Our team is currently reviewing both the issue and your pull request and we will reach out if we need any further details. Thank you.

  9. added and removed
    waiting on reporterWaiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale.
    on Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions