11using ModelContextProtocol . Client ;
2+ using ModelContextProtocol . Protocol ;
23using ModelContextProtocol . Tests . Utils ;
34using System . Runtime . InteropServices ;
45using 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