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.'], }; }