From 9769d6be0fcaad9305d8456de5514ab620c9823b Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Thu, 23 Jul 2026 21:29:47 -0500 Subject: [PATCH] =?UTF-8?q?fix(installer):=20copilot-vscode=20global=20ent?= =?UTF-8?q?ry=20drops=20${workspaceFolder}=20=E2=80=94=20VS=20Code=20toast?= =?UTF-8?q?s=20an=20error=20in=20every=20folderless=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user-level mcp.json entry using ${workspaceFolder} makes VS Code refuse to start the server in ANY window without a folder open (loose files, welcome tab), toasting "Variable workspaceFolder can not be resolved" — recurring error-noise, hit live during validation. The pin was never needed for VS Code: unlike Cursor, VS Code documents stdio-server cwd as the workspace folder, and the codegraph server resolves its project via roots/list with a cwd fallback. Global entries are now variable-free (`serve --mcp`); local installs keep the absolute --path. This supersedes the "open a folder" install note from the previous commit, which is removed again. Co-Authored-By: Claude Fable 5 --- __tests__/installer-targets.test.ts | 20 +++++------ src/installer/targets/copilot-vscode.ts | 45 +++++++++++++------------ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 3ee37c7..9644929 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -1945,11 +1945,18 @@ describe('Installer targets — Copilot family', () => { expect(cfg.mcpServers).toBeUndefined(); }); - it('copilot-vscode: global install pins --path to ${workspaceFolder}', () => { + it('copilot-vscode: global install writes a variable-free entry — no --path, no ${workspaceFolder}', () => { + // VS Code refuses to start a user-level server whose entry uses + // ${workspaceFolder} in any window with no folder open, toasting + // "Variable workspaceFolder can not be resolved" (hit live). VS Code + // documents cwd = workspace folder for stdio servers, and the + // codegraph server resolves the project from roots/cwd — so the + // global entry must carry no --path and no variables at all. const t = getTarget('copilot-vscode')!; const result = t.install('global', { autoAllow: true }); const cfg = JSON.parse(fs.readFileSync(result.files[0].path, 'utf-8')); - expect(cfg.servers.codegraph.args).toEqual(['serve', '--mcp', '--path', '${workspaceFolder}']); + expect(cfg.servers.codegraph.args).toEqual(['serve', '--mcp']); + expect(JSON.stringify(cfg)).not.toContain('${'); }); it.runIf(process.platform === 'darwin')('copilot-vscode: global path is ~/Library/Application Support/Code/User/mcp.json on macOS', () => { @@ -2095,15 +2102,6 @@ describe('Installer targets — Copilot family', () => { expect(result.notes?.join(' ')).toMatch(/[Rr]estart VS Code/); }); - it('copilot-vscode: global install warns that ${workspaceFolder} needs an open folder; local does not', () => { - const t = getTarget('copilot-vscode')!; - // VS Code refuses to start a user-level server whose entry uses - // ${workspaceFolder} when no folder is open — surface that up front. - const globalNotes = t.install('global', { autoAllow: true }).notes?.join(' '); - expect(globalNotes).toMatch(/open a folder/i); - const localNotes = t.install('local', { autoAllow: true }).notes?.join(' '); - expect(localNotes).not.toMatch(/open a folder/i); - }); // ---- copilot-cli ---- diff --git a/src/installer/targets/copilot-vscode.ts b/src/installer/targets/copilot-vscode.ts index 985aced..6ac525e 100644 --- a/src/installer/targets/copilot-vscode.ts +++ b/src/installer/targets/copilot-vscode.ts @@ -16,17 +16,24 @@ * instructions, the single source of truth (#529). * - No permissions concept — `autoAllow` is silently ignored. * - * ## Why we inject `--path` (mirrors Cursor) + * ## Why `--path` only for local installs (NOT the Cursor pattern) * - * VS Code's docs don't specify the working directory stdio MCP servers - * are launched with, and (like Cursor) we can't rely on it being the - * workspace root. Rather than depend on undocumented cwd behavior we - * pin the project explicitly: + * Unlike Cursor, VS Code DOCUMENTS the launch cwd for stdio MCP + * servers: "Working directory for the server command. Defaults to the + * workspace folder when run in a workspace" (mcp-configuration + * reference). The codegraph server resolves its project via the MCP + * roots/list dance with a cwd fallback, so cwd alone is sufficient: * - * - `local` install: absolute path (known at install time). - * - `global` install: `${workspaceFolder}` — VS Code expands its - * standard variables inside mcp.json, giving per-workspace behavior - * from a single user-level config. + * - `local` install: absolute `--path` (known at install time) — + * deterministic, and free of variables. + * - `global` install: NO `--path`. Do not be tempted to pin it with + * `${workspaceFolder}`: VS Code refuses to start a user-level + * server whose entry uses that variable whenever a window has no + * folder open (loose files, welcome tab), surfacing an error toast + * "Variable workspaceFolder can not be resolved" in every such + * window — exactly the error-noise that teaches users to disable + * the server. With no `--path`, a folderless window still starts + * the server fine and it serves the "no project" guidance. * * ## JSONC * @@ -78,13 +85,16 @@ function mcpJsonPath(loc: Location): string { /** * Build the codegraph server entry for VS Code at the given location. - * Shared `{type, command, args}` shape plus the `--path` pin — see - * file header for why we don't trust VS Code's launch cwd. + * Local installs pin `--path`; global installs rely on VS Code's + * documented workspace-folder cwd — see file header for why the global + * entry must stay variable-free. */ function buildVscodeServerEntry(loc: Location): { type: string; command: string; args: string[] } { const base = getMcpServerConfig(); - const pathArg = loc === 'local' ? process.cwd() : '${workspaceFolder}'; - return { ...base, args: [...base.args, '--path', pathArg] }; + if (loc === 'local') { + return { ...base, args: [...base.args, '--path', process.cwd()] }; + } + return { ...base, args: [...base.args] }; } function readConfigText(file: string): string { @@ -127,16 +137,9 @@ class CopilotVscodeTarget implements AgentTarget { } install(loc: Location, _opts: InstallOptions): WriteResult { - const notes = ['Restart VS Code for MCP changes to take effect.']; - if (loc === 'global') { - // The global entry pins --path via ${workspaceFolder}; VS Code - // refuses to start it in a window with no folder open, with a - // cryptic "Variable workspaceFolder can not be resolved" toast. - notes.push('VS Code: the server starts per-workspace — open a folder (File → Open Folder) before starting it; a no-folder window reports "Variable workspaceFolder can not be resolved".'); - } return { files: [writeMcpEntry(loc)], - notes, + notes: ['Restart VS Code for MCP changes to take effect.'], }; }