Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/vscode-mcp-workspace-root.md
Original file line number Diff line number Diff line change
@@ -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).
4 changes: 2 additions & 2 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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`

Expand Down
8 changes: 8 additions & 0 deletions src/agents-init-mcp-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/agents-init-mcp-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
},
{
Expand Down
138 changes: 134 additions & 4 deletions src/agents-init-mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"],
Expand Down Expand Up @@ -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}"],
});
});
});
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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-"));
Expand Down Expand Up @@ -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<string, { args: string[] }>;
};
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 {
Expand Down
6 changes: 3 additions & 3 deletions src/agents-init-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AgentsInitMcpTargetDef, "format" | "workspaceRootArg">,
invocation: ResolvedCodemapInvocation,
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -168,6 +168,12 @@ describe("CLI unknown / invalid args", () => {
readFileSync(join(dir, ".cursor", "mcp.json"), "utf-8"),
) as { mcpServers: Record<string, { command: string }> };
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<string, { args: string[] }> };
expect(vscode.servers.codemap?.args).toContain("--root");
expect(vscode.servers.codemap?.args).toContain("${workspaceFolder}");
} finally {
rmSync(dir, { recursive: true, force: true });
}
Expand Down
Loading