Skip to content

Commit 96ef6f3

Browse files
committed
fix: use vscode runtime for mcp launcher
1 parent f6d7512 commit 96ef6f3

File tree

4 files changed

+48
-13
lines changed

4 files changed

+48
-13
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "copilot-cockpit",
33
"displayName": "Copilot Cockpit",
44
"description": "VS Code-native orchestration for AI agents with controlled workflows, scheduling, and human-in-the-loop execution",
5-
"version": "1.1.140",
5+
"version": "1.1.141",
66
"publisher": "local-dev",
77
"license": "MIT",
88
"icon": "images/icon.png",

src/mcpConfigManager.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ export type McpServerEntry = {
66
type: "stdio";
77
command: string;
88
args: string[];
9+
env?: Record<string, string>;
910
};
1011

1112
type NodeLaunchCommand = {
1213
command: string;
1314
argsPrefix: string[];
15+
env?: Record<string, string>;
1416
};
1517

1618
type NodeResolutionRuntime = {
@@ -228,6 +230,16 @@ export function resolveNodeLaunchCommand(
228230
};
229231
}
230232

233+
if (runtime.fileExists(runtime.execPath)) {
234+
return {
235+
command: runtime.execPath,
236+
argsPrefix: [],
237+
env: {
238+
ELECTRON_RUN_AS_NODE: "1",
239+
},
240+
};
241+
}
242+
231243
const pathResolvedNode = resolveNodeFromPath(runtime);
232244
if (pathResolvedNode) {
233245
return {
@@ -266,6 +278,7 @@ export function buildSchedulerMcpServerEntry(
266278
return {
267279
type: "stdio",
268280
command: nodeLaunch.command,
281+
...(nodeLaunch.env ? { env: nodeLaunch.env } : {}),
269282
args:
270283
nodeLaunch.argsPrefix.length > 0
271284
? [...nodeLaunch.argsPrefix, buildNodeShellExecutionCommand(launcherPath)]
@@ -302,6 +315,13 @@ function buildSchedulerCodexServerTable(workspaceRoot: string): string {
302315
"[mcp_servers.scheduler]",
303316
`command = "${escapeTomlString(nodeLaunch.command)}"`,
304317
`args = [${args.map((value) => `"${escapeTomlString(value)}"`).join(", ")}]`,
318+
...(nodeLaunch.env
319+
? [
320+
`env = { ${Object.entries(nodeLaunch.env)
321+
.map(([key, value]) => `${key} = "${escapeTomlString(value)}"`)
322+
.join(", ")} }`,
323+
]
324+
: []),
305325
"enabled = true",
306326
"startup_timeout_sec = 30",
307327
].join("\n");
@@ -442,19 +462,28 @@ export function getSchedulerMcpSetupState(
442462
const actualType = scheduler.type;
443463
const actualCommand = scheduler.command;
444464
const actualArgs = Array.isArray(scheduler.args) ? scheduler.args : undefined;
465+
const actualEnv = isPlainObject(scheduler.env)
466+
? Object.fromEntries(
467+
Object.entries(scheduler.env)
468+
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
469+
)
470+
: undefined;
471+
const expectedEnv = expected.env;
445472

446473
if (
447474
actualType === expected.type &&
448475
actualCommand === expected.command &&
449476
actualArgs &&
450477
actualArgs.length === expected.args.length &&
451-
actualArgs.every((value, index) => value === expected.args[index])
478+
actualArgs.every((value, index) => value === expected.args[index]) &&
479+
JSON.stringify(actualEnv ?? {}) === JSON.stringify(expectedEnv ?? {})
452480
) {
453-
if (!fs.existsSync(expected.args[0])) {
481+
const launcherPath = getWorkspaceMcpLauncherPath(workspaceRoot);
482+
if (!fs.existsSync(launcherPath)) {
454483
return {
455484
status: "stale",
456485
configPath,
457-
reason: `Configured server path does not exist: ${expected.args[0]}`,
486+
reason: `Configured launcher path does not exist: ${launcherPath}`,
458487
};
459488
}
460489

@@ -478,6 +507,7 @@ export function getSchedulerMcpSetupState(
478507
type: actualType,
479508
command: actualCommand,
480509
args: actualArgs,
510+
env: actualEnv,
481511
})} instead of ${JSON.stringify(expected)}.`,
482512
};
483513
} catch (error) {

src/test/suite/mcpConfigManager.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ suite("MCP Config Manager Tests", () => {
4242
assert.strictEqual(fs.existsSync(configPath), true);
4343

4444
const saved = JSON.parse(fs.readFileSync(configPath, "utf8")) as {
45-
servers?: Record<string, { command?: string; args?: string[] }>;
45+
servers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }>;
4646
};
4747
assert.strictEqual(saved.servers?.scheduler?.command, expectedEntry.command);
48+
assert.deepStrictEqual(saved.servers?.scheduler?.env ?? {}, expectedEntry.env ?? {});
4849
assert.strictEqual(
4950
fs.existsSync(getWorkspaceMcpLauncherPath(workspaceRoot)),
5051
true,
@@ -101,12 +102,13 @@ suite("MCP Config Manager Tests", () => {
101102
const expectedEntry = buildSchedulerMcpServerEntry(workspaceRoot);
102103

103104
const saved = JSON.parse(fs.readFileSync(configPath, "utf8")) as {
104-
servers?: Record<string, { command?: string; args?: string[] }>;
105+
servers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }>;
105106
metadata?: { owner?: string };
106107
};
107108
assert.strictEqual(saved.metadata?.owner, "tests");
108109
assert.strictEqual(saved.servers?.existing?.args?.[0], "existing-server.js");
109110
assert.strictEqual(saved.servers?.scheduler?.command, expectedEntry.command);
111+
assert.deepStrictEqual(saved.servers?.scheduler?.env ?? {}, expectedEntry.env ?? {});
110112
assert.strictEqual(
111113
saved.servers?.scheduler?.args?.[0],
112114
getWorkspaceMcpLauncherPath(workspaceRoot),
@@ -225,9 +227,10 @@ suite("MCP Config Manager Tests", () => {
225227
const expectedEntry = buildSchedulerMcpServerEntry(workspaceRoot);
226228

227229
const repaired = JSON.parse(fs.readFileSync(configPath, "utf8")) as {
228-
servers?: Record<string, { command?: string; args?: string[] }>;
230+
servers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }>;
229231
};
230232
assert.strictEqual(repaired.servers?.scheduler?.command, expectedEntry.command);
233+
assert.deepStrictEqual(repaired.servers?.scheduler?.env ?? {}, expectedEntry.env ?? {});
231234
assert.strictEqual(
232235
repaired.servers?.scheduler?.args?.[0],
233236
getWorkspaceMcpLauncherPath(workspaceRoot),
@@ -240,19 +243,20 @@ suite("MCP Config Manager Tests", () => {
240243
}
241244
});
242245

243-
test("falls back to the user's interactive login shell on macOS when node is not directly discoverable", () => {
246+
test("prefers the VS Code runtime with ELECTRON_RUN_AS_NODE when the extension host is not plain node", () => {
244247
const launch = resolveNodeLaunchCommand({
245248
platform: "darwin",
246249
execPath: "/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper",
247250
env: {
248251
PATH: "",
249252
SHELL: "/bin/zsh",
250253
},
251-
fileExists: (filePath: string) => filePath === "/bin/zsh",
254+
fileExists: (filePath: string) => filePath === "/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper",
252255
});
253256

254-
assert.strictEqual(launch.command, "/bin/zsh");
255-
assert.deepStrictEqual(launch.argsPrefix, ["-ilc"]);
257+
assert.strictEqual(launch.command, "/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper");
258+
assert.deepStrictEqual(launch.argsPrefix, []);
259+
assert.deepStrictEqual(launch.env ?? {}, { ELECTRON_RUN_AS_NODE: "1" });
256260
});
257261

258262
test("prefers an absolute node executable when one is available on macOS", () => {
@@ -267,6 +271,7 @@ suite("MCP Config Manager Tests", () => {
267271

268272
assert.strictEqual(launch.command, "/opt/homebrew/bin/node");
269273
assert.deepStrictEqual(launch.argsPrefix, []);
274+
assert.strictEqual(launch.env, undefined);
270275
});
271276

272277
test("builds a shell command that resolves node before launching the MCP server", () => {

0 commit comments

Comments
 (0)