@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.
- package/README.md +10 -10
- package/build/agent-capabilities/cli.js +2 -1
- package/build/agent-launchers/claude-executor-adapter.js +17 -4
- package/build/claude-user-config-doctor.js +42 -11
- package/build/cli-release.js +2 -1
- package/build/commands.generated.js +4 -4
- package/build/conduct-epic/bridge-client.js +354 -113
- package/build/conduct-epic/checkpoint-store.js +75 -2
- package/build/conduct-epic/cli.js +795 -109
- package/build/conduct-epic/cut-protocol.js +327 -0
- package/build/conduct-epic/pr-state.js +113 -24
- package/build/conduct-epic/spawn.js +14 -2
- package/build/conductor/bridge-api-client.js +27 -1
- package/build/conductor/cli.js +46 -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/tools.js +32 -3
- package/build/conductor/worker-ledger-cli.js +27 -1
- package/build/conductor-bin.js +15 -15
- package/build/credentials-cli.js +3 -2
- package/build/doctor.js +107 -41
- package/build/executor/cli.js +48 -1
- package/build/executor/env.js +21 -0
- package/build/executor/index-scope.js +39 -0
- package/build/executor/job-log-registry.js +69 -0
- package/build/executor/job-runner.js +148 -26
- package/build/executor/live-worker-registry.js +83 -0
- package/build/executor/observation.js +167 -6
- package/build/executor/platform.js +147 -3
- package/build/executor/process.js +58 -14
- package/build/executor/runner.js +235 -48
- package/build/executor/test-clock.js +3 -2
- package/build/index-scope-contract.js +96 -0
- package/build/index.js +153 -204
- package/build/init.js +83 -22
- package/build/install-bridge-conductor.js +323 -14
- package/build/install-bridge.js +202 -38
- 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 +3 -3
- package/build/plane/defaults.js +4 -1
- package/build/plane/preflight.js +81 -10
- 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 +74 -1
- package/build/schedule-run.js +3 -2
- package/build/setup-epic.js +453 -78
- 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 +1 -1
- package/docs/CONDUCTOR.md +22 -0
- package/docs/install/mcp-tool-integrations.md +19 -3
- package/package.json +2 -2
package/build/install-bridge.js
CHANGED
|
@@ -143,13 +143,15 @@ import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
|
|
|
143
143
|
import os from "os";
|
|
144
144
|
import path from "path";
|
|
145
145
|
import readline from "readline";
|
|
146
|
-
import { runInit, buildBridgeApiEntry, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
|
|
146
|
+
import { runInit, InitPreflightError, buildBridgeApiEntry, mergeBridgeApiProfileToken, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
|
|
147
147
|
import { hasProjectRootMarker as sharedHasProjectRootMarker } from "./project-root.js";
|
|
148
148
|
import { VERSION } from "./version.generated.js";
|
|
149
149
|
import { validateRepoName } from "./bridge-config.js";
|
|
150
|
-
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
|
|
151
|
-
import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
|
|
152
|
-
import {
|
|
150
|
+
import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, hostAdapterForTarget, } from "./mcp-host-targets.js";
|
|
151
|
+
import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
|
|
152
|
+
import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
|
|
153
|
+
import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
|
|
154
|
+
import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
|
|
153
155
|
// Executor provisioning (BAPI-779). The generator and the lifecycle module are
|
|
154
156
|
// imported DIRECTLY and driven programmatically — never by spawning
|
|
155
157
|
// `executor install-service` as a subprocess, which would lose the typed plan,
|
|
@@ -157,8 +159,10 @@ import { recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, wri
|
|
|
157
159
|
import { collectExecutorInstallPreflight, } from "./executor/install-preflight.js";
|
|
158
160
|
import { inspectExecutorServiceState, startExecutorService, } from "./executor/service-lifecycle.js";
|
|
159
161
|
import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunchdPlistPathForId, executorSystemdUnitPathForId, resolvePackagedExecutorInvocation, writeExecutorServicePlan, } from "./executor/service-unit.js";
|
|
162
|
+
import { collectConductorNativeLedgerSafe, } from "./conductor/doctor.js";
|
|
163
|
+
import { installConductorGitHooks, } from "./conductor/git-hooks.js";
|
|
160
164
|
import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
|
|
161
|
-
import { runConductorInstallDoctor, } from "./conductor/install-doctor.js";
|
|
165
|
+
import { runConductorInstallDoctor, CONDUCTOR_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
|
|
162
166
|
import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
163
167
|
import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
|
|
164
168
|
import { runSetupEpicCli } from "./setup-epic.js";
|
|
@@ -194,7 +198,7 @@ export function buildPrewarmArgs() {
|
|
|
194
198
|
// cache with the exact tarball this `@${VERSION}`-pinned warm needs, so resolve
|
|
195
199
|
// from cache and skip the redundant registry-metadata round-trip (falls back to
|
|
196
200
|
// the network on a genuine miss).
|
|
197
|
-
return ["-y", "--prefer-offline",
|
|
201
|
+
return ["-y", "--prefer-offline", `${MCP_PACKAGE_NAME}@${VERSION}`, "--version"];
|
|
198
202
|
}
|
|
199
203
|
/** Secret-free preview of the pre-warm command (no env, no key). */
|
|
200
204
|
export function buildPrewarmCommandPreview() {
|
|
@@ -232,7 +236,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
|
|
|
232
236
|
"contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
|
|
233
237
|
"and never branch on it. Keep an empty section visible with a short neutral line rather than " +
|
|
234
238
|
"dropping it, and after both sections close with a pointer to the Bridge MCP server README at " +
|
|
235
|
-
|
|
239
|
+
`https://www.npmjs.com/package/${MCP_PACKAGE_NAME} for the complete tool documentation, ` +
|
|
236
240
|
"including when either or both sections are empty. Do not render the obsolete five-section report, " +
|
|
237
241
|
"and do not locally filter, count, sort, regroup, infer availability, fabricate an item, or fall " +
|
|
238
242
|
"back to the complete tool_capabilities catalog or the workflows collection. If " +
|
|
@@ -302,7 +306,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
302
306
|
const setupUrl = buildInstallBridgeSetupUrl(baseUrl);
|
|
303
307
|
return [
|
|
304
308
|
"Usage:",
|
|
305
|
-
|
|
309
|
+
` npx -y ${MCP_PACKAGE_NAME} install [flags]`,
|
|
306
310
|
"",
|
|
307
311
|
"One-command Bridge API project bootstrap. Scaffolds the project, writes the",
|
|
308
312
|
"per-host MCP config with your credentials, verifies connectivity, persists the",
|
|
@@ -502,8 +506,17 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
|
|
|
502
506
|
" which writes the safe project-default supervisor",
|
|
503
507
|
" posture in one transaction; it optionally",
|
|
504
508
|
" scaffolds a parameterized claude-review workflow",
|
|
505
|
-
" (its own separate consent),
|
|
506
|
-
"
|
|
509
|
+
" (its own separate consent), then closes the local",
|
|
510
|
+
" out-of-the-box gap in two more phases, each behind",
|
|
511
|
+
" its OWN consent: tool visibility adds the",
|
|
512
|
+
" `conductor` token to existing .mcp.json /",
|
|
513
|
+
" .vscode/mcp.json / .cursor/mcp.json entries (never",
|
|
514
|
+
" Codex or Copilot, and never creating a file) and",
|
|
515
|
+
" prints the MCP-client restart advisory; local",
|
|
516
|
+
" observability installs the managed conductor Git",
|
|
517
|
+
" hooks and then reports native ledger loadability",
|
|
518
|
+
" read-only. It finally prints the capability",
|
|
519
|
+
" matrix and re-runs the doctor. `--dry-run`",
|
|
507
520
|
" performs NO write of any kind and no write-consent",
|
|
508
521
|
" prompt (the base-URL confirmation still applies).",
|
|
509
522
|
" Executor service units are generated by",
|
|
@@ -770,7 +783,7 @@ export function promptSecretViaReadline(promptText, input = process.stdin, outpu
|
|
|
770
783
|
* The deferral command printed with the GitHub offer (BAPI-669, U6). Declining is
|
|
771
784
|
* cheap precisely because this exists — the connection is a standalone command.
|
|
772
785
|
*/
|
|
773
|
-
export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND =
|
|
786
|
+
export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND = `npx -y ${MCP_PACKAGE_NAME} connect-github`;
|
|
774
787
|
/**
|
|
775
788
|
* The `Step 4b` label + purpose + deferral lines printed immediately before the
|
|
776
789
|
* GitHub prompt (BAPI-669, U6). Shared with {@link buildLaunchStepPreview} so the
|
|
@@ -845,13 +858,13 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log) {
|
|
|
845
858
|
const code = await runGithubConnectionFlow(connectDeps, api, repoName);
|
|
846
859
|
if (code !== 0) {
|
|
847
860
|
log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
|
|
848
|
-
`'npx -y @
|
|
861
|
+
`'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
|
|
849
862
|
}
|
|
850
863
|
}
|
|
851
864
|
catch {
|
|
852
865
|
// The install is already durable; a failure here is never fatal to it.
|
|
853
866
|
log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
|
|
854
|
-
`GitHub later with 'npx -y @
|
|
867
|
+
`GitHub later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
|
|
855
868
|
}
|
|
856
869
|
}
|
|
857
870
|
/**
|
|
@@ -995,6 +1008,12 @@ export function createDefaultInstallBridgeDeps() {
|
|
|
995
1008
|
fetch: params.fetch,
|
|
996
1009
|
reviewPolicySource: params.reviewPolicySource,
|
|
997
1010
|
readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
|
|
1011
|
+
// BAPI-775: the SAME project root the rest of installation resolves
|
|
1012
|
+
// against, and the SAME read-only host-config inspector the
|
|
1013
|
+
// tool-visibility phase reports from — so the doctor's profile-token
|
|
1014
|
+
// section and the phase can never disagree about what is on disk.
|
|
1015
|
+
projectRoot: params.cwd,
|
|
1016
|
+
inspectProfileToken: (projectRoot, token) => inspectBridgeApiProfileToken(projectRoot, token, { readFile: params.readFile }),
|
|
998
1017
|
// Read-only service-state probe (BAPI-779). Typed to return a state,
|
|
999
1018
|
// never a runner, so the doctor cannot start/enable/reload anything.
|
|
1000
1019
|
inspectExecutorServiceState: params.inspectExecutorServiceState,
|
|
@@ -1141,6 +1160,46 @@ function buildConductorExecutorDeps(deps, baseUrl) {
|
|
|
1141
1160
|
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
1142
1161
|
};
|
|
1143
1162
|
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Compose the local out-of-the-box provisioning bundle the two BAPI-775 phases
|
|
1165
|
+
* run on, from the FINAL merged deps.
|
|
1166
|
+
*
|
|
1167
|
+
* Same shape and same rationale as {@link buildConductorExecutorDeps}: every
|
|
1168
|
+
* entry is a thin adapter over an existing implementation, and nothing here
|
|
1169
|
+
* decides anything. Consent, ordering, degradation, and reporting live in
|
|
1170
|
+
* `install-bridge-conductor.ts`.
|
|
1171
|
+
*
|
|
1172
|
+
* `mergeBridgeApiProfileToken` is called with the resolved project root and the
|
|
1173
|
+
* conductor token, reusing the helper byte-for-byte: its idempotent token-set
|
|
1174
|
+
* merge and its refusal to create a missing host config are load-bearing, and
|
|
1175
|
+
* reimplementing either here would fork the only writer of
|
|
1176
|
+
* `BRIDGE_MCP_PROFILE`.
|
|
1177
|
+
*/
|
|
1178
|
+
function buildConductorLocalDeps(deps) {
|
|
1179
|
+
return {
|
|
1180
|
+
mergeProfileToken: () => mergeBridgeApiProfileToken(deps.cwd, CONDUCTOR_PROFILE_TOKEN),
|
|
1181
|
+
inspectProfileToken: () => inspectBridgeApiProfileToken(deps.cwd, CONDUCTOR_PROFILE_TOKEN, {
|
|
1182
|
+
readFile: deps.readFile,
|
|
1183
|
+
}),
|
|
1184
|
+
// The repository root is this command's cwd, which is what the hook
|
|
1185
|
+
// installer resolves its worktree and hooks directory from.
|
|
1186
|
+
installGitHooks: async () => installConductorGitHooks({ cwd: deps.cwd }),
|
|
1187
|
+
// The SHARED collector (BAPI-775), so the phase's ledger verdict and the
|
|
1188
|
+
// conductor doctor's cannot diverge. Its safe variant contains any
|
|
1189
|
+
// collection failure as a generic degraded inspection.
|
|
1190
|
+
probeNativeLedger: () => collectConductorNativeLedgerSafe(),
|
|
1191
|
+
recordCapability: async (capability) => {
|
|
1192
|
+
const result = await recordInstalledConductorCapability(deps.cwd, capability, {
|
|
1193
|
+
readFile: deps.readFile,
|
|
1194
|
+
writeFile: (p, data) => deps.writeFile(p, data),
|
|
1195
|
+
rename: deps.rename,
|
|
1196
|
+
mkdir: (p, o) => deps.mkdir(p, o),
|
|
1197
|
+
unlink: deps.unlink,
|
|
1198
|
+
});
|
|
1199
|
+
return result.ok ? { ok: true } : { ok: false, error: result.error };
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1144
1203
|
/**
|
|
1145
1204
|
* Read-only local service-state inspector for the unified doctor (BAPI-779).
|
|
1146
1205
|
*
|
|
@@ -1928,8 +1987,8 @@ function hostConfigTargetsForPlatforms(platforms) {
|
|
|
1928
1987
|
const set = new Set(platforms);
|
|
1929
1988
|
return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
|
|
1930
1989
|
.map((id) => MCP_HOST_TARGETS[id])
|
|
1931
|
-
.filter((t) => t.scope === "project" && t.format === "json")
|
|
1932
|
-
.map((t) => ({ relPath: t.relPath, topLevelKey: t.topLevelKey }));
|
|
1990
|
+
.filter((t) => t.scope === "project" && hostAdapterForTarget(t).format === "json")
|
|
1991
|
+
.map((t) => ({ relPath: t.relPath, topLevelKey: hostAdapterForTarget(t).topLevelKey }));
|
|
1933
1992
|
}
|
|
1934
1993
|
/**
|
|
1935
1994
|
* Derive the human-facing labels for a set of selected platforms, in registry
|
|
@@ -1942,11 +2001,13 @@ export function labelsForPlatforms(platforms) {
|
|
|
1942
2001
|
const set = new Set(platforms);
|
|
1943
2002
|
return HOST_PLATFORM_ORDER.filter((id) => set.has(id)).map((id) => MCP_HOST_TARGETS[id].label);
|
|
1944
2003
|
}
|
|
2004
|
+
/** Project-local host-config metadata sourced from the shared registry/adapter. */
|
|
2005
|
+
function toHostConfigTarget(target) {
|
|
2006
|
+
return { relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
|
|
2007
|
+
}
|
|
1945
2008
|
/** Resolve which project-local host configs to write, mirroring runInit detection. */
|
|
1946
2009
|
async function resolveHostConfigTargets(deps) {
|
|
1947
|
-
const targets = [
|
|
1948
|
-
{ relPath: ".mcp.json", topLevelKey: "mcpServers" },
|
|
1949
|
-
];
|
|
2010
|
+
const targets = [toHostConfigTarget(MCP_HOST_TARGETS["claude-code"])];
|
|
1950
2011
|
const dirExists = async (rel) => {
|
|
1951
2012
|
try {
|
|
1952
2013
|
await deps.stat(path.join(deps.cwd, rel));
|
|
@@ -1957,10 +2018,10 @@ async function resolveHostConfigTargets(deps) {
|
|
|
1957
2018
|
}
|
|
1958
2019
|
};
|
|
1959
2020
|
if (await dirExists(".vscode")) {
|
|
1960
|
-
targets.push(
|
|
2021
|
+
targets.push(toHostConfigTarget(MCP_HOST_TARGETS["copilot-vscode"]));
|
|
1961
2022
|
}
|
|
1962
2023
|
if ((await dirExists(".cursor")) || deps.env.CURSOR_TRACE_DIR) {
|
|
1963
|
-
targets.push(
|
|
2024
|
+
targets.push(toHostConfigTarget(MCP_HOST_TARGETS.cursor));
|
|
1964
2025
|
}
|
|
1965
2026
|
return targets;
|
|
1966
2027
|
}
|
|
@@ -2021,7 +2082,7 @@ export function buildInstallBridgeSecretFreeServerEntry(cwd, repoName, baseUrl,
|
|
|
2021
2082
|
*/
|
|
2022
2083
|
function formatCredentialStoreGuidance(repoName, credentialStorePath) {
|
|
2023
2084
|
return [
|
|
2024
|
-
|
|
2085
|
+
` The ${MCP_SERVER_NAME} server resolves your key at runtime from the BAPI_API_KEY environment`,
|
|
2025
2086
|
` variable or the user-scoped credential store (${credentialStorePath}, target`,
|
|
2026
2087
|
` bapi:${repoName}), so it stays out of this file entirely.`,
|
|
2027
2088
|
];
|
|
@@ -2034,7 +2095,7 @@ function formatCredentialStoreGuidance(repoName, credentialStorePath) {
|
|
|
2034
2095
|
* existing file — never for replacing it.
|
|
2035
2096
|
*/
|
|
2036
2097
|
function formatSecretFreeManualMerge(relPath, topLevelKey, secretFreeEntry) {
|
|
2037
|
-
const snippet = JSON.stringify({ [topLevelKey]: {
|
|
2098
|
+
const snippet = JSON.stringify({ [topLevelKey]: { [MCP_SERVER_NAME]: secretFreeEntry } }, null, 2);
|
|
2038
2099
|
return [
|
|
2039
2100
|
` To configure ${relPath} by hand, MERGE this secret-free entry into the existing`,
|
|
2040
2101
|
" file (do not replace the file):",
|
|
@@ -2102,7 +2163,16 @@ async function detectExistingRealKey(deps, targets) {
|
|
|
2102
2163
|
if (result.state === "absent")
|
|
2103
2164
|
continue;
|
|
2104
2165
|
const topLevel = asRecord(result.config[target.topLevelKey]);
|
|
2105
|
-
|
|
2166
|
+
// BAPI-807: a real key under a legacy `bridge-api` entry is just as much a
|
|
2167
|
+
// reason to ask for overwrite consent as one under `bridge`. A both-key
|
|
2168
|
+
// config is conservatively treated as possibly key-bearing for the same
|
|
2169
|
+
// reason `invalid` is.
|
|
2170
|
+
const resolution = resolveRegistrationKey(topLevel);
|
|
2171
|
+
if (resolution.state === "conflict")
|
|
2172
|
+
return true;
|
|
2173
|
+
if (resolution.state === "absent")
|
|
2174
|
+
continue;
|
|
2175
|
+
const entry = asRecord(topLevel?.[resolution.key]);
|
|
2106
2176
|
const env = asRecord(entry?.["env"]);
|
|
2107
2177
|
if (env && !isPlaceholderApiKey(env["BAPI_API_KEY"])) {
|
|
2108
2178
|
return true;
|
|
@@ -2175,6 +2245,28 @@ export function preserveExistingLauncherArgs(existing, entry) {
|
|
|
2175
2245
|
const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
|
|
2176
2246
|
return preserved ? { ...entry, args: preserved } : entry;
|
|
2177
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Read-only pre-scan for both-key configs across EVERY project target (BAPI-807).
|
|
2250
|
+
*
|
|
2251
|
+
* Run before the registration phase begins writing, so a conflict discovered in
|
|
2252
|
+
* `.cursor/mcp.json` cannot leave `.mcp.json` and `.vscode/mcp.json` already
|
|
2253
|
+
* rewritten. A target that is absent or unparseable contributes no conflict —
|
|
2254
|
+
* those are handled by the existing per-target branches, which have their own
|
|
2255
|
+
* non-fatal semantics.
|
|
2256
|
+
*/
|
|
2257
|
+
async function detectDuplicateRegistrations(deps, targets) {
|
|
2258
|
+
const conflicts = [];
|
|
2259
|
+
for (const target of targets) {
|
|
2260
|
+
const read = await readHostConfig(deps, path.join(deps.cwd, target.relPath));
|
|
2261
|
+
if (read.state !== "parsed")
|
|
2262
|
+
continue;
|
|
2263
|
+
const topLevel = asRecord(read.config[target.topLevelKey]);
|
|
2264
|
+
if (resolveRegistrationKey(topLevel).state === "conflict") {
|
|
2265
|
+
conflicts.push(target.relPath);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return conflicts;
|
|
2269
|
+
}
|
|
2178
2270
|
/**
|
|
2179
2271
|
* Write the `bridge-api` entry into each host config via read-merge-write,
|
|
2180
2272
|
* preserving unrelated servers and top-level keys (BAPI-666). Per target:
|
|
@@ -2192,6 +2284,13 @@ export function preserveExistingLauncherArgs(existing, entry) {
|
|
|
2192
2284
|
async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
2193
2285
|
const written = [];
|
|
2194
2286
|
const skipped = [];
|
|
2287
|
+
// BAPI-807: detect both-key configs across ALL targets BEFORE the first write.
|
|
2288
|
+
// A multi-target registration phase that discovered the conflict on its third
|
|
2289
|
+
// target would already have rewritten the first two.
|
|
2290
|
+
const duplicateRegistrations = await detectDuplicateRegistrations(deps, targets);
|
|
2291
|
+
if (duplicateRegistrations.length > 0) {
|
|
2292
|
+
return { written, skipped, duplicateRegistrations };
|
|
2293
|
+
}
|
|
2195
2294
|
for (const target of targets) {
|
|
2196
2295
|
const fullPath = path.join(deps.cwd, target.relPath);
|
|
2197
2296
|
const read = await readHostConfig(deps, fullPath);
|
|
@@ -2235,21 +2334,33 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
2235
2334
|
// never initialized before its tracked-state consent is resolved.
|
|
2236
2335
|
const config = read.state === "parsed" ? read.config : {};
|
|
2237
2336
|
const topLevel = asRecord(config[target.topLevelKey]) ?? {};
|
|
2337
|
+
// BAPI-807: resolve WHICH key this config's registration lives under before
|
|
2338
|
+
// writing. A legacy-only entry is replaced under `bridge-api`, a canonical-only
|
|
2339
|
+
// entry under `bridge`, and an absent one is created under MCP_SERVER_NAME.
|
|
2340
|
+
const resolution = resolveRegistrationKey(topLevel);
|
|
2341
|
+
if (resolution.state === "conflict") {
|
|
2342
|
+
// Unreachable: the pre-scan above returned before any write when it found a
|
|
2343
|
+
// conflict. Re-checked rather than asserted, so a future caller that skips
|
|
2344
|
+
// the pre-scan still cannot write over an ambiguous registration.
|
|
2345
|
+
skipped.push({ relPath: target.relPath, reason: "invalid" });
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const registrationKey = resolution.state === "absent" ? resolution.writeKey : resolution.key;
|
|
2238
2349
|
// BAPI-714 (Group C): a PRE-EXISTING Bridge entry keeps its own launcher args
|
|
2239
2350
|
// composition — only the package-spec token is refreshed — so the installer
|
|
2240
2351
|
// never migrates a legacy bare launcher to `serve`. A newly created entry gets
|
|
2241
2352
|
// the full template args, `serve` included. The rule is identical for the
|
|
2242
2353
|
// real-key and secret-free variants: credential safety must not decide whether
|
|
2243
2354
|
// a launcher is migrated.
|
|
2244
|
-
topLevel[
|
|
2355
|
+
topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], entry);
|
|
2245
2356
|
config[target.topLevelKey] = topLevel;
|
|
2246
2357
|
await deps.mkdir(path.dirname(fullPath), { recursive: true });
|
|
2247
2358
|
await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
|
|
2248
2359
|
encoding: "utf-8",
|
|
2249
2360
|
});
|
|
2250
|
-
written.push({ relPath: target.relPath, mode });
|
|
2361
|
+
written.push({ relPath: target.relPath, mode, registrationKey });
|
|
2251
2362
|
}
|
|
2252
|
-
return { written, skipped };
|
|
2363
|
+
return { written, skipped, duplicateRegistrations: [] };
|
|
2253
2364
|
}
|
|
2254
2365
|
/**
|
|
2255
2366
|
* Provision the selected GLOBAL (Codex, Copilot CLI) and MANUAL (Windsurf)
|
|
@@ -2266,6 +2377,7 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
|
|
|
2266
2377
|
*/
|
|
2267
2378
|
async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
2268
2379
|
const logLines = [];
|
|
2380
|
+
const duplicateRegistrations = [];
|
|
2269
2381
|
let anyManualRequired = false;
|
|
2270
2382
|
const provisionDeps = {
|
|
2271
2383
|
fs: {
|
|
@@ -2286,7 +2398,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
|
2286
2398
|
continue;
|
|
2287
2399
|
const target = MCP_HOST_TARGETS[id];
|
|
2288
2400
|
// Skip project JSON targets — those are handled by writeHostConfigs.
|
|
2289
|
-
if (target.scope === "project" && target.format === "json")
|
|
2401
|
+
if (target.scope === "project" && hostAdapterForTarget(target).format === "json")
|
|
2290
2402
|
continue;
|
|
2291
2403
|
const outcome = await provisionHostTarget(target, entry, provisionDeps);
|
|
2292
2404
|
switch (outcome.status) {
|
|
@@ -2297,12 +2409,19 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
|
2297
2409
|
break;
|
|
2298
2410
|
case "manual-required":
|
|
2299
2411
|
anyManualRequired = true;
|
|
2300
|
-
logLines.push(` ${target.label}: add the
|
|
2412
|
+
logLines.push(` ${target.label}: add the ${MCP_SERVER_NAME} MCP server manually to ${outcome.displayPath} ` +
|
|
2301
2413
|
"(the API key is redacted in printed instructions).");
|
|
2302
2414
|
break;
|
|
2303
2415
|
case "skipped-invalid":
|
|
2304
2416
|
logLines.push(` ${target.label}: skipped ${outcome.displayPath} — existing config is not valid; left untouched.`);
|
|
2305
2417
|
break;
|
|
2418
|
+
case "skipped-duplicate-registration":
|
|
2419
|
+
// BAPI-807: NOT an ordinary vendor-fallback condition. The target was
|
|
2420
|
+
// left byte-identical and the caller must fail loudly rather than report
|
|
2421
|
+
// a configured host.
|
|
2422
|
+
duplicateRegistrations.push(target.displayPath);
|
|
2423
|
+
logLines.push(` ${target.label}: skipped ${outcome.displayPath} — ${outcome.detail}`);
|
|
2424
|
+
break;
|
|
2306
2425
|
case "failed":
|
|
2307
2426
|
logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);
|
|
2308
2427
|
break;
|
|
@@ -2311,7 +2430,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
|
|
|
2311
2430
|
if (needKey && anyManualRequired) {
|
|
2312
2431
|
logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName, needKey.credentialStorePath)}`);
|
|
2313
2432
|
}
|
|
2314
|
-
return logLines;
|
|
2433
|
+
return { logLines, duplicateRegistrations };
|
|
2315
2434
|
}
|
|
2316
2435
|
/** Build the `/jira/ping` URL exactly like the MCP `ping` tool / buildGetUrl. */
|
|
2317
2436
|
export function buildPingUrl(baseUrl, repoName) {
|
|
@@ -2842,7 +2961,7 @@ export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
|
|
|
2842
2961
|
"Signup now verifies your email address with a code before creating a workspace. Upgrade",
|
|
2843
2962
|
"and re-run:",
|
|
2844
2963
|
"",
|
|
2845
|
-
|
|
2964
|
+
` npx -y ${MCP_PACKAGE_NAME}@latest install --email <addr>`,
|
|
2846
2965
|
].join("\n");
|
|
2847
2966
|
/**
|
|
2848
2967
|
* Mask an address for display: first character, then the domain.
|
|
@@ -3619,11 +3738,11 @@ function buildManualHostInstructions(entry, editors, needKey) {
|
|
|
3619
3738
|
"Add the server manually (replace <REDACTED> with your key):",
|
|
3620
3739
|
];
|
|
3621
3740
|
if (editors.windsurf) {
|
|
3622
|
-
const windsurfSnippet = JSON.stringify({ mcpServers: {
|
|
3741
|
+
const windsurfSnippet = JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: entry.command, args: entry.args, env: redactedEnv } } }, null, 2);
|
|
3623
3742
|
lines.push("", " Windsurf → ~/.codeium/windsurf/mcp_config.json:", windsurfSnippet);
|
|
3624
3743
|
}
|
|
3625
3744
|
if (editors.codex) {
|
|
3626
|
-
lines.push("",
|
|
3745
|
+
lines.push("", ` Codex → ~/.codex/config.toml (add an [mcp_servers.${MCP_SERVER_NAME}] table with the`, " same command/args/env shown above, BAPI_API_KEY set to your key).");
|
|
3627
3746
|
}
|
|
3628
3747
|
if (needKey) {
|
|
3629
3748
|
lines.push("", ` ${formatNeedKeyCredentialStoreLine(needKey.repoName, needKey.credentialStorePath)}`);
|
|
@@ -3638,7 +3757,7 @@ function buildManualHostInstructions(entry, editors, needKey) {
|
|
|
3638
3757
|
* ONCE and reused by the Step 1–5 catch, the `index.ts` dispatch catch, the
|
|
3639
3758
|
* tests, and the feature documentation, so the command text cannot drift.
|
|
3640
3759
|
*/
|
|
3641
|
-
export const INSTALL_BRIDGE_DOCTOR_COMMAND =
|
|
3760
|
+
export const INSTALL_BRIDGE_DOCTOR_COMMAND = `npx -y ${MCP_PACKAGE_NAME} doctor`;
|
|
3642
3761
|
/**
|
|
3643
3762
|
* The canonical pointer LINE (BAPI-669, U9a). One syntax — no quotes, no alternate
|
|
3644
3763
|
* punctuation — shared by the Step 1–5 catch and by every operational fatal in the
|
|
@@ -3980,6 +4099,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
3980
4099
|
// seams above are: a test that overrides only `fetch` or only `homedir`
|
|
3981
4100
|
// must see its override here, not a stale default captured earlier.
|
|
3982
4101
|
executorProvisioning: buildConductorExecutorDeps(deps, conductorBaseUrl),
|
|
4102
|
+
// Built from the FINAL merged deps for the same reason (BAPI-775): a test
|
|
4103
|
+
// that overrides only `readFile` or only `cwd` must see its override here.
|
|
4104
|
+
localProvisioning: buildConductorLocalDeps(deps),
|
|
3983
4105
|
};
|
|
3984
4106
|
return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
|
|
3985
4107
|
}
|
|
@@ -5072,6 +5194,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5072
5194
|
trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
|
|
5073
5195
|
}
|
|
5074
5196
|
const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
|
|
5197
|
+
// BAPI-807: a both-key project config is a FAIL-LOUD condition, not an
|
|
5198
|
+
// ordinary skip. Nothing was written (the pre-write scan returned before the
|
|
5199
|
+
// first target), and continuing would report a successful install over a
|
|
5200
|
+
// registration Bridge deliberately refused to touch.
|
|
5201
|
+
if (writeResult.duplicateRegistrations.length > 0) {
|
|
5202
|
+
errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);
|
|
5203
|
+
for (const relPath of writeResult.duplicateRegistrations) {
|
|
5204
|
+
errorLog(` ${relPath}`);
|
|
5205
|
+
}
|
|
5206
|
+
errorLog(" Nothing was written. Re-run install-bridge once one entry is removed.");
|
|
5207
|
+
return 1;
|
|
5208
|
+
}
|
|
5075
5209
|
// BAPI-708 (B-a): these files are the MCP SERVER REGISTRATION (BAPI_REPO_NAME,
|
|
5076
5210
|
// BAPI_BASE_URL, BAPI_DOCS_DIR, BAPI_PROJECT_ROOT, and the version-pinned npx
|
|
5077
5211
|
// launcher) — calling them a credentials file was simply inaccurate. Where the
|
|
@@ -5094,9 +5228,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5094
5228
|
const needKeyManual = bootstrapInviteMode
|
|
5095
5229
|
? { repoName, credentialStorePath }
|
|
5096
5230
|
: undefined;
|
|
5097
|
-
const
|
|
5098
|
-
for (const line of
|
|
5231
|
+
const globalProvisioning = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry, needKeyManual);
|
|
5232
|
+
for (const line of globalProvisioning.logLines)
|
|
5099
5233
|
log(line);
|
|
5234
|
+
// BAPI-807: a global target that refused because it already carries both
|
|
5235
|
+
// registrations is fail-loud, exactly like the project-config case above —
|
|
5236
|
+
// never an ordinary vendor fallback the run can shrug off.
|
|
5237
|
+
if (globalProvisioning.duplicateRegistrations.length > 0) {
|
|
5238
|
+
errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);
|
|
5239
|
+
for (const displayPath of globalProvisioning.duplicateRegistrations) {
|
|
5240
|
+
errorLog(` ${displayPath}`);
|
|
5241
|
+
}
|
|
5242
|
+
errorLog(" That host was left untouched. Re-run install-bridge once one entry is removed.");
|
|
5243
|
+
return 1;
|
|
5244
|
+
}
|
|
5100
5245
|
// Legacy manual-editor instructions cover editors that are DETECTED but were
|
|
5101
5246
|
// NOT part of the selection (so the emitter above did not handle them). The two
|
|
5102
5247
|
// editors are suppressed INDEPENDENTLY: Codex is dropped when it was selected
|
|
@@ -5187,7 +5332,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5187
5332
|
`start-tickets model routing may not resolve the key for bapi:${repoName} ` +
|
|
5188
5333
|
"and will fail open to the premium/Opus tier (the most expensive) — " +
|
|
5189
5334
|
"set BAPI_API_KEY in the shell or re-run install, then verify with " +
|
|
5190
|
-
|
|
5335
|
+
`'npx -y ${MCP_PACKAGE_NAME} doctor'.`);
|
|
5191
5336
|
}
|
|
5192
5337
|
}
|
|
5193
5338
|
catch {
|
|
@@ -5196,7 +5341,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5196
5341
|
log(" warning: could not persist the routing credential (unexpected error). " +
|
|
5197
5342
|
"start-tickets model routing may need BAPI_API_KEY in the shell and will fail open " +
|
|
5198
5343
|
"to the premium/Opus tier (the most expensive) until fixed — verify with " +
|
|
5199
|
-
|
|
5344
|
+
`'npx -y ${MCP_PACKAGE_NAME} doctor'.`);
|
|
5200
5345
|
}
|
|
5201
5346
|
}
|
|
5202
5347
|
// ---- commit-your-assets notice (BAPI-708, Part C) ----
|
|
@@ -5280,7 +5425,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5280
5425
|
"server provides.");
|
|
5281
5426
|
// The trust dialog is the single most common place a spawned session stalls:
|
|
5282
5427
|
// the tab opens, nothing is approved, and the install looks hung.
|
|
5283
|
-
log(`In the new tab: approve the workspace and the '
|
|
5428
|
+
log(`In the new tab: approve the workspace and the '${MCP_SERVER_NAME}' MCP server if ${handoffToolLabel} ` +
|
|
5284
5429
|
"asks — configuration can't proceed until you do.");
|
|
5285
5430
|
// Recovery was previously printed ONLY on no-spawn paths, so a tab that opened
|
|
5286
5431
|
// and then died left the user with no instruction at all.
|
|
@@ -5304,6 +5449,25 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
|
|
|
5304
5449
|
return 0;
|
|
5305
5450
|
}
|
|
5306
5451
|
catch (error) {
|
|
5452
|
+
// BAPI-807: a launcher-config PREFLIGHT refusal is actionable, and the
|
|
5453
|
+
// operator must be told what to do — collapsing it into the generic
|
|
5454
|
+
// "unexpected error" cause is how a duplicate registration would look like
|
|
5455
|
+
// a bug in Bridge instead of two entries in their own config.
|
|
5456
|
+
//
|
|
5457
|
+
// This is safe under the module's secret-suppression discipline because
|
|
5458
|
+
// `InitPreflightError` is a TYPE this codebase owns, and its payload is a
|
|
5459
|
+
// closed set: a repo-relative config path plus a reason string drawn from
|
|
5460
|
+
// the fixed `REASON_TEXT` table. No caught free-form message, no config
|
|
5461
|
+
// body, and no env value is read here.
|
|
5462
|
+
if (error instanceof InitPreflightError) {
|
|
5463
|
+
errorLog("Error: these MCP launcher configs cannot be safely reconciled:");
|
|
5464
|
+
for (const failure of error.failures) {
|
|
5465
|
+
errorLog(` ${failure.relPath}: ${failure.reason}`);
|
|
5466
|
+
}
|
|
5467
|
+
errorLog(" Nothing was changed. Resolve these by hand, then re-run install-bridge.");
|
|
5468
|
+
errorLog(INSTALL_BRIDGE_DOCTOR_POINTER);
|
|
5469
|
+
return 1;
|
|
5470
|
+
}
|
|
5307
5471
|
// Resume guidance ONLY after the exchange actually succeeded (R5-2): before
|
|
5308
5472
|
// that the invite is unspent and there is nothing to resume. The wording comes
|
|
5309
5473
|
// from the mode-aware helper, never a fresh literal, so a self-serve user is
|
package/build/install-doctor.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
import path from "path";
|
|
23
23
|
import { resolveBapiCredentials } from "./credential-store.js";
|
|
24
24
|
import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
|
|
25
|
+
import { getProjectJsonTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
|
|
26
|
+
import { existingRegistrationKey, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
|
|
25
27
|
/** Default production base URL (mirrors install-bridge's DEFAULT_BAPI_BASE_URL). */
|
|
26
28
|
const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
27
29
|
/**
|
|
@@ -36,12 +38,15 @@ const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
|
36
38
|
function buildSetupUrl(baseUrl) {
|
|
37
39
|
return `${baseUrl.replace(/\/+$/, "")}/setup`;
|
|
38
40
|
}
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Project-local MCP configs whose env blocks may carry repo/base-URL values.
|
|
43
|
+
* Derived from the shared host-target registry's project-scoped JSON targets
|
|
44
|
+
* rather than duplicating each path/root-key pair here.
|
|
45
|
+
*/
|
|
46
|
+
const MCP_CONFIG_ENV_TARGETS = getProjectJsonTargets().map((target) => ({
|
|
47
|
+
relPath: target.relPath,
|
|
48
|
+
topLevelKey: hostAdapterForTarget(target).topLevelKey,
|
|
49
|
+
}));
|
|
45
50
|
/** Timeout for each read-only probe request. */
|
|
46
51
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
47
52
|
/**
|
|
@@ -72,9 +77,18 @@ export async function resolveInstallDoctorTarget(deps) {
|
|
|
72
77
|
catch {
|
|
73
78
|
continue;
|
|
74
79
|
}
|
|
75
|
-
|
|
80
|
+
// BAPI-807: read the env block through the RESOLVED registration key. A
|
|
81
|
+
// config written before the rename registers `bridge-api`, and looking
|
|
82
|
+
// only under the canonical name would report a perfectly working install
|
|
83
|
+
// as having no repo/base-URL configured at all.
|
|
84
|
+
const servers = parsed && typeof parsed === "object"
|
|
85
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
parsed[topLevelKey]
|
|
87
|
+
: undefined;
|
|
88
|
+
const registrationKey = existingRegistrationKey(servers);
|
|
89
|
+
const envBlock = registrationKey !== null
|
|
76
90
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
-
|
|
91
|
+
servers[registrationKey]?.env
|
|
78
92
|
: undefined;
|
|
79
93
|
if (!envBlock)
|
|
80
94
|
continue;
|
|
@@ -410,7 +424,7 @@ export async function collectInstallStatusChecks(deps) {
|
|
|
410
424
|
label: "GitHub connection",
|
|
411
425
|
status: "WARN",
|
|
412
426
|
detail: "no GitHub repository is connected to this project",
|
|
413
|
-
remediation: `run 'npx -y @
|
|
427
|
+
remediation: `run 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${target.repoName}'.`,
|
|
414
428
|
});
|
|
415
429
|
}
|
|
416
430
|
}
|
package/build/install-reexec.js
CHANGED
|
@@ -40,6 +40,7 @@ import { VERSION } from "./version.generated.js";
|
|
|
40
40
|
import { isNewerVersion } from "./update-check.js";
|
|
41
41
|
import { fetchLatestVersion } from "./cli-release.js";
|
|
42
42
|
import { runInstallBridgeCli } from "./install-bridge.js";
|
|
43
|
+
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
43
44
|
/** The sentinel that marks an already-re-exec'd child. Matches `upgrade-cli.ts`. */
|
|
44
45
|
export const INSTALL_REEXEC_SENTINEL = "--internal-reexec";
|
|
45
46
|
/**
|
|
@@ -189,7 +190,7 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
|
|
|
189
190
|
const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
|
|
190
191
|
const childArgs = [
|
|
191
192
|
"-y",
|
|
192
|
-
|
|
193
|
+
`${MCP_PACKAGE_NAME}@latest`,
|
|
193
194
|
"install",
|
|
194
195
|
INSTALL_REEXEC_SENTINEL,
|
|
195
196
|
...forwardedArgs,
|