Skip to content

Commit e24d6a2

Browse files
committed
fix: harden mac mcp node discovery
1 parent b38a908 commit e24d6a2

File tree

5 files changed

+38
-5
lines changed

5 files changed

+38
-5
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.138",
5+
"version": "1.1.139",
66
"publisher": "local-dev",
77
"license": "MIT",
88
"icon": "images/icon.png",

src/cockpitWebviewSharedStyles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export function buildSchedulerWebviewSharedStyles(): string {
2+
return "";
23
}

src/mcpConfigManager.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ function shellEscapeSingleQuoted(value: string): string {
7171
return String(value).replace(/'/g, `'\\''`);
7272
}
7373

74+
function shellEscapeDoubleQuoted(value: string): string {
75+
return String(value)
76+
.replace(/\\/g, "\\\\")
77+
.replace(/\$/g, "\\$")
78+
.replace(/`/g, "\\`")
79+
.replace(/"/g, '\\"');
80+
}
81+
7482
function isPlainObject(value: unknown): value is Record<string, unknown> {
7583
return !!value && typeof value === "object" && !Array.isArray(value);
7684
}
@@ -179,6 +187,19 @@ function getPosixShellCandidates(runtime: NodeResolutionRuntime): string[] {
179187
);
180188
}
181189

190+
export function buildNodeShellExecutionCommand(launcherPath: string): string {
191+
const escapedLauncherPath = shellEscapeDoubleQuoted(launcherPath);
192+
return [
193+
'NODE_BIN="$(command -v node 2>/dev/null || true)"',
194+
'if [ -z "$NODE_BIN" ] && [ -n "$NVM_BIN" ] && [ -x "$NVM_BIN/node" ]; then NODE_BIN="$NVM_BIN/node"; fi',
195+
'if [ -z "$NODE_BIN" ] && [ -d "$HOME/.nvm/versions/node" ]; then NODE_BIN="$(find "$HOME/.nvm/versions/node" -type f -path "*/bin/node" 2>/dev/null | sort | tail -n 1)"; fi',
196+
'if [ -z "$NODE_BIN" ] && [ -d "$HOME/.asdf/installs/nodejs" ]; then NODE_BIN="$(find "$HOME/.asdf/installs/nodejs" -type f -path "*/bin/node" 2>/dev/null | sort | tail -n 1)"; fi',
197+
'if [ -z "$NODE_BIN" ]; then for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do if [ -x "$candidate" ]; then NODE_BIN="$candidate"; break; fi; done; fi',
198+
'if [ -z "$NODE_BIN" ]; then echo "Copilot Cockpit MCP launcher could not find node. Install Node.js or expose it in your shell startup." >&2; exit 127; fi',
199+
`exec "$NODE_BIN" "${escapedLauncherPath}"`,
200+
].join("; ");
201+
}
202+
182203
function resolvePosixShellCommand(runtime: NodeResolutionRuntime): NodeLaunchCommand | undefined {
183204
for (const shellPath of getPosixShellCandidates(runtime)) {
184205
if (runtime.fileExists(shellPath)) {
@@ -244,7 +265,7 @@ export function buildSchedulerMcpServerEntry(
244265
command: nodeLaunch.command,
245266
args:
246267
nodeLaunch.argsPrefix.length > 0
247-
? [...nodeLaunch.argsPrefix, `node '${shellEscapeSingleQuoted(launcherPath)}'`]
268+
? [...nodeLaunch.argsPrefix, buildNodeShellExecutionCommand(launcherPath)]
248269
: [launcherPath],
249270
};
250271
}
@@ -272,7 +293,7 @@ function buildSchedulerCodexServerTable(workspaceRoot: string): string {
272293
const nodeLaunch = resolveNodeLaunchCommand();
273294
const args =
274295
nodeLaunch.argsPrefix.length > 0
275-
? [...nodeLaunch.argsPrefix, `node '${shellEscapeSingleQuoted(launcherPath)}'`]
296+
? [...nodeLaunch.argsPrefix, buildNodeShellExecutionCommand(launcherPath)]
276297
: [launcherPath];
277298
return [
278299
"[mcp_servers.scheduler]",

src/test/suite/mcpConfigManager.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as os from "os";
44
import type { SchedulerMcpSetupState } from "../../mcpConfigManager";
55
import * as path from "path";
66
import {
7+
buildNodeShellExecutionCommand,
78
buildSchedulerMcpServerEntry,
89
getSchedulerMcpSetupState,
910
getWorkspaceMcpConfigPath,
@@ -267,4 +268,14 @@ suite("MCP Config Manager Tests", () => {
267268
assert.strictEqual(launch.command, "/opt/homebrew/bin/node");
268269
assert.deepStrictEqual(launch.argsPrefix, []);
269270
});
271+
272+
test("builds a shell command that resolves node before launching the MCP server", () => {
273+
const command = buildNodeShellExecutionCommand("/workspace/.vscode/copilot-cockpit-support/mcp/launcher.js");
274+
275+
assert.ok(command.includes('command -v node'));
276+
assert.ok(command.includes('NVM_BIN'));
277+
assert.ok(command.includes('.nvm/versions/node'));
278+
assert.ok(command.includes('.asdf/installs/nodejs'));
279+
assert.ok(command.includes('exec "$NODE_BIN" "/workspace/.vscode/copilot-cockpit-support/mcp/launcher.js"'));
280+
});
270281
});

0 commit comments

Comments
 (0)