@bridge_gpt/mcp-server 0.2.41 → 0.2.43
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.
- package/README.md +330 -191
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/agents.generated.js +2 -2
- package/build/claude-review-workflow.js +510 -45
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +6 -5
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +17 -0
- package/build/conduct-epic/cli.js +947 -99
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +148 -1
- package/build/conductor/cli.js +109 -1
- package/build/conductor/doctor.js +101 -16
- package/build/conductor/epic-reconcile.js +72 -19
- package/build/conductor/epic-runtime.js +15 -3
- package/build/conductor/errors.js +47 -0
- package/build/conductor/git-hooks.js +205 -11
- package/build/conductor/install-doctor.js +230 -1
- package/build/conductor/local-merge.js +130 -28
- package/build/conductor/recovery-cli.js +313 -0
- package/build/conductor/recovery-operations.js +219 -0
- package/build/conductor/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +20 -16
- package/build/credentials-cli.js +3 -2
- package/build/docs.generated.js +2 -1
- package/build/doctor.js +120 -44
- package/build/drive-epic.js +375 -0
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/http-client.js +71 -3
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-errors.js +9 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +198 -29
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +259 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +454 -48
- package/build/executor/test-clock.js +3 -2
- package/build/executor/worker-finalization.js +233 -56
- package/build/executor/worktree.js +8 -1
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +2277 -270
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +225 -47
- package/build/install-doctor.js +23 -9
- package/build/install-reexec.js +2 -1
- package/build/launcher-config-inspection.js +83 -22
- package/build/mcp-host-config.js +331 -67
- package/build/mcp-host-targets.js +45 -21
- package/build/mcp-identity.js +92 -0
- package/build/mcp-install-state.js +94 -1
- package/build/mcp-invoke.js +2 -1
- package/build/mcp-provisioning.js +45 -12
- package/build/mcp-registration-doctor.js +35 -13
- package/build/mcp-server-invocation.js +4 -2
- package/build/merge-pull-request.js +208 -9
- package/build/pipelines.generated.js +305 -15
- package/build/plane/cli.js +73 -7
- package/build/plane/defaults.js +18 -5
- package/build/plane/manifest.js +90 -0
- package/build/plane/preflight.js +100 -10
- package/build/plane/shutdown.js +71 -3
- package/build/plane/test-fakes.js +9 -1
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +3 -2
- package/build/review-tickets.js +8 -7
- package/build/run-unit-tests-launcher.js +149 -6
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +531 -82
- package/build/sfcc/tool-wrapper.js +15 -0
- package/build/start-tickets-prereqs.js +11 -6
- package/build/start-tickets.js +91 -85
- package/build/update-check.js +3 -2
- package/build/upgrade-advice.js +2 -1
- package/build/upgrade-cli.js +50 -18
- package/build/version.generated.js +2 -1
- package/build/worktree-core.js +31 -17
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
- package/pipelines/greenfield-setup.json +286 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/build/mcp-invoke.js
CHANGED
|
@@ -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
|
-
|
|
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` ->
|
|
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" ?
|
|
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
|
-
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
205
|
-
*
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
95
|
-
*
|
|
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,
|
|
100
|
-
|
|
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
|
|
118
|
+
const servers = doc[topLevelKey];
|
|
110
119
|
if (!servers || typeof servers !== "object" || Array.isArray(servers))
|
|
111
120
|
continue;
|
|
112
|
-
const
|
|
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:
|
|
138
|
+
detail: `${resolution.key} shim registered in ${displayPath}${legacyNote}`,
|
|
117
139
|
};
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
return {
|
|
121
143
|
found: false,
|
|
122
|
-
detail:
|
|
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
|
-
|
|
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 =
|
|
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
|