diff --git a/.changeset/vscode-mcp-workspace-root.md b/.changeset/vscode-mcp-workspace-root.md new file mode 100644 index 00000000..86c7bfea --- /dev/null +++ b/.changeset/vscode-mcp-workspace-root.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/codemap": patch +--- + +`codemap agents init --mcp` now includes `--root ${workspaceFolder}` in the VS Code / Copilot MCP config (`.vscode/mcp.json`), same as Cursor. Re-run `codemap agents init --mcp` to upgrade an existing `.vscode/mcp.json` from older init output (or `--interactive` and select Copilot only). diff --git a/docs/agents.md b/docs/agents.md index e8c3c9e1..488d4c8a 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -142,7 +142,7 @@ Example: `CODEMAP_MCP_TOOLS=query,context,show codemap mcp --no-watch` | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Cursor | `.cursor/mcp.json` — PM-resolved spawn + `mcp --watch --root ${workspaceFolder}` (e.g. `npx codemap`, `pnpm exec codemap`, `yarn exec codemap`, `bunx codemap`) | | Claude Code | `.mcp.json` + `.claude/settings.json` — `permissions.allow` includes `mcp__codemap__*` | -| VS Code / Copilot | `.vscode/mcp.json` — `servers.codemap` with `type: stdio` | +| VS Code / Copilot | `.vscode/mcp.json` — `servers.codemap` with `type: stdio` + `mcp --watch --root ${workspaceFolder}` (PM-resolved spawn, same tail as Cursor) | | Continue | `.continue/mcpServers/codemap-mcp.json` (JSON `mcpServers`; also accepted from Cursor/Cline exports) | | Amazon Q Developer | **`.amazonq/default.json`** (IDE canonical) + **`.amazonq/mcp.json`** (legacy workspace; still read when global `useLegacyMcpJson` is true — AWS default). [AWS MCP IDE docs](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/mcp-ide.html) | | Gemini CLI | `.gemini/settings.json` — top-level `mcpServers.codemap` | @@ -153,7 +153,7 @@ With **`--mcp`** and no `--target` filter, all **project-local** rows above are Merge is idempotent: foreign MCP servers and existing settings keys are preserved; only the `codemap` server entry and permission pattern are upserted. **`command` / spawn args are resolved from the project** (when `@stainless-code/codemap` is listed in `package.json`, the local PM runner is used — e.g. `pnpm exec codemap`, `yarn exec codemap`, `bunx codemap`; otherwise PM dlx of `@stainless-code/codemap@latest` — e.g. `npx @stainless-code/codemap@latest`, `pnpm dlx @stainless-code/codemap@latest`, `yarn dlx @stainless-code/codemap@latest`; yarn classic may fall back to `npx` per `package-manager-detector`; Bun uses **`bunx`**, not `bun x`). Init logs the chosen invocation (`MCP CLI: …`). -**Side-effect-only re-runs:** When `.agents/` already exists, `codemap agents init --mcp`, **`--interactive`** target wiring (or any explicit integration targets), or `--git-hooks` still apply without `--force`. `codemap agents init --no-git-hooks --mcp` uninstalls hook blocks and writes MCP even when `.agents/` is absent. Template refresh still requires `--force`. Unparseable MCP JSON and invalid `mcpServers` / `servers` **shape** are **rejected** (fix the file manually — init never wipes or resets those maps, even with `--force`). Foreign MCP servers in a valid map are always preserved on merge. +**Side-effect-only re-runs:** When `.agents/` already exists, `codemap agents init --mcp`, **`--interactive`** target wiring (or any explicit integration targets), or `--git-hooks` still apply without `--force`. `codemap agents init --no-git-hooks --mcp` uninstalls hook blocks and writes MCP even when `.agents/` is absent. Template refresh still requires `--force`. Unparseable MCP JSON and invalid `mcpServers` / `servers` **shape** are **rejected** (fix the file manually — init never wipes or resets those maps, even with `--force`). Foreign MCP servers in a valid map are always preserved on merge. Re-runs upsert the codemap server entry in place (e.g. add `--root ${workspaceFolder}` on VS Code when an older `.vscode/mcp.json` omitted it). Cursor and VS Code get explicit `--root` because host docs do not guarantee stdio spawn `cwd` equals the workspace folder. ## Section assembler and `*.gen.md` diff --git a/src/agents-init-mcp-registry.test.ts b/src/agents-init-mcp-registry.test.ts index a70bb875..6d84c89f 100644 --- a/src/agents-init-mcp-registry.test.ts +++ b/src/agents-init-mcp-registry.test.ts @@ -10,6 +10,14 @@ import { } from "./agents-init-mcp-registry"; describe("AGENTS_INIT_MCP_REGISTRY", () => { + it("cursor and vscode inject workspace root via registry flag", () => { + expect(getAgentsInitMcpTargetDef("cursor").workspaceRootArg).toBe(true); + expect(getAgentsInitMcpTargetDef("vscode").workspaceRootArg).toBe(true); + expect( + getAgentsInitMcpTargetDef("claude-code").workspaceRootArg, + ).toBeUndefined(); + }); + it("has unique ids", () => { const ids = AGENTS_INIT_MCP_REGISTRY.map((def) => def.id); expect(new Set(ids).size).toBe(ids.length); diff --git a/src/agents-init-mcp-registry.ts b/src/agents-init-mcp-registry.ts index 86de7697..a5ae6f6b 100644 --- a/src/agents-init-mcp-registry.ts +++ b/src/agents-init-mcp-registry.ts @@ -31,7 +31,7 @@ export interface AgentsInitMcpTargetDef { readonly docsUrl: string; /** Written by default when `--mcp` has no integration filter. */ readonly defaultOnMcp: boolean; - /** Cursor: inject `--root ${workspaceFolder}`. */ + /** Cursor / VS Code: inject `--root ${workspaceFolder}`. */ readonly workspaceRootArg?: boolean | undefined; /** Matching `agents init --interactive` integration pick, when any. */ readonly integrationTarget?: AgentsInitTarget | undefined; @@ -77,6 +77,7 @@ export const AGENTS_INIT_MCP_REGISTRY: readonly AgentsInitMcpTargetDef[] = docsUrl: "https://code.visualstudio.com/docs/copilot/reference/mcp-configuration", defaultOnMcp: true, + workspaceRootArg: true, integrationTarget: "copilot", }, { diff --git a/src/agents-init-mcp.test.ts b/src/agents-init-mcp.test.ts index b82af33c..dca3b49a 100644 --- a/src/agents-init-mcp.test.ts +++ b/src/agents-init-mcp.test.ts @@ -64,7 +64,7 @@ function seedBunInstalledCodemapProject(dir: string): void { } describe("buildCodemapMcpSpawn", () => { - it("includes workspace root for Cursor", () => { + it("includes workspace root when includeWorkspaceRoot is true", () => { expect(buildCodemapMcpSpawn(NPM_LOCAL_INVOCATION, true)).toEqual({ command: "npx", args: ["codemap", "mcp", "--watch", "--root", "${workspaceFolder}"], @@ -179,13 +179,13 @@ describe("mergeCodemapVsCodeServer", () => { other: { command: "npx", args: ["-y", "other"] }, }, }, - buildCodemapMcpSpawn(NPM_LOCAL_INVOCATION, false), + buildCodemapMcpSpawn(NPM_LOCAL_INVOCATION, true), ); expect(merged.servers?.other?.command).toBe("npx"); expect(merged.servers?.[CODEMAP_MCP_SERVER_KEY]).toEqual({ type: "stdio", command: "npx", - args: ["codemap", "mcp", "--watch"], + args: ["codemap", "mcp", "--watch", "--root", "${workspaceFolder}"], }); }); }); @@ -300,7 +300,13 @@ describe("applyAgentsInitMcp", () => { }; expect(vscode.servers[CODEMAP_MCP_SERVER_KEY]?.type).toBe("stdio"); expect(vscode.servers[CODEMAP_MCP_SERVER_KEY]?.command).toBe("npx"); - expect(vscode.servers[CODEMAP_MCP_SERVER_KEY]?.args?.[0]).toBe("codemap"); + expect(vscode.servers[CODEMAP_MCP_SERVER_KEY]?.args).toEqual([ + "codemap", + "mcp", + "--watch", + "--root", + "${workspaceFolder}", + ]); const amazonDefault = JSON.parse( readFileSync(join(dir, ".amazonq", "default.json"), "utf-8"), @@ -376,6 +382,31 @@ describe("applyAgentsInitMcp", () => { } }); + it("writes project .vscode/mcp.json when vscode target selected", async () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-vs-")); + try { + seedInstalledCodemapProject(dir); + await applyAgentsInitMcp({ projectRoot: dir, targets: ["vscode"] }); + expect(existsSync(join(dir, ".vscode", "mcp.json"))).toBe(true); + expect(existsSync(join(dir, ".cursor", "mcp.json"))).toBe(false); + const vscode = JSON.parse( + readFileSync(join(dir, ".vscode", "mcp.json"), "utf-8"), + ) as { + servers: Record< + string, + { type: string; command: string; args: string[] } + >; + }; + expect(vscode.servers[CODEMAP_MCP_SERVER_KEY]).toEqual({ + type: "stdio", + command: "npx", + args: ["codemap", "mcp", "--watch", "--root", "${workspaceFolder}"], + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("writes project .cline/mcp.json when cline target selected", async () => { const dir = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-cl-")); const fakeHome = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-cl-home-")); @@ -447,6 +478,105 @@ describe("applyAgentsInitMcp", () => { } }); + it("merges into existing .vscode/mcp.json without clobbering other servers", async () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-vs-merge-")); + try { + seedInstalledCodemapProject(dir); + mkdirSync(join(dir, ".vscode"), { recursive: true }); + writeFileSync( + join(dir, ".vscode", "mcp.json"), + `${JSON.stringify( + { + servers: { + foreign: { type: "stdio", command: "node", args: ["server.js"] }, + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + await applyAgentsInitMcp({ projectRoot: dir, targets: ["vscode"] }); + const parsed = JSON.parse( + readFileSync(join(dir, ".vscode", "mcp.json"), "utf-8"), + ) as { + servers: Record< + string, + { type?: string; command: string; args: string[] } + >; + }; + expect(parsed.servers.foreign).toEqual({ + type: "stdio", + command: "node", + args: ["server.js"], + }); + expect(parsed.servers[CODEMAP_MCP_SERVER_KEY]?.args).toEqual([ + "codemap", + "mcp", + "--watch", + "--root", + "${workspaceFolder}", + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("is idempotent on vscode re-run", async () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-vs-idem-")); + try { + seedInstalledCodemapProject(dir); + await applyAgentsInitMcp({ projectRoot: dir, targets: ["vscode"] }); + const before = readFileSync(join(dir, ".vscode", "mcp.json"), "utf-8"); + await applyAgentsInitMcp({ projectRoot: dir, targets: ["vscode"] }); + expect(readFileSync(join(dir, ".vscode", "mcp.json"), "utf-8")).toBe( + before, + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it("upgrades stale vscode codemap entry without --root on re-run", async () => { + const dir = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-vs-upgrade-")); + try { + seedInstalledCodemapProject(dir); + mkdirSync(join(dir, ".vscode"), { recursive: true }); + writeFileSync( + join(dir, ".vscode", "mcp.json"), + `${JSON.stringify( + { + servers: { + [CODEMAP_MCP_SERVER_KEY]: { + type: "stdio", + command: "npx", + args: ["codemap", "mcp", "--watch"], + }, + }, + }, + null, + 2, + )}\n`, + "utf-8", + ); + await applyAgentsInitMcp({ projectRoot: dir, targets: ["vscode"] }); + const parsed = JSON.parse( + readFileSync(join(dir, ".vscode", "mcp.json"), "utf-8"), + ) as { + servers: Record; + }; + expect(parsed.servers[CODEMAP_MCP_SERVER_KEY]?.args).toEqual([ + "codemap", + "mcp", + "--watch", + "--root", + "${workspaceFolder}", + ]); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("merges into existing .cursor/mcp.json without clobbering other servers", async () => { const dir = mkdtempSync(join(tmpdir(), "codemap-agents-mcp-")); try { diff --git a/src/agents-init-mcp.ts b/src/agents-init-mcp.ts index ee5b46e8..2847a6e3 100644 --- a/src/agents-init-mcp.ts +++ b/src/agents-init-mcp.ts @@ -52,7 +52,7 @@ export interface ClaudeSettingsFile { }; } -/** Host-specific codemap MCP entry (Cursor root arg, Amazon Q IDE transport fields, …). */ +/** Host-specific codemap MCP entry (workspace-root arg, Amazon Q IDE transport fields, …). */ export function buildMcpServerEntryForDef( def: Pick, invocation: ResolvedCodemapInvocation, @@ -434,8 +434,8 @@ export interface ApplyAgentsInitMcpOptions { } /** - * Write MCP config for selected integrations. Cursor uses - * `${workspaceFolder}` root injection; most other clients rely on workspace cwd. + * Write MCP config for selected integrations. Cursor and VS Code get + * `${workspaceFolder}` root injection; other cwd-based clients omit `--root`. */ export async function applyAgentsInitMcp( opts: ApplyAgentsInitMcpOptions, diff --git a/src/cli.test.ts b/src/cli.test.ts index ee41d27b..0b3443f1 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -150,7 +150,7 @@ describe("CLI unknown / invalid args", () => { } }); - test("agents init --force --mcp writes .cursor/mcp.json under --root", async () => { + test("agents init --force --mcp writes project MCP configs under --root", async () => { const dir = mkdtempSync(join(tmpdir(), "codemap-cli-agents-mcp-")); try { const { exitCode, err } = await runCli([ @@ -168,6 +168,12 @@ describe("CLI unknown / invalid args", () => { readFileSync(join(dir, ".cursor", "mcp.json"), "utf-8"), ) as { mcpServers: Record }; expect(parsed.mcpServers.codemap?.command).toBe("npx"); + expect(existsSync(join(dir, ".vscode", "mcp.json"))).toBe(true); + const vscode = JSON.parse( + readFileSync(join(dir, ".vscode", "mcp.json"), "utf-8"), + ) as { servers: Record }; + expect(vscode.servers.codemap?.args).toContain("--root"); + expect(vscode.servers.codemap?.args).toContain("${workspaceFolder}"); } finally { rmSync(dir, { recursive: true, force: true }); }