Describe the bug
On Windows, StdioClientTransport wraps every non-cmd.exe command as cmd.exe /c <Command> <Arguments...>. When Command is an absolute path that contains a space (e.g. anything under C:\Program Files\...) and Arguments is non-empty, the resulting command line confuses cmd.exe's /c quote-handling rules: cmd strips the outer quotes around the executable path, splits on the first space, and tries to launch a non-existent program. The MCP server is never started.
The relevant code is in [src/ModelContextProtocol.Core/Client/StdioClientTransport.cs](https://github.com/modelcontextprotocol/csharp-sdk/blob/main/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs):
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
!string.Equals(Path.GetFileName(command), "cmd.exe", StringComparison.OrdinalIgnoreCase))
{
arguments = arguments is null or [] ? ["/c", command] : ["/c", command, ..arguments];
command = "cmd.exe";
}
Per cmd /?, when /c is followed by more than two quote characters, cmd strips the first and last quote and re-parses the middle. Because ProcessStartInfo.ArgumentList correctly quotes both the space-containing path and any further argument, you always end up with 4+ quote characters and trigger this stripping behavior. The /s switch plus an extra outer pair of quotes around the whole command-and-args region would prevent this, but the SDK does not emit either.
To Reproduce
Steps to reproduce the behavior:
- Place any working stdio MCP server executable at a path containing a space, e.g.
C:\Program Files\MssqlMcp\server.exe.
- Create a client with at least one argument:
var options = new StdioClientTransportOptions
{
Name = "MssqlMcp",
Command = @"C:\Program Files\MssqlMcp\server.exe",
Arguments = [connectionString]
};
var transport = new StdioClientTransport(options);
var client = await McpClient.CreateAsync(transport);
- Observe that
CreateAsync fails — the child process either exits immediately or never starts.
- Move/copy the same executable to a space-free path (e.g.
C:\Tools\MssqlMcp\server.exe), update Command, and the exact same code works.
Expected behavior
StdioClientTransport should launch the configured executable regardless of whether its path contains spaces, just as it does for paths without spaces. The Windows cmd /c wrapping should be transparent to the caller.
Logs
Trace-level log from StdioClientTransport shows the wrapped invocation; the spawned cmd.exe produces output like:
'C:\Program' is not recognized as an internal or external command,
operable program or batch file.
(Exact message varies depending on whether the path's first space-separated token happens to match an existing file.)
Additional context
- Platform: Windows (any version). Not reproducible on Linux/macOS — the wrapping branch is Windows-only.
- Reproduces whenever
Path.GetFileName(Command) != "cmd.exe" and the path contains a space and Arguments is non-empty. With zero arguments, cmd's "exactly two quotes around an executable" special case happens to make it work, which is why this bug is intermittent and easy to miss in samples (npx, dotnet, uvicorn all live on space-free PATH entries).
Workarounds users currently have to apply:
- Install/copy the server to a space-free directory.
A proper fix would be for the SDK to either (a) build the joined-and-quoted command string itself when wrapping with cmd (e.g. ["/d", "/s", "/c", $"\"\"{command}\" {joinedArgs}\""]) rather than relying on ArgumentList's per-arg quoting, or (b) skip the cmd wrapping entirely when Command resolves to a real executable file — the original justification in the source comment ("usually npx or uvicorn") only applies to PATH-resolved batch/script commands, not absolute .exe paths.
Describe the bug
On Windows,
StdioClientTransportwraps every non-cmd.execommand ascmd.exe /c <Command> <Arguments...>. WhenCommandis an absolute path that contains a space (e.g. anything underC:\Program Files\...) andArgumentsis non-empty, the resulting command line confusescmd.exe's/cquote-handling rules: cmd strips the outer quotes around the executable path, splits on the first space, and tries to launch a non-existent program. The MCP server is never started.The relevant code is in
[src/ModelContextProtocol.Core/Client/StdioClientTransport.cs](https://github.com/modelcontextprotocol/csharp-sdk/blob/main/src/ModelContextProtocol.Core/Client/StdioClientTransport.cs):Per
cmd /?, when/cis followed by more than two quote characters, cmd strips the first and last quote and re-parses the middle. BecauseProcessStartInfo.ArgumentListcorrectly quotes both the space-containing path and any further argument, you always end up with 4+ quote characters and trigger this stripping behavior. The/sswitch plus an extra outer pair of quotes around the whole command-and-args region would prevent this, but the SDK does not emit either.To Reproduce
Steps to reproduce the behavior:
C:\Program Files\MssqlMcp\server.exe.CreateAsyncfails — the child process either exits immediately or never starts.C:\Tools\MssqlMcp\server.exe), updateCommand, and the exact same code works.Expected behavior
StdioClientTransportshould launch the configured executable regardless of whether its path contains spaces, just as it does for paths without spaces. The Windowscmd /cwrapping should be transparent to the caller.Logs
Trace-level log from
StdioClientTransportshows the wrapped invocation; the spawnedcmd.exeproduces output like:(Exact message varies depending on whether the path's first space-separated token happens to match an existing file.)
Additional context
Path.GetFileName(Command) != "cmd.exe"and the path contains a space andArgumentsis non-empty. With zero arguments, cmd's "exactly two quotes around an executable" special case happens to make it work, which is why this bug is intermittent and easy to miss in samples (npx,dotnet,uvicornall live on space-free PATH entries).Workarounds users currently have to apply:
A proper fix would be for the SDK to either (a) build the joined-and-quoted command string itself when wrapping with cmd (e.g.
["/d", "/s", "/c", $"\"\"{command}\" {joinedArgs}\""]) rather than relying onArgumentList's per-arg quoting, or (b) skip the cmd wrapping entirely whenCommandresolves to a real executable file — the original justification in the source comment ("usually npx or uvicorn") only applies to PATH-resolved batch/script commands, not absolute.exepaths.