@bridge_gpt/mcp-server 0.2.39 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +10 -10
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/claude-user-config-doctor.js +42 -11
  5. package/build/cli-release.js +2 -1
  6. package/build/commands.generated.js +4 -4
  7. package/build/conduct-epic/bridge-client.js +354 -113
  8. package/build/conduct-epic/checkpoint-store.js +75 -2
  9. package/build/conduct-epic/cli.js +795 -109
  10. package/build/conduct-epic/cut-protocol.js +327 -0
  11. package/build/conduct-epic/pr-state.js +113 -24
  12. package/build/conduct-epic/spawn.js +14 -2
  13. package/build/conductor/bridge-api-client.js +27 -1
  14. package/build/conductor/cli.js +46 -1
  15. package/build/conductor/doctor.js +101 -16
  16. package/build/conductor/epic-reconcile.js +72 -19
  17. package/build/conductor/epic-runtime.js +15 -3
  18. package/build/conductor/errors.js +47 -0
  19. package/build/conductor/git-hooks.js +205 -11
  20. package/build/conductor/install-doctor.js +230 -1
  21. package/build/conductor/local-merge.js +130 -28
  22. package/build/conductor/tools.js +32 -3
  23. package/build/conductor/worker-ledger-cli.js +27 -1
  24. package/build/conductor-bin.js +15 -15
  25. package/build/credentials-cli.js +3 -2
  26. package/build/doctor.js +107 -41
  27. package/build/executor/cli.js +48 -1
  28. package/build/executor/env.js +21 -0
  29. package/build/executor/index-scope.js +39 -0
  30. package/build/executor/job-log-registry.js +69 -0
  31. package/build/executor/job-runner.js +148 -26
  32. package/build/executor/live-worker-registry.js +83 -0
  33. package/build/executor/observation.js +167 -6
  34. package/build/executor/platform.js +147 -3
  35. package/build/executor/process.js +58 -14
  36. package/build/executor/runner.js +235 -48
  37. package/build/executor/test-clock.js +3 -2
  38. package/build/index-scope-contract.js +96 -0
  39. package/build/index.js +153 -204
  40. package/build/init.js +83 -22
  41. package/build/install-bridge-conductor.js +323 -14
  42. package/build/install-bridge.js +202 -38
  43. package/build/install-doctor.js +23 -9
  44. package/build/install-reexec.js +2 -1
  45. package/build/launcher-config-inspection.js +83 -22
  46. package/build/mcp-host-config.js +331 -67
  47. package/build/mcp-host-targets.js +45 -21
  48. package/build/mcp-identity.js +92 -0
  49. package/build/mcp-install-state.js +94 -1
  50. package/build/mcp-invoke.js +2 -1
  51. package/build/mcp-provisioning.js +45 -12
  52. package/build/mcp-registration-doctor.js +35 -13
  53. package/build/mcp-server-invocation.js +4 -2
  54. package/build/merge-pull-request.js +208 -9
  55. package/build/pipelines.generated.js +3 -3
  56. package/build/plane/defaults.js +4 -1
  57. package/build/plane/preflight.js +81 -10
  58. package/build/plane/test-fakes.js +9 -1
  59. package/build/readme.generated.js +1 -1
  60. package/build/regression-check.js +3 -2
  61. package/build/review-tickets.js +8 -7
  62. package/build/run-unit-tests-launcher.js +74 -1
  63. package/build/schedule-run.js +3 -2
  64. package/build/setup-epic.js +453 -78
  65. package/build/sfcc/tool-wrapper.js +15 -0
  66. package/build/start-tickets-prereqs.js +11 -6
  67. package/build/start-tickets.js +91 -85
  68. package/build/update-check.js +3 -2
  69. package/build/upgrade-advice.js +2 -1
  70. package/build/upgrade-cli.js +50 -18
  71. package/build/version.generated.js +1 -1
  72. package/docs/CONDUCTOR.md +22 -0
  73. package/docs/install/mcp-tool-integrations.md +19 -3
  74. package/package.json +2 -2
@@ -25,6 +25,44 @@ function joinPath(base, rel) {
25
25
  return `${trimmedBase}/${trimmedRel}`;
26
26
  }
27
27
  // ---------------------------------------------------------------------------
28
+ // Adapters — the exhaustive physical-format map every HostKind must resolve.
29
+ // `satisfies` (rather than an explicit `Record<HostKind, HostAdapter>`
30
+ // annotation) keeps each adapter's literal field types narrow while still
31
+ // making an incomplete map a compile error.
32
+ // ---------------------------------------------------------------------------
33
+ export const HOST_ADAPTERS = {
34
+ "claude-code-json": {
35
+ format: "json",
36
+ topLevelKey: "mcpServers",
37
+ transportType: undefined, // Claude Code omits `type`.
38
+ },
39
+ "cursor-json": {
40
+ format: "json",
41
+ topLevelKey: "mcpServers",
42
+ transportType: "stdio",
43
+ },
44
+ "vscode-json": {
45
+ format: "json",
46
+ topLevelKey: "servers",
47
+ transportType: "stdio",
48
+ },
49
+ "codex-toml": {
50
+ format: "toml",
51
+ topLevelKey: "mcp_servers",
52
+ transportType: undefined, // TOML table shape has no `type`.
53
+ },
54
+ "copilot-cli": {
55
+ format: "json",
56
+ topLevelKey: "mcpServers",
57
+ transportType: "local",
58
+ extraEntryKeys: { tools: ["*"] },
59
+ },
60
+ };
61
+ /** Pure typed lookup from a target's declared {@link HostKind} to its adapter. */
62
+ export function hostAdapterForTarget(target) {
63
+ return HOST_ADAPTERS[target.hostKind];
64
+ }
65
+ // ---------------------------------------------------------------------------
28
66
  // Registry — additive; add a new entry (and a PlatformId member) per platform.
29
67
  // ---------------------------------------------------------------------------
30
68
  /**
@@ -38,9 +76,7 @@ export const MCP_HOST_TARGETS = {
38
76
  scope: "project",
39
77
  relPath: ".mcp.json",
40
78
  displayPath: ".mcp.json",
41
- format: "json",
42
- topLevelKey: "mcpServers",
43
- transportType: undefined, // Claude Code omits `type`.
79
+ hostKind: "claude-code-json",
44
80
  vendorCli: { bin: "claude", kind: "claude-add-json" },
45
81
  launchAgent: "claude",
46
82
  worktreeSupported: true,
@@ -54,9 +90,7 @@ export const MCP_HOST_TARGETS = {
54
90
  scope: "project",
55
91
  relPath: ".cursor/mcp.json",
56
92
  displayPath: ".cursor/mcp.json",
57
- format: "json",
58
- topLevelKey: "mcpServers",
59
- transportType: "stdio",
93
+ hostKind: "cursor-json",
60
94
  launchAgent: "cursor-agent",
61
95
  worktreeSupported: true,
62
96
  writeStrategy: "direct",
@@ -70,9 +104,7 @@ export const MCP_HOST_TARGETS = {
70
104
  scope: "project",
71
105
  relPath: ".vscode/mcp.json",
72
106
  displayPath: ".vscode/mcp.json",
73
- format: "json",
74
- topLevelKey: "servers",
75
- transportType: "stdio",
107
+ hostKind: "vscode-json",
76
108
  worktreeSupported: false,
77
109
  writeStrategy: "direct",
78
110
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".vscode")),
@@ -83,10 +115,7 @@ export const MCP_HOST_TARGETS = {
83
115
  scope: "global",
84
116
  absPathResolver: (homedir) => joinPath(homedir, ".copilot/mcp-config.json"),
85
117
  displayPath: "~/.copilot/mcp-config.json",
86
- format: "json",
87
- topLevelKey: "mcpServers",
88
- transportType: "local",
89
- extraEntryKeys: { tools: ["*"] },
118
+ hostKind: "copilot-cli",
90
119
  vendorCli: { bin: "copilot", kind: "copilot-add" },
91
120
  worktreeSupported: false,
92
121
  writeStrategy: "vendor-first",
@@ -99,9 +128,7 @@ export const MCP_HOST_TARGETS = {
99
128
  scope: "global",
100
129
  absPathResolver: (homedir) => joinPath(homedir, ".codex/config.toml"),
101
130
  displayPath: "~/.codex/config.toml",
102
- format: "toml",
103
- topLevelKey: "mcp_servers",
104
- transportType: undefined, // TOML table shape has no `type`.
131
+ hostKind: "codex-toml",
105
132
  vendorCli: { bin: "codex", kind: "codex-add" },
106
133
  worktreeSupported: false,
107
134
  writeStrategy: "vendor-first",
@@ -113,10 +140,7 @@ export const MCP_HOST_TARGETS = {
113
140
  scope: "global",
114
141
  absPathResolver: (homedir) => joinPath(homedir, ".codeium/windsurf/mcp_config.json"),
115
142
  displayPath: "~/.codeium/windsurf/mcp_config.json",
116
- format: "json",
117
- topLevelKey: "mcpServers",
118
- // Windsurf historically renders with no `type` (like Claude); preserve that.
119
- transportType: undefined,
143
+ hostKind: "claude-code-json", // Windsurf uses the Claude-compatible JSON shape.
120
144
  worktreeSupported: false,
121
145
  // Preserve current behavior: never auto-modify the global Windsurf file.
122
146
  writeStrategy: "manual-instructions",
@@ -164,7 +188,7 @@ export function getWorktreeHostTargets() {
164
188
  }
165
189
  /** Project-scoped JSON targets (used by init, launcher-token merge, doctor). */
166
190
  export function getProjectJsonTargets() {
167
- return allHostTargets().filter((t) => t.scope === "project" && t.format === "json");
191
+ return allHostTargets().filter((t) => t.scope === "project" && hostAdapterForTarget(t).format === "json");
168
192
  }
169
193
  /** Targets written automatically (vendor-first or direct). */
170
194
  export function getAutomaticHostTargets() {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Single TypeScript source of truth for the current MCP server identity
3
+ * (BAPI-806) and for permanent legacy-name compatibility (BAPI-807).
4
+ *
5
+ * Every production reader, writer, doctor, and display string must import
6
+ * these constants rather than duplicating the server name or npm package
7
+ * literal. This module is intentionally stateless and dependency-free so it
8
+ * can be imported from anywhere in `mcp_server/src` without introducing a
9
+ * require cycle.
10
+ *
11
+ * The registration key and the npm package are DIFFERENT identities. Flipping
12
+ * the host-facing key from `bridge-api` to `bridge` leaves
13
+ * {@link MCP_PACKAGE_NAME}, the `bridge-api-mcp-server` bin names, the
14
+ * `bapi:<repo>` credential namespace, `mcp-invoke --target bapi`, and every
15
+ * `BAPI_*` environment variable untouched.
16
+ */
17
+ /** The current host-facing MCP server registration name. */
18
+ export const MCP_SERVER_NAME = "bridge";
19
+ /** The current published npm package name for the MCP server. */
20
+ export const MCP_PACKAGE_NAME = "@bridge_gpt/mcp-server";
21
+ /** The README resource URI served by the MCP server. */
22
+ export const MCP_README_URI = `${MCP_SERVER_NAME}://readme`;
23
+ /**
24
+ * Registration names that are recognized FOREVER when reading an existing host
25
+ * configuration, and never written for a fresh registration (BAPI-807).
26
+ *
27
+ * This is a PERMANENT compatibility surface, not a migration window. Bridge does
28
+ * not rename, migrate, or repair an existing `bridge-api` registration: someone
29
+ * whose config was written before the rename keeps working indefinitely, and
30
+ * every reader — `init`, `upgrade`, `doctor`, install, and worktree provisioning
31
+ * — must treat a legacy key as a first-class existing registration rather than
32
+ * as absence. Do not add a deprecation date, a removal plan, or an automatic
33
+ * rewrite; the whole point is that no user is ever required to act.
34
+ */
35
+ export const LEGACY_SERVER_NAMES = ["bridge-api"];
36
+ /**
37
+ * Every recognized key, canonical FIRST. Readers that need to enumerate keys
38
+ * (the Codex table matcher, the Claude scope inspector) build their candidate
39
+ * list from this rather than restating the literals.
40
+ */
41
+ export const RECOGNIZED_SERVER_NAMES = [
42
+ MCP_SERVER_NAME,
43
+ ...LEGACY_SERVER_NAMES,
44
+ ];
45
+ /** True for a string that is one of Bridge's recognized registration keys. */
46
+ export function isRecognizedServerName(value) {
47
+ return (typeof value === "string" &&
48
+ RECOGNIZED_SERVER_NAMES.includes(value));
49
+ }
50
+ /**
51
+ * Resolve which registration key a caller should read or write in a servers map.
52
+ *
53
+ * Two properties are load-bearing and must not be "simplified" away:
54
+ *
55
+ * - **Own properties only.** A `bridge` or `bridge-api` inherited from
56
+ * `Object.prototype` (or from a JSON-parsed object's prototype chain in a
57
+ * polluted runtime) is NOT a registration. Resolving one would let prototype
58
+ * pollution redirect a write.
59
+ * - **Presence, not validity.** Two present keys are a conflict even when one
60
+ * entry is `null`, an array, or otherwise malformed. Picking the "valid
61
+ * looking" one would silently choose which of a user's two registrations
62
+ * wins, which is exactly the decision a human has to make.
63
+ *
64
+ * A non-object input (missing servers section, `null`, an array) resolves to
65
+ * `absent`: there is nothing there to update, and a fresh write is canonical.
66
+ */
67
+ export function resolveRegistrationKey(servers) {
68
+ const absent = { state: "absent", writeKey: MCP_SERVER_NAME };
69
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
70
+ return absent;
71
+ }
72
+ const present = RECOGNIZED_SERVER_NAMES.filter((name) => Object.prototype.hasOwnProperty.call(servers, name));
73
+ if (present.length === 0)
74
+ return absent;
75
+ if (present.length > 1)
76
+ return { state: "conflict", keys: present };
77
+ const [only] = present;
78
+ return only === MCP_SERVER_NAME
79
+ ? { state: "canonical", key: MCP_SERVER_NAME }
80
+ : { state: "legacy", key: only };
81
+ }
82
+ /**
83
+ * The key an EXISTING registration lives under, or `null` when there is none or
84
+ * the state is ambiguous. Convenience for readers that already handled — or
85
+ * deliberately do not care about — the absent and conflict branches.
86
+ */
87
+ export function existingRegistrationKey(servers) {
88
+ const resolution = resolveRegistrationKey(servers);
89
+ return resolution.state === "canonical" || resolution.state === "legacy"
90
+ ? resolution.key
91
+ : null;
92
+ }
@@ -7,7 +7,8 @@
7
7
  * per-developer and is gitignored before it is written.
8
8
  *
9
9
  * SECURITY: the state is strictly SECRET-FREE. It stores only a validated
10
- * platform roster and repo-relative project config paths. It never contains API
10
+ * platform roster, repo-relative project config paths, home-relative generated
11
+ * service-unit locators, and identifiers from a closed capability vocabulary. It never contains API
11
12
  * keys, env blocks, absolute home paths, or vendor command payloads. Reads
12
13
  * validate every stored platform ID against {@link MCP_HOST_TARGETS}; writes
13
14
  * serialize only the schema-approved fields.
@@ -21,6 +22,22 @@ import { HOST_PLATFORM_ORDER, isHostPlatformId, } from "./mcp-host-targets.js";
21
22
  export const MCP_INSTALL_STATE_VERSION = 1;
22
23
  /** The relative path of the state file within a project. */
23
24
  export const MCP_INSTALL_STATE_RELPATH = ".bridge/install-state.json";
25
+ /**
26
+ * The conductor capabilities `install conductor` may record (BAPI-775).
27
+ *
28
+ * A CLOSED vocabulary, validated on read and on write: install state is a
29
+ * diagnostic inventory of what this tool provisioned, so an arbitrary
30
+ * caller-supplied string must never become a recorded capability.
31
+ */
32
+ export const MCP_INSTALL_CAPABILITY_IDS = [
33
+ "conductor_local_observability",
34
+ "conductor_tool_visibility",
35
+ ];
36
+ /** Whether a value is one of the ratified capability identifiers. */
37
+ export function isMcpInstallCapabilityId(value) {
38
+ return (typeof value === "string" &&
39
+ MCP_INSTALL_CAPABILITY_IDS.includes(value));
40
+ }
24
41
  // ---------------------------------------------------------------------------
25
42
  // Path helpers (POSIX-normalized; avoids node:path so tests are deterministic).
26
43
  // ---------------------------------------------------------------------------
@@ -60,6 +77,23 @@ function normalizeProjectPaths(paths) {
60
77
  out.sort();
61
78
  return out;
62
79
  }
80
+ /**
81
+ * Validate, dedupe, and stably order capability identifiers (BAPI-775).
82
+ *
83
+ * Unknown identifiers are DROPPED rather than failing the document, matching the
84
+ * executor-locator rule below: a capability nobody can act on must not take the
85
+ * platform roster down with it.
86
+ */
87
+ function normalizeCapabilities(values) {
88
+ const wanted = new Set();
89
+ for (const value of values) {
90
+ if (isMcpInstallCapabilityId(value))
91
+ wanted.add(value);
92
+ }
93
+ // Registry order, not insertion order, so two runs recording the same pair
94
+ // serialize identically.
95
+ return MCP_INSTALL_CAPABILITY_IDS.filter((id) => wanted.has(id));
96
+ }
63
97
  // ---------------------------------------------------------------------------
64
98
  // Executor service-unit locators (BAPI-779)
65
99
  // ---------------------------------------------------------------------------
@@ -193,6 +227,12 @@ export async function readMcpInstallState(cwd, deps) {
193
227
  if (rawUnitPaths !== undefined && !Array.isArray(rawUnitPaths)) {
194
228
  return { status: "invalid", reason: "executorServiceUnitPaths is not an array" };
195
229
  }
230
+ // Same absent-vs-wrong-type rule as the field above (BAPI-775): every document
231
+ // written before conductor capabilities existed simply lacks the key.
232
+ const rawCapabilities = doc.capabilities;
233
+ if (rawCapabilities !== undefined && !Array.isArray(rawCapabilities)) {
234
+ return { status: "invalid", reason: "capabilities is not an array" };
235
+ }
196
236
  return {
197
237
  status: "valid",
198
238
  state: {
@@ -203,6 +243,7 @@ export async function readMcpInstallState(cwd, deps) {
203
243
  // failing the read: they cannot be acted on, and losing the whole
204
244
  // document over one bad entry would take the platform roster with it.
205
245
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths(Array.isArray(rawUnitPaths) ? rawUnitPaths : []),
246
+ capabilities: normalizeCapabilities(Array.isArray(rawCapabilities) ? rawCapabilities : []),
206
247
  },
207
248
  };
208
249
  }
@@ -211,6 +252,8 @@ export async function readMcpInstallState(cwd, deps) {
211
252
  // ---------------------------------------------------------------------------
212
253
  /** Serialize an install-state document deterministically (stable key order). */
213
254
  export function serializeMcpInstallState(state) {
255
+ // Re-normalized here for the same last-gate reason as the locators below.
256
+ const capabilities = normalizeCapabilities(state.capabilities ?? []);
214
257
  // Explicit key order — do not rely on object insertion order for stability.
215
258
  const ordered = {
216
259
  version: state.version,
@@ -220,6 +263,12 @@ export function serializeMcpInstallState(state) {
220
263
  // this function is the last gate before bytes hit disk, and a caller could
221
264
  // hand it a hand-built state carrying an arbitrary path.
222
265
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths(state.executorServiceUnitPaths ?? []),
266
+ // BAPI-775: emitted ONLY when a capability was actually recorded. An empty
267
+ // roster and an absent key mean the same thing to every reader (the read
268
+ // path treats an absent array as empty), and omitting it keeps a document
269
+ // that predates conductor capabilities byte-identical after a rewrite —
270
+ // additive on disk, not merely additive in the schema.
271
+ ...(capabilities.length > 0 ? { capabilities } : {}),
223
272
  };
224
273
  return JSON.stringify(ordered, null, 2) + "\n";
225
274
  }
@@ -233,6 +282,7 @@ export function serializeMcpInstallState(state) {
233
282
  export async function writeMcpInstallState(cwd, input, deps) {
234
283
  const existing = await readMcpInstallState(cwd, deps);
235
284
  const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
285
+ const priorCapabilities = existing.status === "valid" ? (existing.state.capabilities ?? []) : [];
236
286
  const state = {
237
287
  version: MCP_INSTALL_STATE_VERSION,
238
288
  selectedPlatforms: normalizePlatforms(input.selectedPlatforms),
@@ -241,6 +291,10 @@ export async function writeMcpInstallState(cwd, input, deps) {
241
291
  ...priorUnitPaths,
242
292
  ...(input.executorServiceUnitPaths ?? []),
243
293
  ]),
294
+ // Preserved for the same reason as the unit locators above: a whole-document
295
+ // rewrite by a caller that knows nothing about capabilities must not erase
296
+ // them (BAPI-775).
297
+ capabilities: normalizeCapabilities(priorCapabilities),
244
298
  };
245
299
  const finalPath = installStatePath(cwd);
246
300
  return persistMcpInstallState(cwd, state, finalPath, deps);
@@ -298,6 +352,7 @@ export async function recordInstalledProjectArtifact(cwd, relPath, deps) {
298
352
  // executor units a previous conductor run recorded (the same additive rule
299
353
  // that motivated this helper for the platform roster).
300
354
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths(priorUnitPaths),
355
+ capabilities: normalizeCapabilities(existing.status === "valid" ? (existing.state.capabilities ?? []) : []),
301
356
  };
302
357
  return persistMcpInstallState(cwd, state, installStatePath(cwd), deps);
303
358
  }
@@ -332,6 +387,44 @@ export async function recordInstalledExecutorServiceUnit(cwd, unitPath, homeDir,
332
387
  selectedPlatforms: normalizePlatforms(priorPlatforms),
333
388
  projectConfigPaths: normalizeProjectPaths(priorPaths),
334
389
  executorServiceUnitPaths: normalizeExecutorServiceUnitPaths([...priorUnitPaths, locator]),
390
+ capabilities: normalizeCapabilities(existing.status === "valid" ? (existing.state.capabilities ?? []) : []),
391
+ };
392
+ return persistMcpInstallState(cwd, state, installStatePath(cwd), deps);
393
+ }
394
+ /**
395
+ * Record a provisioned conductor capability WITHOUT clobbering existing state
396
+ * (BAPI-775).
397
+ *
398
+ * The third member of the additive-recorder family, and it follows the same rule
399
+ * the other two do: read the current document, write back the union of every
400
+ * dimension. Recording tool visibility must not drop the workflow artifact path,
401
+ * the platform roster, or an executor unit a previous run recorded.
402
+ *
403
+ * The capability id is validated against the closed
404
+ * {@link MCP_INSTALL_CAPABILITY_IDS} vocabulary BEFORE any read or write, so an
405
+ * unrecognized identifier is refused with an advisory result and touches
406
+ * nothing — never silently dropped after a rewrite. Repeating a capability is a
407
+ * set-style no-op, so re-running `install conductor` cannot duplicate an entry.
408
+ */
409
+ export async function recordInstalledConductorCapability(cwd, capability, deps) {
410
+ if (!isMcpInstallCapabilityId(capability)) {
411
+ return {
412
+ ok: false,
413
+ error: "unknown conductor capability identifier; nothing was recorded " +
414
+ `(expected one of: ${MCP_INSTALL_CAPABILITY_IDS.join(", ")}).`,
415
+ };
416
+ }
417
+ const existing = await readMcpInstallState(cwd, deps);
418
+ const priorPlatforms = existing.status === "valid" ? existing.state.selectedPlatforms : [];
419
+ const priorPaths = existing.status === "valid" ? existing.state.projectConfigPaths : [];
420
+ const priorUnitPaths = existing.status === "valid" ? existing.state.executorServiceUnitPaths : [];
421
+ const priorCapabilities = existing.status === "valid" ? (existing.state.capabilities ?? []) : [];
422
+ const state = {
423
+ version: MCP_INSTALL_STATE_VERSION,
424
+ selectedPlatforms: normalizePlatforms(priorPlatforms),
425
+ projectConfigPaths: normalizeProjectPaths(priorPaths),
426
+ executorServiceUnitPaths: normalizeExecutorServiceUnitPaths(priorUnitPaths),
427
+ capabilities: normalizeCapabilities([...priorCapabilities, capability]),
335
428
  };
336
429
  return persistMcpInstallState(cwd, state, installStatePath(cwd), deps);
337
430
  }
@@ -26,6 +26,7 @@ import os from "os";
26
26
  import { resolveRepoNameForProjectRoot, readBridgeConfig, validateMcpTarget, } from "./bridge-config.js";
27
27
  import { resolveBapiCredentials } from "./credential-store.js";
28
28
  import { describeBaseUrlRejection, validateHttpBaseUrl } from "./base-url.js";
29
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
29
30
  import { getThirdPartyTargetDefinition, resolveThirdPartyTargetEnv, validateThirdPartyTargetManifestEntry, } from "./third-party-mcp-targets.js";
30
31
  // ---------------------------------------------------------------------------
31
32
  // Usage / argument parsing
@@ -36,7 +37,7 @@ export function getMcpInvokeUsage() {
36
37
  " node <abs>/mcp_server/build/index.js mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",
37
38
  "",
38
39
  " (npm-channel fallback may invoke the same shim through a package spec, e.g.",
39
- " npx -y @bridge_gpt/mcp-server@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)",
40
+ ` npx -y ${MCP_PACKAGE_NAME}@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)`,
40
41
  "",
41
42
  "Internal worktree shim: resolves the target's launch command and credentials",
42
43
  "from the given project root, then spawns the real MCP server over stdio.",
@@ -24,6 +24,8 @@ import path from "path";
24
24
  import { readBridgeConfig } from "./bridge-config.js";
25
25
  import { getThirdPartyTargetDefinition, validateThirdPartyTargetManifestEntry, } from "./third-party-mcp-targets.js";
26
26
  import { buildMcpShimCommand, } from "./mcp-server-invocation.js";
27
+ import { getWorktreeHostTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
28
+ import { existingRegistrationKey, MCP_SERVER_NAME } from "./mcp-identity.js";
27
29
  // ---------------------------------------------------------------------------
28
30
  // Pure helpers
29
31
  // ---------------------------------------------------------------------------
@@ -53,9 +55,9 @@ export function normalizeWorktreePathForRegistration(worktreePath, deps) {
53
55
  }
54
56
  return { ok: true, path: resolved };
55
57
  }
56
- /** Map an MCP target to its registration server name (`bapi` -> `bridge-api`). */
58
+ /** Map an MCP target to its registration server name (`bapi` -> the canonical Bridge key). */
57
59
  export function serverNameForMcpTarget(target) {
58
- return target === "bapi" ? "bridge-api" : target;
60
+ return target === "bapi" ? MCP_SERVER_NAME : target;
59
61
  }
60
62
  /**
61
63
  * Build a secret-free shim entry for any target from a structured invocation.
@@ -136,10 +138,10 @@ export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath,
136
138
  /** Both registration files written for every provisioned worktree. */
137
139
  export function getWorktreeMcpRegistrationTargets(worktreePath, platform) {
138
140
  const api = pathApiForProvisioningPlatform(platform);
139
- return [
140
- { filePath: api.join(worktreePath, ".mcp.json"), topLevelKey: "mcpServers" },
141
- { filePath: api.join(worktreePath, ".cursor", "mcp.json"), topLevelKey: "mcpServers" },
142
- ];
141
+ return getWorktreeHostTargets().map((target) => ({
142
+ filePath: api.join(worktreePath, target.relPath),
143
+ topLevelKey: hostAdapterForTarget(target).topLevelKey,
144
+ }));
143
145
  }
144
146
  /**
145
147
  * Absolute path to the worktree's Claude local settings file
@@ -183,8 +185,18 @@ export function mergeEnabledMcpjsonServers(existing, serverNames) {
183
185
  *
184
186
  * Deliberately UNCHANGED by BAPI-727: because a generated name is replaced rather
185
187
  * than merged field-by-field, re-provisioning an existing worktree automatically
186
- * upgrades an older `bridge-api` entry that lacks `--base-url` to the URL-bearing
188
+ * upgrades an older Bridge entry that lacks `--base-url` to the URL-bearing
187
189
  * one. No migration step is needed.
190
+ *
191
+ * BAPI-807 keeps that force-upgrade working across the registration rename. The
192
+ * Bridge entry is written under the key that ALREADY exists in the document
193
+ * (`bridge` or the permanently-supported `bridge-api`), not blindly under the
194
+ * generated canonical name. Writing the canonical name unconditionally would
195
+ * leave a pre-rename `bridge-api` entry sitting untouched beside the new one —
196
+ * and because a legacy entry can carry an embedded `BAPI_API_KEY`, that stale
197
+ * entry would preserve a secret this function exists to strip. Tier-2 targets
198
+ * (`sfcc`, …) are unaffected: they are not Bridge's own registration and are
199
+ * always written under their own name.
188
200
  */
189
201
  export function mergeMcpRegistrations(existing, topLevelKey, entries) {
190
202
  const result = existing && typeof existing === "object" && !Array.isArray(existing)
@@ -194,18 +206,39 @@ export function mergeMcpRegistrations(existing, topLevelKey, entries) {
194
206
  const servers = current && typeof current === "object" && !Array.isArray(current)
195
207
  ? { ...current }
196
208
  : {};
209
+ // Resolve ONCE against the pre-merge document, so a legacy entry is replaced
210
+ // in place rather than duplicated.
211
+ const bridgeKey = existingRegistrationKey(servers);
197
212
  for (const [name, entry] of Object.entries(entries)) {
198
- servers[name] = entry;
213
+ const writeKey = name === MCP_SERVER_NAME && bridgeKey !== null && bridgeKey !== MCP_SERVER_NAME
214
+ ? bridgeKey
215
+ : name;
216
+ servers[writeKey] = entry;
199
217
  }
200
218
  result[topLevelKey] = servers;
201
219
  return result;
202
220
  }
203
221
  /**
204
- * Back-compat single-entry merge. Sets only `mcpServers["bridge-api"]`; unrelated
205
- * top-level fields and servers are preserved.
222
+ * Back-compat single-entry merge for the Bridge shim. Unrelated top-level fields
223
+ * and servers are preserved.
224
+ *
225
+ * BAPI-807: the write key is RESOLVED from the existing document rather than
226
+ * assumed canonical. A worktree whose registration was provisioned before the
227
+ * rename keeps its `bridge-api` key — reprovisioning updates that entry in place
228
+ * instead of adding a second `bridge` one beside it, which would leave the
229
+ * worktree with two shims pointing at the same server. A fresh worktree, and any
230
+ * document with no recognized key, gets {@link MCP_SERVER_NAME}.
231
+ *
232
+ * A both-key document resolves to the canonical key here rather than refusing:
233
+ * this path only ever REPLACES one generated shim entry, both entries are
234
+ * Bridge's own generated shims, and the worktree registration doctor
235
+ * (`probeWorktreeMcpRegistration`) is the surface that reports the duplicate.
236
+ * Refusing here would leave a worktree unable to reprovision at all.
206
237
  */
207
238
  export function mergeBridgeApiMcpRegistration(existing, topLevelKey, entry) {
208
- return mergeMcpRegistrations(existing, topLevelKey, { "bridge-api": entry });
239
+ // `mergeMcpRegistrations` now resolves the Bridge key itself, so passing the
240
+ // canonical name is sufficient — and keeps ONE place that decides the key.
241
+ return mergeMcpRegistrations(existing, topLevelKey, { [MCP_SERVER_NAME]: entry });
209
242
  }
210
243
  // ---------------------------------------------------------------------------
211
244
  // Filesystem writes
@@ -310,7 +343,7 @@ function withWarnings(row, warnings) {
310
343
  * FAIL-OPEN, and BAPI-790 makes that posture worth stating explicitly, because a
311
344
  * fail-CLOSED requirement now sits directly downstream of it. This function
312
345
  * ATTEMPTS the write and degrades to warnings; the executor then separately
313
- * VERIFIES that a usable `.mcp.json` exists and registers `bridge-api`, and
346
+ * VERIFIES that a usable `.mcp.json` exists and registers a recognized Bridge key, and
314
347
  * refuses to spawn when it does not (`verifyRequiredWorktreeMcpRegistration` in
315
348
  * `executor/job-runner.ts`). The split is deliberate: a provisioning hiccup on a
316
349
  * Tier-2 target must not kill a job, but a worker that loads MCP servers strictly
@@ -7,6 +7,9 @@
7
7
  * the sole injected dependency is `readFile`.
8
8
  */
9
9
  import path from "path";
10
+ import { getWorktreeHostTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
11
+ import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
12
+ import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
10
13
  /**
11
14
  * Read and parse a JSON file.
12
15
  *
@@ -72,7 +75,7 @@ export function isBridgeApiShimEntry(entry, worktreeRoot) {
72
75
  const args = candidate.args.filter((a) => typeof a === "string");
73
76
  if (candidate.command === "npx") {
74
77
  // npm-channel fallback: must reference the package spec.
75
- if (!args.some((a) => a.startsWith("@bridge_gpt/mcp-server")))
78
+ if (!args.some((a) => a.startsWith(MCP_PACKAGE_NAME)))
76
79
  return false;
77
80
  }
78
81
  else {
@@ -90,36 +93,55 @@ export function isBridgeApiShimEntry(entry, worktreeRoot) {
90
93
  }
91
94
  /**
92
95
  * Inspect both `<worktreeRoot>/.mcp.json` and `<worktreeRoot>/.cursor/mcp.json`.
93
- * Reports `found` when at least one registration file contains a valid
94
- * `bridge-api` shim entry pointing at this worktree; otherwise `found: false`
95
- * with an actionable, secret-free hint. Never spawns anything.
96
+ * Reports `found` when at least one registration file contains a valid Bridge
97
+ * shim entry under EITHER the canonical or a permanently supported legacy key
98
+ * (BAPI-807) pointing at this worktree; otherwise `found: false` with an
99
+ * actionable, secret-free hint. Never spawns anything.
100
+ *
101
+ * A file carrying BOTH recognized keys is a FAULT, not a healthy registration:
102
+ * which shim the agent actually loads is undefined, so it is reported with
103
+ * repair guidance rather than silently accepted because one of the two happened
104
+ * to validate.
96
105
  */
97
106
  export async function probeWorktreeMcpRegistration(worktreeRoot, deps) {
98
- const targets = [
99
- path.join(worktreeRoot, ".mcp.json"),
100
- path.join(worktreeRoot, ".cursor", "mcp.json"),
101
- ];
102
- for (const filePath of targets) {
107
+ const targets = getWorktreeHostTargets().map((target) => ({
108
+ filePath: path.join(worktreeRoot, target.relPath),
109
+ topLevelKey: hostAdapterForTarget(target).topLevelKey,
110
+ }));
111
+ for (const { filePath, topLevelKey } of targets) {
103
112
  const read = await readJsonIfPresent(filePath, deps);
104
113
  if (read.state !== "present")
105
114
  continue;
106
115
  const doc = read.value;
107
116
  if (!doc || typeof doc !== "object" || Array.isArray(doc))
108
117
  continue;
109
- const servers = doc.mcpServers;
118
+ const servers = doc[topLevelKey];
110
119
  if (!servers || typeof servers !== "object" || Array.isArray(servers))
111
120
  continue;
112
- const entry = servers["bridge-api"];
121
+ const displayPath = path.basename(path.dirname(filePath)) === ".cursor" ? ".cursor/mcp.json" : ".mcp.json";
122
+ const resolution = resolveRegistrationKey(servers);
123
+ if (resolution.state === "conflict") {
124
+ return {
125
+ found: false,
126
+ detail: `${displayPath}: ${DUPLICATE_REGISTRATION_GUIDANCE}`,
127
+ };
128
+ }
129
+ if (resolution.state === "absent")
130
+ continue;
131
+ const entry = servers[resolution.key];
113
132
  if (isBridgeApiShimEntry(entry, worktreeRoot)) {
133
+ // A legacy-key shim is HEALTHY. It is identified as such so an operator
134
+ // reading the report knows why the key differs and that nothing is wrong.
135
+ const legacyNote = resolution.state === "legacy" ? " (supported legacy registration)" : "";
114
136
  return {
115
137
  found: true,
116
- detail: `bridge-api shim registered in ${path.basename(path.dirname(filePath)) === ".cursor" ? ".cursor/mcp.json" : ".mcp.json"}`,
138
+ detail: `${resolution.key} shim registered in ${displayPath}${legacyNote}`,
117
139
  };
118
140
  }
119
141
  }
120
142
  return {
121
143
  found: false,
122
- detail: "No worktree .mcp.json or .cursor/mcp.json points at the bridge-api mcp-invoke shim. " +
144
+ detail: `No worktree .mcp.json or .cursor/mcp.json points at the ${MCP_SERVER_NAME} mcp-invoke shim. ` +
123
145
  "Re-run start-tickets to provision the worktree MCP registration.",
124
146
  };
125
147
  }
@@ -21,13 +21,15 @@
21
21
  * re-plumbing argv construction.
22
22
  */
23
23
  import path from "node:path";
24
- export const MCP_SERVER_PACKAGE_NAME = "@bridge_gpt/mcp-server";
24
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
25
+ /** Compatibility alias — `executor/service-unit.ts` still imports this name. */
26
+ export const MCP_SERVER_PACKAGE_NAME = MCP_PACKAGE_NAME;
25
27
  /**
26
28
  * Resolvable npm-channel fallback spec. Deliberately a moving channel (`@latest`)
27
29
  * rather than an exact generated version pin, so the fallback path can never
28
30
  * reintroduce the ETARGET/404 failure the absolute-path form exists to remove.
29
31
  */
30
- export const DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC = "@bridge_gpt/mcp-server@latest";
32
+ export const DEFAULT_MCP_SERVER_NPM_CHANNEL_SPEC = `${MCP_PACKAGE_NAME}@latest`;
31
33
  /**
32
34
  * Build the concrete `{ command, args }` launch pair for a target + worktree from
33
35
  * a structured invocation. Pure — no filesystem or process access. The