Skip to content

fix(stdio): handle spaces in Command path on Windows (cmd /d /s /c)#1703

Open
yayayouyou wants to merge 2 commits into
modelcontextprotocol:mainfrom
yayayouyou:fix/stdio-cmd-space-in-command
Open

fix(stdio): handle spaces in Command path on Windows (cmd /d /s /c)#1703
yayayouyou wants to merge 2 commits into
modelcontextprotocol:mainfrom
yayayouyou:fix/stdio-cmd-space-in-command

Conversation

@yayayouyou

Copy link
Copy Markdown

Fixes #1601.

Problem

On Windows, StdioClientTransport wraps every non-cmd.exe command as cmd.exe /c <command> <args...>, adding the arguments through ProcessStartInfo.ArgumentList. When the command is an absolute path that contains a space (e.g. anything under C:\Program Files\...) and there is at least one argument, ArgumentList quotes both the space-containing path and the argument, so more than two quote characters follow /c. Per cmd /?, cmd then strips the first and last quote and re-parses the middle, so it tries to launch C:\Program and the MCP server never starts. The same executable at a space-free path works, which is why this is easy to miss (npx, uvicorn, dotnet all live on space-free PATH entries).

Fix

Build the cmd.exe command line by hand for the wrapping case and assign it to startInfo.Arguments directly, bypassing ArgumentList:

  • Invoke cmd.exe /d /s /c "<inner>". /s makes cmd strip exactly the single outer pair of quotes and take the rest verbatim (avoiding the multi-quote re-parsing rule); /d skips AutoRun.
  • <inner> is the command followed by the arguments, each quoted with the Win32 CommandLineToArgvW rules only when it contains whitespace, a quote, or a cmd metacharacter (& | < > ^ ( )) — quoting also makes those metacharacters literal. A bare command name is left unquoted so cmd still resolves it against the current directory / PATH.
  • The ^-escaping used on the ArgumentList path (EscapeArgumentString) is not applied here, since those characters are already literal inside the quoted /s /c region.

Non-Windows and the explicit cmd.exe command path are unchanged.

Testing

Verified on Windows 11 with .NET 10:

  • New WindowsCmdWrapper_UsesDsCWithSingleOuterQuotePair asserts the exact generated command line for: space-in-path + arg, arg with spaces, no-args, metacharacters, plain args, and an empty arg.
  • New CommandPathWithSpaces_RoundTripsArgumentsThroughCmd copies the test server to a directory whose path contains a space, launches it by absolute path, and asserts the argument echoes back unchanged for spaces, & ^ < > |, quotes, trailing backslash, and a URL with &.
  • The existing EscapesCliArgumentsCorrectly round-trip test (no-space command + tricky args) still passes, confirming no regression for the common case.

dotnet test tests/ModelContextProtocol.Tests/ is green.


AI-assistance disclosure: I used AI assistance to investigate and implement this change, and I reviewed and tested it myself on Windows.

On Windows, StdioClientTransport wraps non-cmd.exe commands as
`cmd.exe /c <command> <args...>` and adds the arguments through
ProcessStartInfo.ArgumentList. When the command is an absolute path
containing a space (e.g. under "Program Files") and there is at least one
argument, ArgumentList quotes both the path and the argument, so more than
two quote characters follow /c. cmd then strips the first and last quote
and re-parses the middle, tries to launch "C:\Program", and the server
never starts.

Build the cmd.exe command line by hand for the wrapping case and assign it
to startInfo.Arguments directly, bypassing ArgumentList:

- Invoke `cmd.exe /d /s /c "<inner>"`. /s makes cmd strip exactly the single
  outer pair of quotes and take the rest verbatim; /d skips AutoRun.
- Quote the command and each argument (Win32 CommandLineToArgvW rules) only
  when it contains whitespace, a quote, or a cmd metacharacter (& | < > ^ ( )),
  which also makes those metacharacters literal. A bare command name is left
  unquoted so cmd still resolves it against the current directory and PATH.

Non-Windows and the explicit cmd.exe command path are unchanged.

Adds a unit test asserting the exact generated command line and a
Windows-only integration test that launches a server from a directory whose
path contains a space and round-trips arguments (spaces, & ^ < > |, quotes,
trailing backslash).

Fixes modelcontextprotocol#1601

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 14, 2026 04:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Windows-specific StdioClientTransport startup failure when the configured Command is an absolute path containing spaces and at least one argument is present, by avoiding ProcessStartInfo.ArgumentList for the cmd.exe wrapping case and instead emitting a cmd.exe /d /s /c "<inner...>" command line directly.

Changes:

  • Reworks Windows cmd.exe wrapping in StdioClientTransport to build a single pre-quoted arguments string (via GetWindowsCmdArguments) and assign it to ProcessStartInfo.Arguments.
  • Adds Windows-only regression tests that validate the exact cmd.exe command line shape and a round-trip integration test using a copied test server under a space-containing directory.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/ModelContextProtocol.Core/Client/StdioClientTransport.cs Builds the Windows cmd.exe wrapper invocation as a single /d /s /c "<inner>" argument string to preserve space-containing executable paths.
tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs Adds Windows-only regression coverage for the wrapper command line and for launching a server from a path containing spaces.

Comment on lines +67 to +69
// On Windows, for stdio, we need to wrap non-shell commands with cmd.exe /c {command} (usually npx or uvicorn).
// The stdio transport will not work correctly if the command is not run in a shell.
string? cmdArguments = null;
Comment on lines +204 to +222
const string spacedCommand = @"C:\Program Files\My Server\server.exe";

var cases = new (string command, string[]? arguments, string expectedArguments)[]
{
// Space in the command path with a trailing argument — the exact shape that failed in #1601.
(spacedCommand, ["--flag"], Wrap(Quote(spacedCommand) + " --flag")),
// Space in the command path with an argument that itself contains spaces.
(spacedCommand, ["arg with spaces"], Wrap(Quote(spacedCommand) + " " + Quote("arg with spaces"))),
// Space in the command path with no arguments.
(spacedCommand, null, Wrap(Quote(spacedCommand))),
// A space-free command path is left unquoted; cmd.exe metacharacters in arguments are quoted so
// cmd treats them literally rather than as operators.
(@"C:\tools\server.exe", ["a&b", "c|d", "e>f", "g<h", "i^j"],
Wrap(@"C:\tools\server.exe" + " " + Quote("a&b") + " " + Quote("c|d") + " " + Quote("e>f") + " " + Quote("g<h") + " " + Quote("i^j"))),
// A bare command name and ordinary arguments without special characters are passed through unquoted.
("server.exe", ["--key=value", "plain"], Wrap("server.exe" + " --key=value plain")),
// An empty argument is preserved as an empty quoted token.
("server.exe", [""], Wrap("server.exe" + " " + Quote(""))),
};
- Update the wrapping comment to reflect the hand-built `cmd.exe /d /s /c`
  command line instead of the old `cmd.exe /c {command}` description.
- Use guaranteed-nonexistent, GUID-based command names in
  WindowsCmdWrapper_UsesDsCWithSingleOuterQuotePair so the wrapper's cmd.exe
  never launches a real program if a name like `server.exe` happens to
  resolve from PATH; the test only inspects the logged command line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yayayouyou

Copy link
Copy Markdown
Author

Thanks @copilot — addressed both points in 0426932:

  • Updated the wrapping comment to describe the hand-built cmd.exe /d /s /c command line (rather than the old /c {command} wording).
  • Changed WindowsCmdWrapper_UsesDsCWithSingleOuterQuotePair to use guaranteed-nonexistent, GUID-based command names (a unique nonexistent directory + a bare name that won't resolve from PATH), so the wrapper's cmd.exe can't accidentally launch a real program; the test only inspects the logged command line.

dotnet test tests/ModelContextProtocol.Tests/ (net10.0) remains green.

(AI-assisted, reviewed and tested on Windows.)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StdioClientTransportOptions is failing with space in Command

3 participants