Преглед на файлове

fix(installer): copilot-vscode global entry drops ${workspaceFolder} — VS Code toasts an error in every folderless window

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 <noreply@anthropic.com>
Colby McHenry преди 1 месец
родител
ревизия
9769d6be0f
променени са 2 файла, в които са добавени 33 реда и са изтрити 32 реда
  1. 9 11
      __tests__/installer-targets.test.ts
  2. 24 21
      src/installer/targets/copilot-vscode.ts

+ 9 - 11
__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 ----
 

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