@bridge_gpt/mcp-server 0.2.68 → 0.2.69

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.
@@ -22,20 +22,16 @@
22
22
  * carries config bodies, `env` maps, credential values, or raw error text.
23
23
  */
24
24
  import path from "path";
25
- import { getProjectJsonTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
26
- import { LEGACY_SERVER_NAMES, MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
25
+ import { codexHome, isCodexHomeOverrideHonored } from "./codex-home.js";
26
+ import { inspectCodexLauncher } from "./codex-launcher-pin.js";
27
+ import { getProjectJsonTargets, hostAdapterForTarget, MCP_HOST_TARGETS, } from "./mcp-host-targets.js";
28
+ import { LEGACY_SERVER_NAMES, MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, WORKTREE_SHIM_TOKEN, } from "./mcp-identity.js";
27
29
  /**
28
30
  * The npm package every Bridge launcher spec names. Retained as a compatibility
29
31
  * alias — `doctor.ts` still imports this name — assigned from the shared
30
32
  * constant so there is no duplicate package-name literal.
31
33
  */
32
34
  export const BRIDGE_PACKAGE_NAME = MCP_PACKAGE_NAME;
33
- /**
34
- * The subcommand token that marks a worktree shim registration. A shim points at
35
- * a sibling worktree's server rather than a published package, so no pin applies
36
- * and no rewrite is ever safe (BAPI-714 / worktree provisioning).
37
- */
38
- const WORKTREE_SHIM_TOKEN = "mcp-invoke";
39
35
  /**
40
36
  * Every project-local MCP config Bridge registers into. Exported so `doctor`,
41
37
  * `init`, and `upgrade` can no longer drift apart on which paths exist or which
@@ -155,6 +151,10 @@ const REASON_TEXT = {
155
151
  "malformed-version": "the launcher pin is not a valid MAJOR.MINOR.PATCH release",
156
152
  "pinned-ahead-of-target": "the launcher pin is newer than the target version and must not be downgraded",
157
153
  "duplicate-registration": DUPLICATE_REGISTRATION_GUIDANCE,
154
+ // BAPI-1445: Codex's global TOML is read by a conservative scanner that refuses
155
+ // anything it cannot interpret with confidence. The wording is fixed so no file
156
+ // content, argument, or environment value can ever reach a diagnostic.
157
+ unlocatable: "the Codex Bridge launcher cannot be located safely; resolve its configuration manually, then re-run the upgrade",
158
158
  };
159
159
  /** Render a reason code as sanitized prose. */
160
160
  export function describeLauncherReason(reason) {
@@ -351,31 +351,38 @@ async function inspectOne(target, options) {
351
351
  reason: "no-package-token",
352
352
  };
353
353
  }
354
- const common = {
354
+ return {
355
355
  ...keyed,
356
356
  filePresent: true,
357
357
  bridgeEntryPresent: true,
358
358
  shape: "standard",
359
359
  spec: pin.spec,
360
+ ...classifyStandardPin(pin, targetVersion),
360
361
  };
362
+ }
363
+ /**
364
+ * Classify a parsed Bridge package token against the target version.
365
+ *
366
+ * Shared by the project JSON inspection and the global Codex inspection so a pin
367
+ * means the same thing wherever it was written. Order matters and is part of the
368
+ * contract: unpinned forms normalize, a non-exact specifier is refused, a
369
+ * non-release target is refused, a pin ahead of the target is never downgraded,
370
+ * and only then is the exact pin compared.
371
+ */
372
+ function classifyStandardPin(pin, targetVersion) {
361
373
  // Bare and explicit `@latest` are the two forms an exact-pin policy normalizes
362
374
  // rather than rejects — both mean "whatever npx resolves", which is exactly the
363
375
  // non-determinism BAPI-429 pinned away from.
364
376
  if (pin.specifier === null || pin.specifier === "latest") {
365
- return { ...common, action: "normalize-unpinned" };
377
+ return { action: "normalize-unpinned" };
366
378
  }
367
379
  if (!isExactVersion(pin.specifier)) {
368
- return {
369
- ...common,
370
- action: "unsupported",
371
- reason: classifyNonExactSpecifier(pin.specifier),
372
- };
380
+ return { action: "unsupported", reason: classifyNonExactSpecifier(pin.specifier) };
373
381
  }
374
382
  const comparison = compareExactVersions(pin.specifier, targetVersion);
375
383
  if (comparison === null) {
376
384
  // The TARGET is not an exact release. Refuse rather than guess.
377
385
  return {
378
- ...common,
379
386
  currentVersion: pin.specifier,
380
387
  action: "unsupported",
381
388
  reason: "malformed-version",
@@ -383,20 +390,18 @@ async function inspectOne(target, options) {
383
390
  }
384
391
  if (comparison > 0) {
385
392
  return {
386
- ...common,
387
393
  currentVersion: pin.specifier,
388
394
  action: "ahead",
389
395
  reason: "pinned-ahead-of-target",
390
396
  };
391
397
  }
392
398
  return {
393
- ...common,
394
399
  currentVersion: pin.specifier,
395
400
  action: comparison === 0 ? "already-current" : "repin",
396
401
  };
397
402
  }
398
403
  /** Classify why a non-exact specifier cannot be treated as a release pin. */
399
- function classifyNonExactSpecifier(specifier) {
404
+ export function classifyNonExactSpecifier(specifier) {
400
405
  if (/^[\^~]|^[<>=]/.test(specifier) || specifier.includes(" - ") || specifier.includes("||")) {
401
406
  return "version-range";
402
407
  }
@@ -410,3 +415,163 @@ function classifyNonExactSpecifier(specifier) {
410
415
  function isPlainObject(value) {
411
416
  return typeof value === "object" && value !== null && !Array.isArray(value);
412
417
  }
418
+ /**
419
+ * Resolve Codex's global config location through {@link codexHome}.
420
+ *
421
+ * `$CODEX_HOME/config.toml` is shown only when an absolute override was actually
422
+ * honored — even if it happens to equal the default location — so a relative or
423
+ * blank override falls back to the default display exactly as `codexHome` falls
424
+ * back to the default path.
425
+ */
426
+ export function resolveCodexConfigLocation(env, homedir) {
427
+ return {
428
+ filePath: path.join(codexHome(env, homedir), "config.toml"),
429
+ displayPath: isCodexHomeOverrideHonored(env)
430
+ ? "$CODEX_HOME/config.toml"
431
+ : MCP_HOST_TARGETS.codex.displayPath,
432
+ };
433
+ }
434
+ /**
435
+ * Inspect Codex's global Bridge launcher, read-only, with exactly one read.
436
+ *
437
+ * Only `ENOENT` is absence. Any other read failure is a blocking
438
+ * `file-unreadable`, and the exception text is discarded: it can carry an
439
+ * absolute path. After the scan, the launcher is classified with the same shared
440
+ * pin rules as the project JSON targets.
441
+ */
442
+ export async function inspectGlobalLauncherConfigs(options) {
443
+ const { targetVersion } = options;
444
+ const { filePath, displayPath } = resolveCodexConfigLocation(options.env, options.homedir);
445
+ const base = {
446
+ hostId: "codex",
447
+ scope: "global",
448
+ displayPath,
449
+ targetVersion,
450
+ spec: null,
451
+ currentVersion: null,
452
+ registrationKey: null,
453
+ legacyRegistration: false,
454
+ };
455
+ let raw;
456
+ try {
457
+ raw = await options.readFile(filePath);
458
+ }
459
+ catch (err) {
460
+ const code = err && typeof err === "object" ? err.code : undefined;
461
+ if (code === "ENOENT") {
462
+ return {
463
+ ...base,
464
+ filePresent: false,
465
+ bridgeEntryPresent: false,
466
+ shape: "none",
467
+ action: "skip-inactive",
468
+ };
469
+ }
470
+ return {
471
+ ...base,
472
+ filePresent: true,
473
+ bridgeEntryPresent: false,
474
+ shape: "none",
475
+ action: "invalid",
476
+ reason: "file-unreadable",
477
+ };
478
+ }
479
+ const found = inspectCodexLauncher(raw);
480
+ switch (found.kind) {
481
+ case "none":
482
+ // A readable file with no Bridge declaration: the user never configured
483
+ // Bridge in Codex, so it is not a participant.
484
+ return {
485
+ ...base,
486
+ filePresent: true,
487
+ bridgeEntryPresent: false,
488
+ shape: "none",
489
+ action: "skip-inactive",
490
+ };
491
+ case "duplicate":
492
+ return {
493
+ ...base,
494
+ filePresent: true,
495
+ bridgeEntryPresent: true,
496
+ shape: "custom",
497
+ action: "duplicate-registration",
498
+ reason: "duplicate-registration",
499
+ };
500
+ case "unlocatable":
501
+ return {
502
+ ...base,
503
+ filePresent: true,
504
+ bridgeEntryPresent: true,
505
+ shape: "custom",
506
+ action: "invalid",
507
+ reason: "unlocatable",
508
+ };
509
+ case "worktree-shim":
510
+ return {
511
+ ...base,
512
+ filePresent: true,
513
+ bridgeEntryPresent: true,
514
+ registrationKey: found.registrationName,
515
+ legacyRegistration: found.registrationName !== MCP_SERVER_NAME,
516
+ shape: "worktree-shim",
517
+ action: "skip-worktree-shim",
518
+ };
519
+ case "located": {
520
+ const keyed = {
521
+ ...base,
522
+ filePresent: true,
523
+ bridgeEntryPresent: true,
524
+ registrationKey: found.registrationName,
525
+ legacyRegistration: found.registrationName !== MCP_SERVER_NAME,
526
+ };
527
+ // Same order as the JSON targets: shim first (the parser already routed
528
+ // shims above), then the package token, then the shared pin rules.
529
+ const pin = parseLauncherPin(found.args);
530
+ if (!pin) {
531
+ return { ...keyed, shape: "custom", action: "invalid", reason: "unlocatable" };
532
+ }
533
+ const classified = classifyStandardPin(pin, targetVersion);
534
+ return {
535
+ ...keyed,
536
+ shape: "standard",
537
+ spec: pin.spec,
538
+ ...classified,
539
+ action: classified.action,
540
+ };
541
+ }
542
+ }
543
+ }
544
+ // ---------------------------------------------------------------------------
545
+ // Effective Claude Code pin (BAPI-1446)
546
+ // ---------------------------------------------------------------------------
547
+ /**
548
+ * The exact release the project's Claude Code launcher is pinned to, or null.
549
+ *
550
+ * Install uses this to pin the SessionStart upgrade-notice hook to the same build
551
+ * the project's MCP config launches. It answers null — never a guess from the
552
+ * running process, and never `@latest` — when the config is absent, unreadable,
553
+ * a worktree shim, a custom launcher, ambiguous (both registration keys), or
554
+ * pinned to anything other than one exact `MAJOR.MINOR.PATCH`. Read-only.
555
+ */
556
+ export async function discoverClaudeCodeEffectivePin(options) {
557
+ try {
558
+ const relPath = MCP_HOST_TARGETS["claude-code"].relPath;
559
+ const target = relPath ? launcherTargetFor(relPath) : undefined;
560
+ if (!target)
561
+ return null;
562
+ const [found] = await inspectLauncherConfigs({
563
+ cwd: options.cwd,
564
+ targetVersion: options.targetVersion,
565
+ readFile: options.readFile,
566
+ targets: [target],
567
+ });
568
+ if (!found || found.shape !== "standard" || !found.bridgeEntryPresent)
569
+ return null;
570
+ if (isPrerequisiteFailure(found))
571
+ return null;
572
+ return isExactVersion(found.currentVersion) ? found.currentVersion : null;
573
+ }
574
+ catch {
575
+ return null;
576
+ }
577
+ }
@@ -77,6 +77,7 @@ export const MCP_HOST_TARGETS = {
77
77
  launchAgent: "claude",
78
78
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
79
79
  writeStrategy: "vendor-first",
80
+ reloadHint: "run /mcp or restart Claude Code",
80
81
  // Claude Code is always a candidate default.
81
82
  detect: () => true,
82
83
  },
@@ -90,6 +91,7 @@ export const MCP_HOST_TARGETS = {
90
91
  launchAgent: "cursor-agent",
91
92
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
92
93
  writeStrategy: "direct",
94
+ reloadHint: "toggle bridge off and on in Cursor Settings → MCP, or reload the window",
93
95
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".cursor")) ||
94
96
  typeof ctx.env.CURSOR_TRACE_DIR === "string" &&
95
97
  ctx.env.CURSOR_TRACE_DIR.length > 0,
@@ -103,6 +105,7 @@ export const MCP_HOST_TARGETS = {
103
105
  hostKind: "vscode-json",
104
106
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: false, sessionSpawning: false },
105
107
  writeStrategy: "direct",
108
+ reloadHint: "restart the bridge server (MCP: List Servers)",
106
109
  detect: (ctx) => ctx.exists(joinPath(ctx.cwd, ".vscode")),
107
110
  },
108
111
  codex: {
@@ -119,6 +122,7 @@ export const MCP_HOST_TARGETS = {
119
122
  // registration remains the ordinary interactive default.
120
123
  capabilities: { mcpConfiguration: true, customCommands: true, worktreeProvisioning: true, sessionSpawning: true },
121
124
  writeStrategy: "vendor-first",
125
+ reloadHint: "restart Codex",
122
126
  detect: (ctx) => ctx.exists(joinPath(ctx.homedir, ".codex")),
123
127
  },
124
128
  windsurf: {
@@ -18,6 +18,13 @@
18
18
  export const MCP_SERVER_NAME = "bridge";
19
19
  /** The current published npm package name for the MCP server. */
20
20
  export const MCP_PACKAGE_NAME = "@bridge_gpt/mcp-server";
21
+ /**
22
+ * The subcommand token that marks a worktree shim registration. A shim points at
23
+ * a sibling worktree's server rather than a published package, so no pin applies
24
+ * and no rewrite is ever safe (BAPI-714 / worktree provisioning). Shared by the
25
+ * JSON launcher inspection and the pure Codex TOML parser (BAPI-1445).
26
+ */
27
+ export const WORKTREE_SHIM_TOKEN = "mcp-invoke";
21
28
  /** The README resource URI served by the MCP server. */
22
29
  export const MCP_README_URI = `${MCP_SERVER_NAME}://readme`;
23
30
  /**