#3788 · Cursor worktree MCP approvals remain bridge-only
Bug Priority: Medium Effort: Medium providers provider-acp
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: medium · Label: partial-repro
TL;DR
The report describes missing project MCP tools when a Cursor session moves into a fresh worktree. On trusted main, a synthetic test confirms that BB writes only its own bridge approval, even when project MCP configuration exists. Identical server configuration has different approval identifiers at different project roots. Both clean checkouts produced the same result. This establishes the BB-side approval gap, but does not verify the installed Cursor CLI’s tool discovery, flag handling, or OAuth behavior.
Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| BB seeds only its bridge approval. | Verified | Both fixture runs write a single bridge ID; direct calls for the synthetic project server return undefined. |
| Identical project servers have different approval IDs in worktrees. | Verified for BB’s implementation | The fixture creates a real Git checkout and linked worktree, and asserts different fingerprints and approval file paths. |
| Project MCP tools fail to load in a live Cursor ACP session. | Unverified end to end | No live Cursor session or external MCP service was started. |
| The global approval flag behaves differently in ACP and headless modes. | Unverified | External CLI behavior is outside this repository-level reproduction. |
| Remote OAuth state also depends on the project path. | Unverified | No authentication state was accessed. |
| The existing hash helper can cover every configured MCP server unchanged. | Not established | The helper accepts normalized stdio command/args/env configuration; remote URL/OAuth support requires separate validation. |
Environment
Trusted public get-bb/bb origin/main at the commit above; macOS (Darwin), Node v22.22.3, repository-pinned pnpm 9.15.0, Vitest 4.1.1. Two detached source worktrees, named base and verify, were created beneath one temporary investigation directory. The second checkout was clean before copying only the authored test. The system pnpm launcher was broken, so a temporary wrapper invoked the already available Corepack with the repository’s pinned pnpm. Both frozen installs completed. Full base build: 57 successful tasks.
No BB dev instance, provider process, external service, or listening port was used. Every test creates its own temporary Git repository, linked worktree, synthetic project config and Cursor data directory, then removes them. The synthetic server commands are data only and are never executed.
Minimal reproduction
- Save the complete reproduction test shown below as
issue-3788.test.ts. - Check out the trusted commit, install and build, copy the test, and run the commands below.
- The observation run should pass five tests. The expectation mode should fail because it asks for the missing project approval. That expectation probes the issue’s desired outcome; it does not endorse silently granting authorization.
git clone https://github.com/get-bb/bb.git bb-repro cd bb-repro git checkout --detach e6c615b87056ed2a3aedd46b4c01064f6fa132a9 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build cp /path/to/saved/issue-3788.test.ts packages/provider-bridge-acp/src/bridge/issue-3788.test.ts pnpm exec turbo run test --filter=@bb/provider-bridge-acp --force -- --run src/bridge/issue-3788.test.ts src/bridge/cursor-mcp-approval.test.ts BB_REPRO_EXPECT_PROJECT_APPROVAL=1 pnpm exec turbo run test --filter=@bb/provider-bridge-acp --env-mode=loose --force -- --run src/bridge/issue-3788.test.ts
Expected for approval continuity: the worktree approval array contains its project server ID. Actual: it contains only the bridge ID. Exact failure lines from the two executions:
@bb/provider-bridge-acp:test: AssertionError: expected [ 'bb-bridge-925895cb714b760c' ] to include 'fixture-service-9b3f2f9ed925312c' @bb/provider-bridge-acp:test: AssertionError: expected [ 'bb-bridge-99ded5d9e31e89b4' ] to include 'fixture-service-11e64b2c40790c9b'
Hashes change between runs because the synthetic roots are newly generated.
Complete reproduction test
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { approveCursorSessionMcpServer, buildCursorMcpApprovalIdentifier } from "./cursor-mcp-approval.js";
it("records the project approval gap in a fresh worktree", async () => {
const temporary = realpathSync(mkdtempSync(join(tmpdir(), "bb-3788-")));
try {
const checkout = join(temporary, "checkout");
const worktree = join(temporary, "worktree");
const cursorData = join(temporary, "cursor-data");
mkdirSync(checkout);
execFileSync("git", ["init", "--quiet", checkout]);
execFileSync("git", ["-C", checkout, "-c", "user.name=Reproduction", "-c", "user.email=repro@localhost", "-c", "commit.gpgsign=false", "commit", "--allow-empty", "--quiet", "-m", "Initialize synthetic fixture"]);
execFileSync("git", ["-C", checkout, "worktree", "add", "--quiet", "--detach", worktree]);
const project = { name: "fixture-service", command: "node", args: ["fixture.mjs"], env: [] };
const bridge = { name: "bb-bridge", command: "node", args: ["bridge-fixture.mjs"], env: [] };
for (const root of [checkout, worktree]) {
mkdirSync(join(root, ".cursor"));
writeFileSync(join(root, ".cursor", "mcp.json"), JSON.stringify({ mcpServers: { [project.name]: { command: project.command, args: project.args, env: {} } } }));
}
const env = { PATH: process.env.PATH, CURSOR_DATA_DIR: cursorData, HOME: temporary };
const original = await approveCursorSessionMcpServer({ agentCommand: "cursor-agent", config: bridge, cwd: checkout, env });
if (!original) throw new Error("Missing checkout bridge approval");
const originalProjectId = buildCursorMcpApprovalIdentifier({ config: project, projectRoot: checkout });
writeFileSync(original.path, JSON.stringify([original.approval, originalProjectId]));
const fresh = await approveCursorSessionMcpServer({ agentCommand: "cursor-agent", config: bridge, cwd: worktree, env });
if (!fresh) throw new Error("Missing worktree bridge approval");
const worktreeProjectId = buildCursorMcpApprovalIdentifier({ config: project, projectRoot: worktree });
expect(worktreeProjectId).not.toBe(originalProjectId);
expect(fresh.path).not.toBe(original.path);
const actual: unknown = JSON.parse(readFileSync(fresh.path, "utf8"));
expect(actual).toEqual([fresh.approval]);
expect(await approveCursorSessionMcpServer({ agentCommand: "cursor-agent", config: project, cwd: worktree, env })).toBeUndefined();
console.log("Observed: path-specific IDs differ; fresh worktree has bridge approval only; direct project approval is skipped.");
if (process.env.BB_REPRO_EXPECT_PROJECT_APPROVAL === "1") {
expect(actual).toContain(worktreeProjectId);
}
} finally {
rmSync(temporary, { recursive: true, force: true });
}
});
Root cause
BB seeds only its own MCP bridge approval; project server approvals are neither read nor transferred, and approval fingerprints include the resolved project root.
- Session MCP construction returns either no servers or a single BB bridge config.
- Session setup passes only the first session MCP config into the approval helper.
- Approval helper exits unless the command is Cursor and the config name is the BB bridge. It does not load project MCP configuration.
- Project slug determines the per-root data directory; fingerprinting and root resolution include the Git top-level path.
- Config normalization supports stdio command, args and env, rather than a general remote server definition.
Given a provider that requires matching project approvals, the fresh worktree will have no inherited approval for a project server. The conditional inference to unavailable tools is consistent with the report but was not tested against a real Cursor process.
Proposed fix and automation decision
Define an explicit consent policy for project MCP servers in derived environments, then implement and test that policy across source approval provenance, changed configuration, worktree paths, concurrent sessions, revocation and remote servers. A user-facing approval flow or a carefully scoped transfer of already approved configuration needs a product and permission review. Do not approve every repository-configured executable merely because it is present.
No production fix or PR was attempted: changing which project MCP servers are approved changes authorization/permission behavior and fails the rule’s simple-fix criteria. The verdict also stops short of a full live reproduction.
Verification
The same agent repeated the test in a second clean detached checkout at exactly the recorded SHA, with fresh synthetic data and no shared test fixture. The observation test plus the four existing approval tests passed in both checkouts (5/5 each). The expectation mode failed at the same missing-approval assertion in both checkouts (1/1 failed each), with Turbo cache bypassed. Commands are identical to those above; the second run uses the verify source directory. No report correction was needed. This is a repeated check by the same agent, not an independent review.
Related issues and PR metadata
A GitHub search returned #2471 (ACP session MCP configuration) and #2222 (Cursor MCP schema compatibility). They are contextual leads, not established duplicates. Both the open-PR search for #3788 and its cross-reference timeline contained no linked PRs at investigation time.
Appendix
Issue content was treated as untrusted claims. Its suggested implementation and shell examples were not executed. The test was authored from trusted repository code and uses synthetic values only. No user credentials or Cursor data were accessed. Relevant commands included fetching origin/main, reading issue properties and cross-reference metadata, creating two detached worktrees, frozen installs, the full base build, and the two test modes above. No production files changed.
Observation mode: both runs reported Test Files 2 passed (2); Tests 5 passed (5). Expectation mode: both runs reported Test Files 1 failed (1); Tests 1 failed (1) with the exact assertion lines above. Full base build: Tasks: 57 successful, 57 total; Cached: 4 cached, 57 total. Raw logs and the test are retained locally; this public repository permits self-contained HTML only.