@@ -878,6 +878,13 @@ func TestBuildExecArgs_WithMCPServers_SSE(t *testing.T) {
878878}
879879
880880// TestBuildExecArgs_WithMCPServers_Stdio tests stdio MCP server serialization.
881+ //
882+ // Args must be emitted as a single TOML inline array under one -c override.
883+ // Emitting one -c mcp_servers.NAME.args=VAL per element does NOT accumulate
884+ // into an array — each -c override replaces the previous value at the same
885+ // dotted path. Codex's config schema requires Vec<String>, so repeated scalar
886+ // overrides cause the subprocess to exit before handshake with
887+ // `invalid type: string ..., expected a sequence`.
881888func TestBuildExecArgs_WithMCPServers_Stdio (t * testing.T ) {
882889 options := & config.Options {
883890 MCPServers : map [string ]mcp.ServerConfig {
@@ -893,12 +900,90 @@ func TestBuildExecArgs_WithMCPServers_Stdio(t *testing.T) {
893900
894901 require .Contains (t , args , "mcp_servers.local-tool.type=stdio" )
895902 require .Contains (t , args , "mcp_servers.local-tool.command=/usr/bin/my-tool" )
896- require .Contains (t , args , "mcp_servers.local-tool.args=--port" )
897- require .Contains (t , args , "mcp_servers.local-tool.args=8080" )
903+ require .Contains (t , args , `mcp_servers.local-tool.args=["--port","8080"]` ,
904+ "Args must be serialized as a single TOML inline array override" )
905+ require .NotContains (t , args , "mcp_servers.local-tool.args=--port" ,
906+ "Args must NOT be emitted as repeated scalar -c overrides" )
907+ require .NotContains (t , args , "mcp_servers.local-tool.args=8080" ,
908+ "Args must NOT be emitted as repeated scalar -c overrides" )
898909 require .Contains (t , args , "mcp_servers.local-tool.env.DEBUG=true" )
899910 require .Contains (t , args , "mcp_servers.local-tool.env.HOME=/tmp" )
900911}
901912
913+ // TestBuildExecArgs_WithMCPServers_StdioArgsNpx covers the real-world npx case
914+ // from the issue report: `npx -y @playwright/mcp@latest`.
915+ func TestBuildExecArgs_WithMCPServers_StdioArgsNpx (t * testing.T ) {
916+ options := & config.Options {
917+ MCPServers : map [string ]mcp.ServerConfig {
918+ "playwright" : & mcp.StdioServerConfig {
919+ Command : "npx" ,
920+ Args : []string {"-y" , "@playwright/mcp@latest" },
921+ },
922+ },
923+ }
924+
925+ args := BuildExecArgs ("test" , options )
926+
927+ require .Contains (t , args , `mcp_servers.playwright.args=["-y","@playwright/mcp@latest"]` )
928+ }
929+
930+ // TestBuildExecArgs_WithMCPServers_StdioArgsSingleton ensures even a single arg
931+ // is wrapped in array form — codex config declares args as Vec<String>, so a
932+ // single scalar value fails type-checking even with one element.
933+ func TestBuildExecArgs_WithMCPServers_StdioArgsSingleton (t * testing.T ) {
934+ options := & config.Options {
935+ MCPServers : map [string ]mcp.ServerConfig {
936+ "solo" : & mcp.StdioServerConfig {
937+ Command : "tool" ,
938+ Args : []string {"only-arg" },
939+ },
940+ },
941+ }
942+
943+ args := BuildExecArgs ("test" , options )
944+
945+ require .Contains (t , args , `mcp_servers.solo.args=["only-arg"]` )
946+ require .NotContains (t , args , "mcp_servers.solo.args=only-arg" )
947+ }
948+
949+ // TestBuildExecArgs_WithMCPServers_StdioArgsEscaping verifies that args with
950+ // quotes, backslashes, or other special characters are safely escaped so the
951+ // resulting TOML is valid.
952+ func TestBuildExecArgs_WithMCPServers_StdioArgsEscaping (t * testing.T ) {
953+ options := & config.Options {
954+ MCPServers : map [string ]mcp.ServerConfig {
955+ "edge" : & mcp.StdioServerConfig {
956+ Command : "echo" ,
957+ Args : []string {`has "quote"` , `c:\path\to` },
958+ },
959+ },
960+ }
961+
962+ args := BuildExecArgs ("test" , options )
963+
964+ require .Contains (t , args , `mcp_servers.edge.args=["has \"quote\"","c:\\path\\to"]` )
965+ }
966+
967+ // TestBuildExecArgs_WithMCPServers_StdioArgsEmpty ensures no args= override is
968+ // emitted at all when Args is empty — emitting args="" would make codex see a
969+ // scalar empty string instead of an empty Vec<String>.
970+ func TestBuildExecArgs_WithMCPServers_StdioArgsEmpty (t * testing.T ) {
971+ options := & config.Options {
972+ MCPServers : map [string ]mcp.ServerConfig {
973+ "no-args" : & mcp.StdioServerConfig {
974+ Command : "my-cmd" ,
975+ },
976+ },
977+ }
978+
979+ args := BuildExecArgs ("test" , options )
980+
981+ for _ , a := range args {
982+ require .NotContains (t , a , "mcp_servers.no-args.args" ,
983+ "Empty Args must not produce any args= override" )
984+ }
985+ }
986+
902987// TestBuildExecArgs_WithMCPServers_SDKSkipped tests that SDK servers are skipped.
903988func TestBuildExecArgs_WithMCPServers_SDKSkipped (t * testing.T ) {
904989 options := & config.Options {
0 commit comments