vexp-mcp 2.0.31 → 2.0.32

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.
@@ -420,8 +420,20 @@ function discoverWorkspaceRoot() {
420
420
  }
421
421
  return cwd;
422
422
  }
423
+ /** Emit a stderr warning at most once per distinct message. The resolvers below
424
+ * run on every connect; without this guard a misconfig would spam the MCP log.
425
+ * stderr is safe — stdout carries the JSON-RPC protocol, stderr is for logs. */
426
+ const _warnedOnce = new Set();
427
+ function warnOnce(msg) {
428
+ if (_warnedOnce.has(msg))
429
+ return;
430
+ _warnedOnce.add(msg);
431
+ console.error(msg);
432
+ }
423
433
  /**
424
- * Resolve the workspace root for daemon spawn / socket targeting.
434
+ * Decide the workspace root AND which signal chose it. Single source of truth
435
+ * for {@link resolveWorkspaceRoot} and {@link resolveTargeting} so they cannot
436
+ * drift.
425
437
  *
426
438
  * Precedence:
427
439
  * 1. `explicit` — an HTTP per-workspace client's pinned root
@@ -432,12 +444,36 @@ function discoverWorkspaceRoot() {
432
444
  * user-scope ~/.claude.json MCP entry is shared (and intentionally no longer
433
445
  * pins VEXP_WORKSPACE — see agent-config.ts:configureClaudeCodeGlobal).
434
446
  * 4. cwd walk-up ({@link discoverWorkspaceRoot})
447
+ *
448
+ * EXCEPTION: a *global/stale* `VEXP_WORKSPACE` must not silently override the
449
+ * per-session `CLAUDE_PROJECT_DIR`. When both are set and DISAGREE, the
450
+ * per-session signal wins (and we warn once) — otherwise every parallel agent
451
+ * session is pinned to one repo's daemon and cross-repo get_skeleton /
452
+ * run_pipeline return []/wrong-repo results. A deliberate single-project pin
453
+ * (VEXP_WORKSPACE set, no CLAUDE_PROJECT_DIR — e.g. Codex, the CLI, VS Code)
454
+ * is unaffected.
435
455
  */
456
+ function resolveWorkspaceWithSource(explicit) {
457
+ if (explicit)
458
+ return { root: explicit, source: "VEXP_WORKSPACE" };
459
+ const vw = process.env["VEXP_WORKSPACE"];
460
+ const cpd = process.env["CLAUDE_PROJECT_DIR"];
461
+ if (vw && cpd && path.resolve(vw).toLowerCase() !== path.resolve(cpd).toLowerCase()) {
462
+ warnOnce(`[vexp-mcp] VEXP_WORKSPACE=${vw} disagrees with this session's ` +
463
+ `CLAUDE_PROJECT_DIR=${cpd}; using CLAUDE_PROJECT_DIR for per-session ` +
464
+ `targeting. Unset the global VEXP_WORKSPACE env var to silence this.`);
465
+ return { root: cpd, source: "CLAUDE_PROJECT_DIR" };
466
+ }
467
+ if (vw)
468
+ return { root: vw, source: "VEXP_WORKSPACE" };
469
+ if (cpd)
470
+ return { root: cpd, source: "CLAUDE_PROJECT_DIR" };
471
+ return { root: discoverWorkspaceRoot(), source: "cwd-discovery" };
472
+ }
473
+ /** @see resolveWorkspaceWithSource for the precedence (and the VEXP_WORKSPACE /
474
+ * CLAUDE_PROJECT_DIR divergence rule). */
436
475
  export function resolveWorkspaceRoot(explicit) {
437
- return (explicit ??
438
- process.env["VEXP_WORKSPACE"] ??
439
- process.env["CLAUDE_PROJECT_DIR"] ??
440
- discoverWorkspaceRoot());
476
+ return resolveWorkspaceWithSource(explicit).root;
441
477
  }
442
478
  /**
443
479
  * Read `~/.vexp/daemons.json` and pick the best entry for the given workspace.
@@ -529,20 +565,9 @@ function pickFromDaemonRegistry(workspaceHint) {
529
565
  export function resolveTargeting() {
530
566
  // Which signal gives us the workspace root? (Reported even when VEXP_SOCKET
531
567
  // overrides the socket, so the diagnostics still show the intended project.)
532
- let source;
533
- let workspaceRoot;
534
- if (process.env["VEXP_WORKSPACE"]) {
535
- workspaceRoot = process.env["VEXP_WORKSPACE"];
536
- source = "VEXP_WORKSPACE";
537
- }
538
- else if (process.env["CLAUDE_PROJECT_DIR"]) {
539
- workspaceRoot = process.env["CLAUDE_PROJECT_DIR"];
540
- source = "CLAUDE_PROJECT_DIR";
541
- }
542
- else {
543
- workspaceRoot = discoverWorkspaceRoot();
544
- source = "cwd-discovery";
545
- }
568
+ // Shared with resolveWorkspaceRoot — including the VEXP_WORKSPACE /
569
+ // CLAUDE_PROJECT_DIR divergence rule.
570
+ const { root: workspaceRoot, source } = resolveWorkspaceWithSource();
546
571
  const explicit = process.env["VEXP_SOCKET"];
547
572
  if (explicit) {
548
573
  return { socket: explicit, source, socketOrigin: "explicit", workspaceRoot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vexp-mcp",
3
- "version": "2.0.31",
3
+ "version": "2.0.32",
4
4
  "description": "vexp MCP server — AI context tools for coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",