Skip to content

Windows stdio resolves .ps1 then fails to spawn it #3496

Description

@yongpange

Initial Checks

Release line

2.x (current stable)

Description

On Windows, get_windows_executable_command() already looks up .ps1 (alongside .cmd / .bat / .exe). The spawn path then passes that path to CreateProcess / anyio.open_process([command, *args]).

Windows cannot execute a .ps1 file directly. The lookup therefore advertises a working command that later fails with WinError 193 (not a valid Win32 application) or FileNotFoundError.

Expected: a command that resolves to something.ps1 is launched through PowerShell (powershell / pwsh -NoProfile -NonInteractive -ExecutionPolicy Bypass -File <script> ...args).

Actual: the SDK finds the .ps1 and then cannot start it.

I hit this on Windows while starting a stdio MCP server whose command is a PowerShell script (or a bare name that shutil.which resolves to .ps1).

I used an LLM to help draft this report after reproducing the failure locally.

Example Code

import asyncio
from pathlib import Path
from mcp.client.stdio import StdioServerParameters, stdio_client

# On Windows, CreateProcess cannot run a .ps1 path returned by
# get_windows_executable_command().
params = StdioServerParameters(command=str(Path(rC:\path\to\server.ps1)))

async def main():
    async with stdio_client(params) as streams:
        print(streams)

asyncio.run(main())

Direct spawn of the same path also fails:

import subprocess
subprocess.Popen([rC:\path\to\echo-ok.ps1])  # WinError 193

Launching via PowerShell works:

subprocess.Popen(
    [powershell, -NoProfile, -NonInteractive, -ExecutionPolicy, Bypass, -File, rC:\path\to\echo-ok.ps1],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    creationflags=getattr(subprocess, CREATE_NO_WINDOW, 0),
)

Python & MCP Python SDK

Python 3.12 on Windows 10/11
MCP Python SDK 2.2.0 (modelcontextprotocol/python-sdk main as of this report)

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 13, 2026
  2. FOWEPJF255 commented on Sep 13, 2026

    @FOWEPJF255

    Confirmed on Windows 10 (PowerShell): a .ps1 path cannot be started via CreateProcess / subprocess.run([ps1_path]) 鈥?fails with OSError: [WinError 193] (not a valid Win32 application). That matches the report: get_windows_executable_command() can return a .ps1 from shutil.which, then stdio spawn fails.

    Expected shape for a fix: when the resolved command ends with .ps1, launch via pwsh/powershell with -NoProfile -NonInteractive -ExecutionPolicy Bypass -File <script> and forward args.

    AI-assisted note: I reproduced the Win32 spawn failure locally and read get_windows_executable_command / stdio spawn path; happy to take an assigned PR if maintainers want an outside fix.

  3. dltsum commented on Sep 13, 2026

    @dltsum

    Hi! I'd like to work on this issue — could you assign it to me?

    I have a complete fix ready in PR #3497 (auto-closed per the assignment rule; it should reopen on its own once assigned). The fix rewrites the argv in create_windows_process so a resolved .ps1 command launches through pwsh/powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File, with a clear FileNotFoundError when no PowerShell host is on PATH. Both the anyio.open_process path and the SelectorEventLoop FallbackProcess path are covered.

    Verified on Windows 11 with a new integration test (real .ps1 echo server doing a JSON-RPC round trip via stdio_client): fails without the fix with the exact reported WinError 193, passes with it. Thanks!

  4. chengwudi1 commented on Sep 13, 2026

    @chengwudi1

    Reproduced on Windows 11 (Python 3.12, main). Direct spawn of a .ps1 fails exactly as you describe — this is the same call create_windows_process makes:

    >>> subprocess.Popen([r"...\echo-ok.ps1"])
    OSError: [WinError 193] %1 is not a valid Win32 application.
    

    One refinement to the trigger, since it decides which code path a fix has to cover: on a default Windows the built-in PATHEXT is .COM;.EXE;.BAT;.CMD;.VBS;... — no .PS1. So shutil.which("server") does not resolve a bare name to a .ps1, and the explicit shutil.which(f"{command}.ps1") in the extension loop does not either (it appends PATHEXT again, finds nothing). The case that does resolve is a command that already carries a directory component — C:\path\server.ps1, or ./server.ps1 — which shutil.which returns as-is via its dirname branch, never reaching the extension list. That is the reproduction I can drive end to end; the bare-name variant in the report would need PATHEXT customized to include .PS1.

    Two things I checked while working out the fix shape:

    • powershell -NoProfile -NonInteractive -File <script> <args...> forwards arguments correctly, including ones that look like switches (--model gpt-oss arrives as two intact argv entries), and stdin/stdout piping works. -File is enough; no -Command quoting needed.
    • pwsh is not guaranteed to exist. This machine has only Windows PowerShell 5.1 (System32\WindowsPowerShell�1.0\powershell.exe), so a launcher should prefer pwsh and fall back, with a clear error naming what it looked for when neither resolves.

    Approach I would take: detect .ps1 at the spawn boundary — after resolution, before anyio.open_process — and wrap the argv there, so both the anyio path and the FallbackProcess path pick it up without touching the resolver's return contract.

    Disclosure: AI-assisted — I used an agent to probe spawn behaviour on this machine and draft this; I ran the commands above and can explain them. Reporter has first call if they want this one.

  5. FOWEPJF255 commented on Sep 14, 2026

    @FOWEPJF255

    Thanks @dltsum 鈥?your #3497 shape (rewrite argv in create_windows_process through pwsh/powershell -File, covering both anyio and FallbackProcess) matches the WinError 193 path I reproduced. Happy to defer to that PR once assigned; no competing work from me.

  6. maxisbey commented on Sep 16, 2026

    @maxisbey
    Contributor

    The failure here isn't specific to the SDK. CreateProcess can't execute a .ps1 file, so subprocess.Popen([r"C:\path\server.ps1"]) raises WinError 193 in any Python program, which is what the report's own example shows. The case that reproduces end to end is passing an explicit script path as command, and that amounts to asking the OS to execute a file it can't execute.

    The way to run a PowerShell script as a stdio server is to name the interpreter, the same as for a .py or .js script:

    StdioServerParameters(
        command="powershell",  # or "pwsh"
        args=["-NoProfile", "-NonInteractive", "-File", r"C:\path\to\server.ps1"],
    )

    I don't think the SDK should do that wrapping itself. It would have to pick between pwsh and powershell for you and decide on flags like -ExecutionPolicy Bypass, which overrides a security setting on the machine. Those choices belong to whoever configures the server.

    On the point that the SDK "advertises" .ps1: it doesn't document or claim .ps1 support anywhere. The only mention is that get_windows_executable_command(), an internal helper, includes .ps1 in the list of extensions it probes when looking up a command:

    for ext in [".cmd", ".bat", ".exe", ".ps1"]:

    That entry is a leftover from the original Windows stdio work and could never have launched anything. We may drop it, but it isn't a promise to run PowerShell scripts.

    Closing as not planned.

    A note on the thread itself: several comments here are agent-written confirmations of each other, plus a request to be assigned a PR that had already been opened. That doesn't add signal, and it isn't how issues get assigned here (see CONTRIBUTING.md). If you hit a problem yourself, a report of what you ran and what happened is always welcome. A chorus of "reproduced, happy to take this" on someone else's issue isn't.

    AI Disclaimer

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions