Skip to content

Commit 6003273

Browse files
Fix MCP env vars: send envValueMode direct across all SDKs (#484)
* Use MCP direct env var mode in Node SDK * Equivalent fixes for other languages * Update @github/copilot to 0.0.411-1 and regenerate SDKs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Python lint errors (line too long) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add normalizer to strip <reminder> tags from user messages The CLI now injects dynamic <reminder> tags with SQL table state into user messages. These tags vary based on runtime state and should not affect snapshot matching. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent df23ff4 commit 6003273

File tree

19 files changed

+1490
-38
lines changed

19 files changed

+1490
-38
lines changed

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
383383
config?.WorkingDirectory,
384384
config?.Streaming == true ? true : null,
385385
config?.McpServers,
386+
"direct",
386387
config?.CustomAgents,
387388
config?.ConfigDir,
388389
config?.SkillDirectories,
@@ -468,6 +469,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
468469
config?.DisableResume == true ? true : null,
469470
config?.Streaming == true ? true : null,
470471
config?.McpServers,
472+
"direct",
471473
config?.CustomAgents,
472474
config?.SkillDirectories,
473475
config?.DisabledSkills,
@@ -1385,6 +1387,7 @@ internal record CreateSessionRequest(
13851387
string? WorkingDirectory,
13861388
bool? Streaming,
13871389
Dictionary<string, object>? McpServers,
1390+
string? EnvValueMode,
13881391
List<CustomAgentConfig>? CustomAgents,
13891392
string? ConfigDir,
13901393
List<string>? SkillDirectories,
@@ -1421,6 +1424,7 @@ internal record ResumeSessionRequest(
14211424
bool? DisableResume,
14221425
bool? Streaming,
14231426
Dictionary<string, object>? McpServers,
1427+
string? EnvValueMode,
14241428
List<CustomAgentConfig>? CustomAgents,
14251429
List<string>? SkillDirectories,
14261430
List<string>? DisabledSkills,

dotnet/test/Harness/E2ETestContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public IReadOnlyDictionary<string, string> GetEnvironment()
9292
public CopilotClient CreateClient() => new(new CopilotClientOptions
9393
{
9494
Cwd = WorkDir,
95+
CliPath = GetCliPath(_repoRoot),
9596
Environment = GetEnvironment(),
9697
GithubToken = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) ? "fake-token-for-e2e-tests" : null,
9798
});

dotnet/test/McpAndAgentsTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,41 @@ public async Task Should_Handle_Multiple_Custom_Agents()
260260
await session.DisposeAsync();
261261
}
262262

263+
[Fact]
264+
public async Task Should_Pass_Literal_Env_Values_To_Mcp_Server_Subprocess()
265+
{
266+
var testHarnessDir = FindTestHarnessDir();
267+
var mcpServers = new Dictionary<string, object>
268+
{
269+
["env-echo"] = new McpLocalServerConfig
270+
{
271+
Type = "local",
272+
Command = "node",
273+
Args = [Path.Combine(testHarnessDir, "test-mcp-server.mjs")],
274+
Env = new Dictionary<string, string> { ["TEST_SECRET"] = "hunter2" },
275+
Cwd = testHarnessDir,
276+
Tools = ["*"]
277+
}
278+
};
279+
280+
var session = await Client.CreateSessionAsync(new SessionConfig
281+
{
282+
McpServers = mcpServers
283+
});
284+
285+
Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);
286+
287+
var message = await session.SendAndWaitAsync(new MessageOptions
288+
{
289+
Prompt = "Use the env-echo/get_env tool to read the TEST_SECRET environment variable. Reply with just the value, nothing else."
290+
});
291+
292+
Assert.NotNull(message);
293+
Assert.Contains("hunter2", message!.Data.Content);
294+
295+
await session.DisposeAsync();
296+
}
297+
263298
[Fact]
264299
public async Task Should_Accept_Both_MCP_Servers_And_Custom_Agents()
265300
{
@@ -301,4 +336,17 @@ public async Task Should_Accept_Both_MCP_Servers_And_Custom_Agents()
301336

302337
await session.DisposeAsync();
303338
}
339+
340+
private static string FindTestHarnessDir()
341+
{
342+
var dir = new DirectoryInfo(AppContext.BaseDirectory);
343+
while (dir != null)
344+
{
345+
var candidate = Path.Combine(dir.FullName, "test", "harness", "test-mcp-server.mjs");
346+
if (File.Exists(candidate))
347+
return Path.GetDirectoryName(candidate)!;
348+
dir = dir.Parent;
349+
}
350+
throw new InvalidOperationException("Could not find test/harness/test-mcp-server.mjs");
351+
}
304352
}

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
464464
req.Provider = config.Provider
465465
req.WorkingDirectory = config.WorkingDirectory
466466
req.MCPServers = config.MCPServers
467+
req.EnvValueMode = "direct"
467468
req.CustomAgents = config.CustomAgents
468469
req.SkillDirectories = config.SkillDirectories
469470
req.DisabledSkills = config.DisabledSkills
@@ -581,6 +582,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
581582
req.DisableResume = Bool(true)
582583
}
583584
req.MCPServers = config.MCPServers
585+
req.EnvValueMode = "direct"
584586
req.CustomAgents = config.CustomAgents
585587
req.SkillDirectories = config.SkillDirectories
586588
req.DisabledSkills = config.DisabledSkills

go/internal/e2e/mcp_and_agents_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package e2e
22

33
import (
4+
"path/filepath"
45
"strings"
56
"testing"
67

@@ -104,6 +105,51 @@ func TestMCPServers(t *testing.T) {
104105
session2.Destroy()
105106
})
106107

108+
t.Run("should pass literal env values to MCP server subprocess", func(t *testing.T) {
109+
ctx.ConfigureForTest(t)
110+
111+
mcpServerPath, err := filepath.Abs("../../../test/harness/test-mcp-server.mjs")
112+
if err != nil {
113+
t.Fatalf("Failed to resolve test-mcp-server path: %v", err)
114+
}
115+
mcpServerDir := filepath.Dir(mcpServerPath)
116+
117+
mcpServers := map[string]copilot.MCPServerConfig{
118+
"env-echo": {
119+
"type": "local",
120+
"command": "node",
121+
"args": []string{mcpServerPath},
122+
"tools": []string{"*"},
123+
"env": map[string]string{"TEST_SECRET": "hunter2"},
124+
"cwd": mcpServerDir,
125+
},
126+
}
127+
128+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
129+
MCPServers: mcpServers,
130+
})
131+
if err != nil {
132+
t.Fatalf("Failed to create session: %v", err)
133+
}
134+
135+
if session.SessionID == "" {
136+
t.Error("Expected non-empty session ID")
137+
}
138+
139+
message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
140+
Prompt: "Use the env-echo/get_env tool to read the TEST_SECRET environment variable. Reply with just the value, nothing else.",
141+
})
142+
if err != nil {
143+
t.Fatalf("Failed to send message: %v", err)
144+
}
145+
146+
if message.Data.Content == nil || !strings.Contains(*message.Data.Content, "hunter2") {
147+
t.Errorf("Expected message to contain 'hunter2', got: %v", message.Data.Content)
148+
}
149+
150+
session.Destroy()
151+
})
152+
107153
t.Run("handle multiple MCP servers", func(t *testing.T) {
108154
ctx.ConfigureForTest(t)
109155

go/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ type createSessionRequest struct {
638638
WorkingDirectory string `json:"workingDirectory,omitempty"`
639639
Streaming *bool `json:"streaming,omitempty"`
640640
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
641+
EnvValueMode string `json:"envValueMode,omitempty"`
641642
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
642643
ConfigDir string `json:"configDir,omitempty"`
643644
SkillDirectories []string `json:"skillDirectories,omitempty"`
@@ -669,6 +670,7 @@ type resumeSessionRequest struct {
669670
DisableResume *bool `json:"disableResume,omitempty"`
670671
Streaming *bool `json:"streaming,omitempty"`
671672
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
673+
EnvValueMode string `json:"envValueMode,omitempty"`
672674
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
673675
SkillDirectories []string `json:"skillDirectories,omitempty"`
674676
DisabledSkills []string `json:"disabledSkills,omitempty"`

nodejs/package-lock.json

Lines changed: 28 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"author": "GitHub",
4141
"license": "MIT",
4242
"dependencies": {
43-
"@github/copilot": "^0.0.411-0",
43+
"@github/copilot": "^0.0.411-1",
4444
"vscode-jsonrpc": "^8.2.1",
4545
"zod": "^4.3.6"
4646
},

nodejs/src/client.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ import type {
3434
ModelInfo,
3535
ResumeSessionConfig,
3636
SessionConfig,
37+
SessionContext,
3738
SessionEvent,
3839
SessionLifecycleEvent,
3940
SessionLifecycleEventType,
4041
SessionLifecycleHandler,
41-
SessionContext,
4242
SessionListFilter,
4343
SessionMetadata,
4444
Tool,
@@ -529,6 +529,7 @@ export class CopilotClient {
529529
workingDirectory: config.workingDirectory,
530530
streaming: config.streaming,
531531
mcpServers: config.mcpServers,
532+
envValueMode: "direct",
532533
customAgents: config.customAgents,
533534
configDir: config.configDir,
534535
skillDirectories: config.skillDirectories,
@@ -611,6 +612,7 @@ export class CopilotClient {
611612
configDir: config.configDir,
612613
streaming: config.streaming,
613614
mcpServers: config.mcpServers,
615+
envValueMode: "direct",
614616
customAgents: config.customAgents,
615617
skillDirectories: config.skillDirectories,
616618
disabledSkills: config.disabledSkills,

nodejs/test/e2e/harness/sdkTestContext.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const SNAPSHOTS_DIR = resolve(__dirname, "../../../../test/snapshots");
1919

2020
export async function createSdkTestContext({
2121
logLevel,
22-
}: { logLevel?: "error" | "none" | "warning" | "info" | "debug" | "all" } = {}) {
22+
}: { logLevel?: "error" | "none" | "warning" | "info" | "debug" | "all"; cliPath?: string } = {}) {
2323
const homeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-config-")));
2424
const workDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-work-")));
2525

@@ -40,6 +40,7 @@ export async function createSdkTestContext({
4040
cwd: workDir,
4141
env,
4242
logLevel: logLevel || "error",
43+
cliPath: process.env.COPILOT_CLI_PATH,
4344
// Use fake token in CI to allow cached responses without real auth
4445
githubToken: process.env.CI === "true" ? "fake-token-for-e2e-tests" : undefined,
4546
});

0 commit comments

Comments
 (0)