Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1842,7 +1842,7 @@ public sealed class McpStdioServerConfig : McpServerConfig
/// Arguments to pass to the command.
/// </summary>
[JsonPropertyName("args")]
public IList<string> Args { get => field ??= []; set; }
public IList<string>? Args { get; set; }

/// <summary>
/// Environment variables to pass to the server.
Expand Down
28 changes: 28 additions & 0 deletions dotnet/test/E2E/SessionMcpAndAgentConfigE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,34 @@ public async Task Should_Accept_MCP_Server_Configuration_On_Session_Create()
await session.DisposeAsync();
}

[Fact]
public async Task Should_Accept_MCP_Server_Configuration_Without_Args()
{
var mcpServers = new Dictionary<string, McpServerConfig>
{
["test-server"] = new McpStdioServerConfig
{
Command = "echo",
Tools = ["*"]
}
};

var session = await CreateSessionAsync(new SessionConfig
{
McpServers = mcpServers
});

Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);

await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" });

var message = await TestHelper.GetFinalAssistantMessageAsync(session);
Assert.NotNull(message);
Assert.Contains("4", message!.Data.Content);

await session.DisposeAsync();
}

[Fact]
public async Task Should_Accept_MCP_Server_Configuration_On_Session_Resume()
{
Expand Down
41 changes: 41 additions & 0 deletions go/internal/e2e/mcp_and_agents_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,47 @@ func TestMCPServersE2E(t *testing.T) {
session.Disconnect()
})

t.Run("accept MCP server config without args", func(t *testing.T) {
ctx.ConfigureForTest(t)

mcpServers := map[string]copilot.MCPServerConfig{
"test-server": copilot.MCPStdioServerConfig{
Command: "echo",
Tools: []string{"*"},
},
}

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
MCPServers: mcpServers,
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}

if session.SessionID == "" {
t.Error("Expected non-empty session ID")
}

_, err = session.Send(t.Context(), copilot.MessageOptions{
Prompt: "What is 2+2?",
})
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}

message, err := testharness.GetFinalAssistantMessage(t.Context(), session)
if err != nil {
t.Fatalf("Failed to get final message: %v", err)
}

if md, ok := message.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(md.Content, "4") {
t.Errorf("Expected message to contain '4', got: %v", message.Data)
}

session.Disconnect()
})

t.Run("accept MCP server config on resume", func(t *testing.T) {
ctx.ConfigureForTest(t)

Expand Down
2 changes: 1 addition & 1 deletion go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ type MCPStdioServerConfig struct {
Tools []string `json:"tools"`
Timeout int `json:"timeout,omitempty"`
Command string `json:"command"`
Args []string `json:"args"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
Cwd string `json:"cwd,omitempty"`
}
Expand Down
2 changes: 1 addition & 1 deletion nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ interface MCPServerConfigBase {
export interface MCPStdioServerConfig extends MCPServerConfigBase {
type?: "local" | "stdio";
command: string;
args: string[];
args?: string[];
/**
* Environment variables to pass to the server.
*/
Expand Down
24 changes: 24 additions & 0 deletions nodejs/test/e2e/mcp_and_agents.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ describe("MCP Servers and Custom Agents", async () => {
await session.disconnect();
});

it("should accept MCP server configuration without args", async () => {
const mcpServers: Record<string, MCPServerConfig> = {
"test-server": {
type: "local",
command: "echo",
tools: ["*"],
} as MCPStdioServerConfig,
};

const session = await client.createSession({
onPermissionRequest: approveAll,
mcpServers,
});

expect(session.sessionId).toBeDefined();

const message = await session.sendAndWait({
prompt: "What is 2+2?",
});
expect(message?.data.content).toContain("4");

await session.disconnect();
});

it("should accept MCP server configuration on session resume", async () => {
// Create a session first
const session1 = await client.createSession({ onPermissionRequest: approveAll });
Expand Down
2 changes: 1 addition & 1 deletion python/copilot/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ class MCPStdioServerConfig(TypedDict, total=False):
type: NotRequired[Literal["local", "stdio"]] # Server type
timeout: NotRequired[int] # Timeout in milliseconds
command: str # Command to run
args: list[str] # Command arguments
args: NotRequired[list[str]] # Command arguments
env: NotRequired[dict[str, str]] # Environment variables
cwd: NotRequired[str] # Working directory

Expand Down
21 changes: 21 additions & 0 deletions python/e2e/test_mcp_and_agents_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ async def test_should_accept_mcp_server_configuration_on_session_create(

await session.disconnect()

async def test_should_accept_mcp_server_configuration_without_args(self, ctx: E2ETestContext):
"""Test that MCP server configuration works without args field"""
mcp_servers: dict[str, MCPServerConfig] = {
"test-server": {
"command": "echo",
"tools": ["*"],
}
}

session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all, mcp_servers=mcp_servers
)

assert session.session_id is not None

message = await session.send_and_wait("What is 2+2?")
assert message is not None
assert "4" in message.data.content

await session.disconnect()

async def test_should_accept_mcp_server_configuration_on_session_resume(
self, ctx: E2ETestContext
):
Expand Down
2 changes: 1 addition & 1 deletion rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ pub struct McpStdioServerConfig {
/// Subprocess executable.
pub command: String,
/// Arguments to pass to the subprocess.
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
/// Environment variables to set on the subprocess. Values are passed
/// through literally to the child process.
Expand Down
42 changes: 42 additions & 0 deletions rust/tests/e2e/mcp_and_agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,48 @@ async fn accept_mcp_server_config_on_create() {
.await;
}

#[tokio::test]
async fn accept_mcp_server_config_without_args() {
with_e2e_context(
"mcp_and_agents",
"accept_mcp_server_config_without_args",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
let client = ctx.start_client().await;

let mcp_servers = HashMap::from([(
"test-server".to_string(),
McpServerConfig::Stdio(McpStdioServerConfig {
tools: vec!["*".to_string()],
command: "echo".to_string(),
..McpStdioServerConfig::default()
}),
)]);

let session = client
.create_session(
ctx.approve_all_session_config()
.with_mcp_servers(mcp_servers),
)
.await
.expect("create session");

let answer = session
.send_and_wait("What is 2+2?")
.await
.expect("send")
.expect("assistant message");
assert!(assistant_message_content(&answer).contains('4'));

session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
)
.await;
}

#[tokio::test]
async fn accept_mcp_server_config_on_resume() {
with_e2e_context(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
models:
- claude-sonnet-4.5
conversations:
- messages:
- role: system
content: ${system}
- role: user
content: What is 2+2?
- role: assistant
content: 2 + 2 = 4
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
models:
- claude-sonnet-4.5
conversations:
- messages:
- role: system
content: ${system}
- role: user
content: What is 2+2?
- role: assistant
content: 2 + 2 = 4
Loading