Skip to content

MCP - StdioClientTransport doesn't handle ampersands ("&") as arguments to cmd.exe #594

@adner

Description

@adner

Describe the bug
When StdioClientTransport uses "cmd.exe /c" to invoke a command with arguments, it doesn't seem to work if an argument contains the ampersand character ("&"). Ran into this issue when I tried using StdioClientTransport to test the Dataverse MCP Server.

To Reproduce
The following code throws an exception:

var clientTransport = new StdioClientTransport(new StdioClientTransportOptions
        {
            Name = "DataverseMcpServer",
            Command = "Microsoft.PowerPlatform.Dataverse.MCP",
            Arguments = [
                "--ConnectionUrl",
                "https://make.powerautomate.com/environments/7c89bd81-ec79-e990-99eb-90d823595740/connections?apiName=shared_commondataserviceforapps&connectionName=91433eff0e204d9a96771a47117a7d48",
                "--MCPServerName",
                "DataverseMCPServer",
                "--TenantId",
                "ea59b638-3d02-4773-83a8-a7f8606da0b6",
                "--EnableHttpLogging",
                "true",
                "--EnableMsalLogging",
                "false",
                "--Debug",
                "false",
                "--BackendProtocol",
                "HTTP"
                ],
        });

        var client = await McpClientFactory.CreateAsync(clientTransport);

It is clear from the error log that it interprets the ampersand in argument 2 as a divider between two subsequent commands. Adding quotations around the argument it not helping either, since the MCP server in this case is not expecting this.

Expected behavior
The expectation is that the arguments specified this way should work, since this is the typical way that arguments are specified when configuring MCP Servers.

Logs
Exception has occurred: CLR/System.IO.IOException
An exception of type 'System.IO.IOException' occurred in System.Private.CoreLib.dll but was not handled in user code: 'MCP server process exited unexpectedly (exit code: 1)
Server's stderr tail:
Unhandled exception. System.ArgumentException: Invalid URL format. Missing connectionName.
at PowerPlatformMCPProxy.ConnectionManagement.UrlConnectionExtractor.ExtractUrlConnectionDetails() in C:__w\1\s\src\MCPServers\PowerPlatformMCPProxy\Clients\UrlConnectionExtractor.cs:line 38
at PowerPlatformMCPProxy.Runtime.MCPServerProxy.RunAsync() in C:__w\1\s\src\MCPServers\PowerPlatformMCPProxy\Runtime\MCPServerProxy.cs:line 58
at Program.

$(String[] args) in C:__w\1\s\src\MCPServers\PowerPlatformMCPProxy\Program.cs:line 35
at Program.(String[] args)
'connectionName' is not recognized as an internal or external command,
operable program or batch file.'
at System.Threading.Channels.AsyncOperation1.GetResult(Int16 token) at System.Threading.Channels.ChannelReader1.d__12.MoveNext()
at System.Threading.Channels.ChannelReader`1.d__12.System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult(Int16 token)
at ModelContextProtocol.McpSession.d__23.MoveNext() in c:\Sandboxes\SemanticKernelMcp\csharp-

Additional context
The following update to StdioClientTransport.ConnectAsync seems to be a workaround, which uses powershell instead of cmd.exe. This requires that the ampersand is surrounded by quotations, like so:

"https://make.powerautomate.com/environments/7c89bd81-ec79-e990-99eb-90d823595740/connections?apiName=shared_commondataserviceforapps\"&\"connectionName=91433eff0e204d9a96771a47117a7d48",

 public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default)
    {
        string endpointName = Name;

        Process? process = null;
        bool processStarted = false;

        string command = _options.Command;
        IList<string>? arguments = _options.Arguments;
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
            !string.Equals(Path.GetFileName(command), "powershell.exe", StringComparison.OrdinalIgnoreCase) &&
            !string.Equals(Path.GetFileName(command), "pwsh.exe", StringComparison.OrdinalIgnoreCase))
        {
            // On Windows, for stdio, we need to wrap non-shell commands in a shell.
            var originalCommand = command;
            command = "powershell.exe";

            var allArgs = arguments is null or [] ? new List<string>() : new List<string>(arguments);
            allArgs.Insert(0, originalCommand);

            var commandLine = string.Join(" ", allArgs); 
            arguments = new[] { "-Command", commandLine };
        }
...

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions