Skip to content

Commit d8768de

Browse files
Fix stdio client transport CLI argument escaping. (#811)
* Fix stdio client transport CLI argument escaping. * Fix failing Windows test. * Add more test cases and refine escaping logic. * Fix test failures in mono * Style fixes * Tweak stdio test * Tweak tests further
1 parent f331a69 commit d8768de

File tree

5 files changed

+137
-14
lines changed

5 files changed

+137
-14
lines changed

ModelContextProtocol.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<File Path="global.json" />
5757
<File Path="LICENSE" />
5858
<File Path="logo.png" />
59+
<File Path="Makefile" />
5960
<File Path="nuget.config" />
6061
<File Path="README.MD" />
6162
</Folder>

src/ModelContextProtocol.Core/Client/StdioClientTransport.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ public async Task<ITransport> ConnectAsync(CancellationToken cancellationToken =
9090
#if NET
9191
foreach (string arg in arguments)
9292
{
93-
startInfo.ArgumentList.Add(arg);
93+
startInfo.ArgumentList.Add(EscapeArgumentString(arg));
9494
}
9595
#else
9696
StringBuilder argsBuilder = new();
9797
foreach (string arg in arguments)
9898
{
99-
PasteArguments.AppendArgument(argsBuilder, arg);
99+
PasteArguments.AppendArgument(argsBuilder, EscapeArgumentString(arg));
100100
}
101101

102102
startInfo.Arguments = argsBuilder.ToString();
@@ -236,6 +236,26 @@ internal static bool HasExited(Process process)
236236
}
237237
}
238238

239+
private static string EscapeArgumentString(string argument) =>
240+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !ContainsWhitespaceRegex.IsMatch(argument) ?
241+
WindowsCliSpecialArgumentsRegex.Replace(argument, static match => "^" + match.Value) :
242+
argument;
243+
244+
private const string WindowsCliSpecialArgumentsRegexString = "[&^><|]";
245+
246+
#if NET
247+
private static Regex WindowsCliSpecialArgumentsRegex => GetWindowsCliSpecialArgumentsRegex();
248+
private static Regex ContainsWhitespaceRegex => GetContainsWhitespaceRegex();
249+
250+
[GeneratedRegex(WindowsCliSpecialArgumentsRegexString, RegexOptions.CultureInvariant)]
251+
private static partial Regex GetWindowsCliSpecialArgumentsRegex();
252+
[GeneratedRegex(@"\s", RegexOptions.CultureInvariant)]
253+
private static partial Regex GetContainsWhitespaceRegex();
254+
#else
255+
private static Regex WindowsCliSpecialArgumentsRegex { get; } = new(WindowsCliSpecialArgumentsRegexString, RegexOptions.Compiled | RegexOptions.CultureInvariant);
256+
private static Regex ContainsWhitespaceRegex { get; } = new(@"\s", RegexOptions.Compiled | RegexOptions.CultureInvariant);
257+
#endif
258+
239259
[LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} connecting.")]
240260
private static partial void LogTransportConnecting(ILogger logger, string endpointName);
241261

tests/ModelContextProtocol.TestServer/Program.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ private static async Task Main(string[] args)
3636
{
3737
Log.Logger.Information("Starting server...");
3838

39+
string? cliArg = ParseCliArgument(args);
3940
McpServerOptions options = new()
4041
{
4142
Capabilities = new ServerCapabilities(),
4243
ServerInstructions = "This is a test server with only stub functionality",
4344
};
4445

45-
ConfigureTools(options);
46+
ConfigureTools(options, cliArg);
4647
ConfigureResources(options);
4748
ConfigurePrompts(options);
4849
ConfigureLogging(options);
@@ -104,7 +105,7 @@ await server.SendMessageAsync(new JsonRpcNotification
104105
}
105106
}
106107

107-
private static void ConfigureTools(McpServerOptions options)
108+
private static void ConfigureTools(McpServerOptions options, string? cliArg)
108109
{
109110
options.Handlers.ListToolsHandler = async (request, cancellationToken) =>
110111
{
@@ -199,6 +200,13 @@ private static void ConfigureTools(McpServerOptions options)
199200
Content = [new TextContentBlock { Text = $"LLM sampling result: {(sampleResult.Content as TextContentBlock)?.Text}" }]
200201
};
201202
}
203+
else if (request.Params?.Name == "echoCliArg")
204+
{
205+
return new CallToolResult
206+
{
207+
Content = [new TextContentBlock { Text = cliArg ?? "null" }]
208+
};
209+
}
202210
else
203211
{
204212
throw new McpException($"Unknown tool: {request.Params?.Name}", McpErrorCode.InvalidParams);
@@ -522,6 +530,19 @@ static CreateMessageRequestParams CreateRequestSamplingParams(string context, st
522530
};
523531
}
524532

533+
private static string? ParseCliArgument(string[] args)
534+
{
535+
foreach (var arg in args)
536+
{
537+
if (arg.StartsWith("--cli-arg="))
538+
{
539+
return arg["--cli-arg=".Length..];
540+
}
541+
}
542+
543+
return null;
544+
}
545+
525546
const string MCP_TINY_IMAGE =
526547
"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";
527548
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
using System.Runtime.InteropServices;
2+
13
namespace ModelContextProtocol.Tests;
24

35
internal static class PlatformDetection
46
{
57
public static bool IsMonoRuntime { get; } = Type.GetType("Mono.Runtime") is not null;
8+
public static bool IsWindows { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
69
}

tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ModelContextProtocol.Client;
2+
using ModelContextProtocol.Protocol;
23
using ModelContextProtocol.Tests.Utils;
34
using System.Runtime.InteropServices;
45
using System.Text;
@@ -15,17 +16,14 @@ public async Task CreateAsync_ValidProcessInvalidServer_Throws()
1516
string id = Guid.NewGuid().ToString("N");
1617

1718
StdioClientTransport transport = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
18-
new(new() { Command = "cmd", Arguments = ["/C", $"echo \"{id}\" >&2"] }, LoggerFactory) :
19-
new(new() { Command = "ls", Arguments = [id] }, LoggerFactory);
19+
new(new() { Command = "cmd", Arguments = ["/c", $"echo {id} >&2 & exit /b 1"] }, LoggerFactory) :
20+
new(new() { Command = "sh", Arguments = ["-c", $"echo {id} >&2; exit 1"] }, LoggerFactory);
2021

21-
IOException e = await Assert.ThrowsAsync<IOException>(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
22-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
23-
{
24-
Assert.Contains(id, e.ToString());
25-
}
22+
await Assert.ThrowsAsync<IOException>(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
2623
}
2724

28-
[Fact(Skip = "Platform not supported by this test.", SkipUnless = nameof(IsStdErrCallbackSupported))]
25+
// [Fact(Skip = "Platform not supported by this test.", SkipUnless = nameof(IsStdErrCallbackSupported))]
26+
[Fact]
2927
public async Task CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked()
3028
{
3129
string id = Guid.NewGuid().ToString("N");
@@ -43,12 +41,92 @@ public async Task CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked()
4341
};
4442

4543
StdioClientTransport transport = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
46-
new(new() { Command = "cmd", Arguments = ["/C", $"echo \"{id}\" >&2"], StandardErrorLines = stdErrCallback }, LoggerFactory) :
47-
new(new() { Command = "ls", Arguments = [id], StandardErrorLines = stdErrCallback }, LoggerFactory);
44+
new(new() { Command = "cmd", Arguments = ["/c", $"echo {id} >&2 & exit /b 1"], StandardErrorLines = stdErrCallback }, LoggerFactory) :
45+
new(new() { Command = "sh", Arguments = ["-c", $"echo {id} >&2; exit 1"], StandardErrorLines = stdErrCallback }, LoggerFactory);
4846

4947
await Assert.ThrowsAsync<IOException>(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
5048

5149
Assert.InRange(count, 1, int.MaxValue);
5250
Assert.Contains(id, sb.ToString());
5351
}
52+
53+
[Theory]
54+
[InlineData(null)]
55+
[InlineData("argument with spaces")]
56+
[InlineData("&")]
57+
[InlineData("|")]
58+
[InlineData(">")]
59+
[InlineData("<")]
60+
[InlineData("^")]
61+
[InlineData(" & ")]
62+
[InlineData(" | ")]
63+
[InlineData(" > ")]
64+
[InlineData(" < ")]
65+
[InlineData(" ^ ")]
66+
[InlineData("& ")]
67+
[InlineData("| ")]
68+
[InlineData("> ")]
69+
[InlineData("< ")]
70+
[InlineData("^ ")]
71+
[InlineData(" &")]
72+
[InlineData(" |")]
73+
[InlineData(" >")]
74+
[InlineData(" <")]
75+
[InlineData(" ^")]
76+
[InlineData("^&<>|")]
77+
[InlineData("^&<>| ")]
78+
[InlineData(" ^&<>|")]
79+
[InlineData("\t^&<>")]
80+
[InlineData("^&\t<>")]
81+
[InlineData("ls /tmp | grep foo.txt > /dev/null")]
82+
[InlineData("let rec Y f x = f (Y f) x")]
83+
[InlineData("value with \"quotes\" and spaces")]
84+
[InlineData("C:\\Program Files\\Test App\\app.dll")]
85+
[InlineData("C:\\EndsWithBackslash\\")]
86+
[InlineData("--already-looks-like-flag")]
87+
[InlineData("-starts-with-dash")]
88+
[InlineData("name=value=another")]
89+
[InlineData("$(echo injected)")]
90+
[InlineData("value-with-\"quotes\"-and-\\backslashes\\")]
91+
[InlineData("http://localhost:1234/callback?foo=1&bar=2")]
92+
public async Task EscapesCliArgumentsCorrectly(string? cliArgumentValue)
93+
{
94+
if (PlatformDetection.IsMonoRuntime && cliArgumentValue?.EndsWith("\\") is true)
95+
{
96+
Assert.Skip("mono runtime does not handle arguments ending with backslash correctly.");
97+
}
98+
99+
string cliArgument = $"--cli-arg={cliArgumentValue}";
100+
101+
StdioClientTransportOptions options = new()
102+
{
103+
Name = "TestServer",
104+
Command = (PlatformDetection.IsMonoRuntime, PlatformDetection.IsWindows) switch
105+
{
106+
(true, _) => "mono",
107+
(_, true) => "TestServer.exe",
108+
_ => "dotnet",
109+
},
110+
Arguments = (PlatformDetection.IsMonoRuntime, PlatformDetection.IsWindows) switch
111+
{
112+
(true, _) => ["TestServer.exe", cliArgument],
113+
(_, true) => [cliArgument],
114+
_ => ["TestServer.dll", cliArgument],
115+
},
116+
};
117+
118+
var transport = new StdioClientTransport(options, LoggerFactory);
119+
120+
// Act: Create client (handshake) and list tools to ensure full round trip works with the argument present.
121+
await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
122+
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
123+
124+
// Assert
125+
Assert.NotNull(tools);
126+
Assert.NotEmpty(tools);
127+
128+
var result = await client.CallToolAsync("echoCliArg", cancellationToken: TestContext.Current.CancellationToken);
129+
var content = Assert.IsType<TextContentBlock>(Assert.Single(result.Content));
130+
Assert.Equal(cliArgumentValue ?? "", content.Text);
131+
}
54132
}

0 commit comments

Comments
 (0)