@buoy-gg/mcp 7.0.3 → 7.0.4

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 CHANGED
@@ -18,6 +18,20 @@ One command, non-destructive:
18
18
 
19
19
  The config it writes launches the server via `npx -y @buoy-gg/mcp@latest`, so every editor restart re-resolves the newest published version — you never get pinned to a stale copy. Restart your editor, open your app with Buoy running, and start with `list_devices`.
20
20
 
21
+ ### Corporate / private npm registries
22
+
23
+ If your machine's `.npmrc` points npm at a private registry that isn't reachable (a common setup on work laptops, e.g. off-VPN), `npx @latest` would hang trying to download the package on every editor launch — so `init` probes that registry first and, when it's unreachable, automatically installs a **pinned local copy** (from public npm) and writes a `node <path>` config instead. That takes the network off the startup path entirely.
24
+
25
+ You can also force it:
26
+
27
+ ```bash
28
+ npx -y @buoy-gg/mcp@latest init --local # always install locally, no network on startup
29
+ npx -y @buoy-gg/mcp@latest init --npx # always use the auto-updating npx entry
30
+ npx -y @buoy-gg/mcp@latest init --registry <url> # registry the local install pulls from
31
+ ```
32
+
33
+ To update a local install later, just re-run `init`.
34
+
21
35
  ## What your agent can do
22
36
 
23
37
  **Read the runtime**
@@ -296,7 +296,7 @@ async function waitForBroker(url, attempts = 20) {
296
296
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
297
297
 
298
298
  // src/version.ts
299
- var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
299
+ var VERSION = "7.0.4" ? "7.0.4" : "0.0.0-dev";
300
300
  var PACKAGE_NAME = "@buoy-gg/mcp";
301
301
 
302
302
  // src/update/checkForUpdate.ts
@@ -2615,13 +2615,17 @@ var deviceIdField = {
2615
2615
  };
2616
2616
 
2617
2617
  // src/init.ts
2618
+ import { spawnSync } from "child_process";
2618
2619
  import fs3 from "fs";
2620
+ import os3 from "os";
2619
2621
  import path3 from "path";
2622
+ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
2623
+ var PROBE_TIMEOUT_MS = 3e3;
2620
2624
  function runInit(opts = {}) {
2621
2625
  const cwd = opts.cwd ?? process.cwd();
2622
2626
  const skillSrcDir = opts.skillSrcDir ?? path3.resolve(__dirname, "../skill");
2623
2627
  const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
2624
- const entry = opts.cliPath ? { type: "stdio", command: "node", args: [opts.cliPath] } : { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
2628
+ const entry = resolveEntry(opts, log);
2625
2629
  const done = [];
2626
2630
  upsertMcpConfig(path3.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
2627
2631
  if (fs3.existsSync(path3.join(cwd, ".cursor"))) {
@@ -2638,12 +2642,125 @@ function runInit(opts = {}) {
2638
2642
  log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
2639
2643
  log(" 2. Open your app with Buoy devtools running on a device or simulator.");
2640
2644
  log(' 3. Say "buoy optimize" to start a guided performance pass.');
2641
- if (!opts.cliPath) {
2645
+ if (entry.command === "npx") {
2642
2646
  log("");
2643
2647
  log(`This launches the server via \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
2644
2648
  log(`up to date automatically. To force-refresh, re-run \`npx -y ${PACKAGE_NAME}@latest init\`.`);
2649
+ } else if (!opts.cliPath) {
2650
+ log("");
2651
+ log(`This uses a pinned local install (no network on startup). To update it later,`);
2652
+ log(`re-run init (\`node ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}...\` \u2014 or reinstall).`);
2653
+ }
2654
+ }
2655
+ function resolveEntry(opts, log) {
2656
+ if (opts.cliPath) {
2657
+ return { type: "stdio", command: "node", args: [opts.cliPath] };
2658
+ }
2659
+ const mode = opts.mode ?? "auto";
2660
+ if (mode === "npx") {
2661
+ return npxEntry();
2662
+ }
2663
+ if (mode === "local") {
2664
+ return localEntry(opts, log);
2665
+ }
2666
+ const probe = probeRegistry();
2667
+ if (probe.reachable) {
2668
+ return npxEntry();
2669
+ }
2670
+ log(
2671
+ `\u26A0 npm registry ${probe.registry ? `(${probe.registry}) ` : ""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`
2672
+ );
2673
+ return localEntry(opts, log);
2674
+ }
2675
+ function npxEntry() {
2676
+ return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
2677
+ }
2678
+ function localEntry(opts, log) {
2679
+ const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? path3.join(os3.homedir(), ".buoy-mcp");
2680
+ const registry = opts.registry ?? PUBLIC_REGISTRY;
2681
+ const cliPath = installLocalCopy(installDir, registry, log);
2682
+ if (cliPath) {
2683
+ return { type: "stdio", command: "node", args: [cliPath] };
2645
2684
  }
2685
+ log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
2686
+ return npxEntry();
2687
+ }
2688
+ function installLocalCopy(installDir, registry, log) {
2689
+ const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
2690
+ fs3.mkdirSync(installDir, { recursive: true });
2691
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2692
+ log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
2693
+ const res = spawnSync(
2694
+ npm,
2695
+ [
2696
+ "install",
2697
+ spec,
2698
+ "--prefix",
2699
+ installDir,
2700
+ "--registry",
2701
+ registry,
2702
+ `--@buoy-gg:registry=${registry}`,
2703
+ "--no-audit",
2704
+ "--no-fund",
2705
+ "--loglevel=error"
2706
+ ],
2707
+ { stdio: ["ignore", "ignore", "inherit"], encoding: "utf8" }
2708
+ );
2709
+ if (res.status !== 0) {
2710
+ log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
2711
+ return null;
2712
+ }
2713
+ const cliPath = path3.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
2714
+ if (!fs3.existsSync(cliPath)) {
2715
+ log(`\u26A0 install finished but ${cliPath} is missing.`);
2716
+ return null;
2717
+ }
2718
+ return cliPath;
2646
2719
  }
2720
+ function probeRegistry() {
2721
+ const registry = effectiveRegistry();
2722
+ if (!registry) return { reachable: false, registry: null };
2723
+ if (/^https?:\/\/registry\.npmjs\.org\/?$/i.test(registry)) {
2724
+ return { reachable: true, registry };
2725
+ }
2726
+ return { reachable: hostReachable(registry), registry };
2727
+ }
2728
+ function effectiveRegistry() {
2729
+ const scoped = npmConfigGet("@buoy-gg:registry");
2730
+ if (scoped) return scoped;
2731
+ const global = npmConfigGet("registry");
2732
+ return global ?? PUBLIC_REGISTRY;
2733
+ }
2734
+ function npmConfigGet(key) {
2735
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2736
+ const res = spawnSync(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS });
2737
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
2738
+ const val = res.stdout.trim();
2739
+ if (!val || val === "undefined" || val === "null") return null;
2740
+ return val;
2741
+ }
2742
+ function hostReachable(registry) {
2743
+ const done = spawnSync(
2744
+ process.execPath,
2745
+ ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS)],
2746
+ { encoding: "utf8", timeout: PROBE_TIMEOUT_MS + 2e3 }
2747
+ );
2748
+ return done.status === 0;
2749
+ }
2750
+ var REACHABILITY_PROBE = `
2751
+ const url = process.argv[1];
2752
+ const ms = Number(process.argv[2]) || 3000;
2753
+ const c = new AbortController();
2754
+ const t = setTimeout(() => c.abort(), ms);
2755
+ fetch(url, { method: 'HEAD', signal: c.signal })
2756
+ .then(() => { clearTimeout(t); process.exit(0); })
2757
+ .catch(() => {
2758
+ // A HEAD may be rejected while the host is up; a bare TCP-level failure is
2759
+ // what we care about. fetch throwing a TypeError = network unreachable.
2760
+ clearTimeout(t);
2761
+ process.exit(1);
2762
+ });
2763
+ `;
2647
2764
  function upsertMcpConfig(file, key, entry, cwd, done) {
2648
2765
  let json3 = {};
2649
2766
  if (fs3.existsSync(file)) {
package/dist/cli.js CHANGED
@@ -336,7 +336,7 @@ var import_node_os = __toESM(require("os"));
336
336
  var import_node_path = __toESM(require("path"));
337
337
 
338
338
  // src/version.ts
339
- var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
339
+ var VERSION = "7.0.4" ? "7.0.4" : "0.0.0-dev";
340
340
  var PACKAGE_NAME = "@buoy-gg/mcp";
341
341
 
342
342
  // src/update/checkForUpdate.ts
@@ -2647,13 +2647,17 @@ var deviceIdField = {
2647
2647
  };
2648
2648
 
2649
2649
  // src/init.ts
2650
+ var import_node_child_process2 = require("child_process");
2650
2651
  var import_node_fs3 = __toESM(require("fs"));
2652
+ var import_node_os3 = __toESM(require("os"));
2651
2653
  var import_node_path3 = __toESM(require("path"));
2654
+ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
2655
+ var PROBE_TIMEOUT_MS = 3e3;
2652
2656
  function runInit(opts = {}) {
2653
2657
  const cwd = opts.cwd ?? process.cwd();
2654
2658
  const skillSrcDir = opts.skillSrcDir ?? import_node_path3.default.resolve(__dirname, "../skill");
2655
2659
  const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
2656
- const entry = opts.cliPath ? { type: "stdio", command: "node", args: [opts.cliPath] } : { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
2660
+ const entry = resolveEntry(opts, log);
2657
2661
  const done = [];
2658
2662
  upsertMcpConfig(import_node_path3.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
2659
2663
  if (import_node_fs3.default.existsSync(import_node_path3.default.join(cwd, ".cursor"))) {
@@ -2670,12 +2674,125 @@ function runInit(opts = {}) {
2670
2674
  log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
2671
2675
  log(" 2. Open your app with Buoy devtools running on a device or simulator.");
2672
2676
  log(' 3. Say "buoy optimize" to start a guided performance pass.');
2673
- if (!opts.cliPath) {
2677
+ if (entry.command === "npx") {
2674
2678
  log("");
2675
2679
  log(`This launches the server via \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
2676
2680
  log(`up to date automatically. To force-refresh, re-run \`npx -y ${PACKAGE_NAME}@latest init\`.`);
2681
+ } else if (!opts.cliPath) {
2682
+ log("");
2683
+ log(`This uses a pinned local install (no network on startup). To update it later,`);
2684
+ log(`re-run init (\`node ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}...\` \u2014 or reinstall).`);
2685
+ }
2686
+ }
2687
+ function resolveEntry(opts, log) {
2688
+ if (opts.cliPath) {
2689
+ return { type: "stdio", command: "node", args: [opts.cliPath] };
2690
+ }
2691
+ const mode = opts.mode ?? "auto";
2692
+ if (mode === "npx") {
2693
+ return npxEntry();
2694
+ }
2695
+ if (mode === "local") {
2696
+ return localEntry(opts, log);
2677
2697
  }
2698
+ const probe = probeRegistry();
2699
+ if (probe.reachable) {
2700
+ return npxEntry();
2701
+ }
2702
+ log(
2703
+ `\u26A0 npm registry ${probe.registry ? `(${probe.registry}) ` : ""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`
2704
+ );
2705
+ return localEntry(opts, log);
2706
+ }
2707
+ function npxEntry() {
2708
+ return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
2678
2709
  }
2710
+ function localEntry(opts, log) {
2711
+ const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path3.default.join(import_node_os3.default.homedir(), ".buoy-mcp");
2712
+ const registry = opts.registry ?? PUBLIC_REGISTRY;
2713
+ const cliPath = installLocalCopy(installDir, registry, log);
2714
+ if (cliPath) {
2715
+ return { type: "stdio", command: "node", args: [cliPath] };
2716
+ }
2717
+ log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
2718
+ return npxEntry();
2719
+ }
2720
+ function installLocalCopy(installDir, registry, log) {
2721
+ const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
2722
+ import_node_fs3.default.mkdirSync(installDir, { recursive: true });
2723
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2724
+ log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
2725
+ const res = (0, import_node_child_process2.spawnSync)(
2726
+ npm,
2727
+ [
2728
+ "install",
2729
+ spec,
2730
+ "--prefix",
2731
+ installDir,
2732
+ "--registry",
2733
+ registry,
2734
+ `--@buoy-gg:registry=${registry}`,
2735
+ "--no-audit",
2736
+ "--no-fund",
2737
+ "--loglevel=error"
2738
+ ],
2739
+ { stdio: ["ignore", "ignore", "inherit"], encoding: "utf8" }
2740
+ );
2741
+ if (res.status !== 0) {
2742
+ log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
2743
+ return null;
2744
+ }
2745
+ const cliPath = import_node_path3.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
2746
+ if (!import_node_fs3.default.existsSync(cliPath)) {
2747
+ log(`\u26A0 install finished but ${cliPath} is missing.`);
2748
+ return null;
2749
+ }
2750
+ return cliPath;
2751
+ }
2752
+ function probeRegistry() {
2753
+ const registry = effectiveRegistry();
2754
+ if (!registry) return { reachable: false, registry: null };
2755
+ if (/^https?:\/\/registry\.npmjs\.org\/?$/i.test(registry)) {
2756
+ return { reachable: true, registry };
2757
+ }
2758
+ return { reachable: hostReachable(registry), registry };
2759
+ }
2760
+ function effectiveRegistry() {
2761
+ const scoped = npmConfigGet("@buoy-gg:registry");
2762
+ if (scoped) return scoped;
2763
+ const global = npmConfigGet("registry");
2764
+ return global ?? PUBLIC_REGISTRY;
2765
+ }
2766
+ function npmConfigGet(key) {
2767
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2768
+ const res = (0, import_node_child_process2.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS });
2769
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
2770
+ const val = res.stdout.trim();
2771
+ if (!val || val === "undefined" || val === "null") return null;
2772
+ return val;
2773
+ }
2774
+ function hostReachable(registry) {
2775
+ const done = (0, import_node_child_process2.spawnSync)(
2776
+ process.execPath,
2777
+ ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS)],
2778
+ { encoding: "utf8", timeout: PROBE_TIMEOUT_MS + 2e3 }
2779
+ );
2780
+ return done.status === 0;
2781
+ }
2782
+ var REACHABILITY_PROBE = `
2783
+ const url = process.argv[1];
2784
+ const ms = Number(process.argv[2]) || 3000;
2785
+ const c = new AbortController();
2786
+ const t = setTimeout(() => c.abort(), ms);
2787
+ fetch(url, { method: 'HEAD', signal: c.signal })
2788
+ .then(() => { clearTimeout(t); process.exit(0); })
2789
+ .catch(() => {
2790
+ // A HEAD may be rejected while the host is up; a bare TCP-level failure is
2791
+ // what we care about. fetch throwing a TypeError = network unreachable.
2792
+ clearTimeout(t);
2793
+ process.exit(1);
2794
+ });
2795
+ `;
2679
2796
  function upsertMcpConfig(file, key, entry, cwd, done) {
2680
2797
  let json3 = {};
2681
2798
  if (import_node_fs3.default.existsSync(file)) {
@@ -2762,7 +2879,13 @@ async function main() {
2762
2879
  }
2763
2880
  if (process.argv[2] === "init") {
2764
2881
  try {
2765
- runInit();
2882
+ const args = process.argv.slice(3);
2883
+ let mode = "auto";
2884
+ if (args.includes("--local")) mode = "local";
2885
+ else if (args.includes("--npx")) mode = "npx";
2886
+ const ri = args.indexOf("--registry");
2887
+ const registry = ri !== -1 ? args[ri + 1] : void 0;
2888
+ runInit({ mode, registry });
2766
2889
  process.exit(0);
2767
2890
  } catch (err) {
2768
2891
  process.stderr.write(`[buoy-mcp] init failed: ${err?.message ?? err}
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SyncClient,
4
4
  createMcpServer,
5
5
  runInit
6
- } from "./chunk-MGZN6MJE.mjs";
6
+ } from "./chunk-EC6YNLYA.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -41,7 +41,13 @@ async function main() {
41
41
  }
42
42
  if (process.argv[2] === "init") {
43
43
  try {
44
- runInit();
44
+ const args = process.argv.slice(3);
45
+ let mode = "auto";
46
+ if (args.includes("--local")) mode = "local";
47
+ else if (args.includes("--npx")) mode = "npx";
48
+ const ri = args.indexOf("--registry");
49
+ const registry = ri !== -1 ? args[ri + 1] : void 0;
50
+ runInit({ mode, registry });
45
51
  process.exit(0);
46
52
  } catch (err) {
47
53
  process.stderr.write(`[buoy-mcp] init failed: ${err?.message ?? err}
package/dist/index.d.mts CHANGED
@@ -132,20 +132,61 @@ declare function createMcpServer(sync: SyncClient): McpServer;
132
132
  * Idempotent + non-destructive: merges into existing configs (never clobbers
133
133
  * other servers), and re-running just refreshes the buoy entry + skill.
134
134
  *
135
+ * ## How the server gets launched (and why corporate registries broke it)
136
+ *
135
137
  * By default the written config launches the server via
136
138
  * `npx -y @buoy-gg/mcp@latest`, so every editor restart re-resolves the newest
137
- * published version — users can't get pinned to a stale local copy. (Passing an
138
- * explicit `cliPath` writes a `node <path>` entry instead, for local dev/testing
139
- * against a working build.)
139
+ * published version — users can't get pinned to a stale local copy.
140
+ *
141
+ * That default fails hard on machines whose `.npmrc` routes npm at an
142
+ * unreachable private registry (e.g. a corporate `@buoy-gg:registry=` or a
143
+ * global `registry=` pointing at an internal host that doesn't resolve off-VPN).
144
+ * There, `npx @latest` blocks trying to download the package from a host that
145
+ * never answers, the editor's MCP startup times out (~30s), and the server
146
+ * silently never comes up — invisible to the user.
147
+ *
148
+ * So `init` now:
149
+ * 1. Probes the npm registry that WOULD serve `@buoy-gg/mcp`. If it's
150
+ * unreachable, it falls back to a **local install** automatically.
151
+ * 2. Local mode installs a pinned copy into a stable dir (`~/.buoy-mcp`) from
152
+ * the public npm registry (bypassing the broken `.npmrc`) and writes a
153
+ * `node <path>` config — taking the network off the editor-startup path
154
+ * entirely.
155
+ * 3. `--local` / `--npx` force a mode; `--registry <url>` overrides which
156
+ * registry the local install pulls from.
157
+ * 4. A `cliPath` (local-dev escape hatch) still writes a `node <path>` entry
158
+ * pointed at a working build, no publish needed.
140
159
  */
160
+ type InitMode = "auto" | "npx" | "local";
141
161
  interface InitOptions {
142
162
  /** Project root to install into. @default process.cwd() */
143
163
  cwd?: string;
144
164
  /**
145
165
  * Absolute path to a built cli.js. When set, the config launches that file
146
- * via `node` instead of `npx @latest` — for local development only.
166
+ * via `node` instead of `npx @latest` — for local development only. Takes
167
+ * precedence over `mode`.
147
168
  */
148
169
  cliPath?: string;
170
+ /**
171
+ * Launch strategy for the written config:
172
+ * - `"auto"` (default): npx `@latest`, but fall back to a local install if
173
+ * the npm registry for `@buoy-gg/mcp` is unreachable.
174
+ * - `"npx"`: always write the `npx -y @latest` entry (skip the probe).
175
+ * - `"local"`: always install a pinned copy to a stable dir and point the
176
+ * config at it (no network on editor startup).
177
+ * @default "auto"
178
+ */
179
+ mode?: InitMode;
180
+ /**
181
+ * Registry to pull the local install from. @default the public npm registry.
182
+ * Only used for `local` (or auto-fallback-to-local) installs.
183
+ */
184
+ registry?: string;
185
+ /**
186
+ * Stable dir the local install lives in.
187
+ * @default `$BUOY_MCP_HOME` or `~/.buoy-mcp`.
188
+ */
189
+ installDir?: string;
149
190
  /** Where the skill source lives. @default resolved from __dirname (../skill). */
150
191
  skillSrcDir?: string;
151
192
  log?: (msg: string) => void;
package/dist/index.d.ts CHANGED
@@ -132,20 +132,61 @@ declare function createMcpServer(sync: SyncClient): McpServer;
132
132
  * Idempotent + non-destructive: merges into existing configs (never clobbers
133
133
  * other servers), and re-running just refreshes the buoy entry + skill.
134
134
  *
135
+ * ## How the server gets launched (and why corporate registries broke it)
136
+ *
135
137
  * By default the written config launches the server via
136
138
  * `npx -y @buoy-gg/mcp@latest`, so every editor restart re-resolves the newest
137
- * published version — users can't get pinned to a stale local copy. (Passing an
138
- * explicit `cliPath` writes a `node <path>` entry instead, for local dev/testing
139
- * against a working build.)
139
+ * published version — users can't get pinned to a stale local copy.
140
+ *
141
+ * That default fails hard on machines whose `.npmrc` routes npm at an
142
+ * unreachable private registry (e.g. a corporate `@buoy-gg:registry=` or a
143
+ * global `registry=` pointing at an internal host that doesn't resolve off-VPN).
144
+ * There, `npx @latest` blocks trying to download the package from a host that
145
+ * never answers, the editor's MCP startup times out (~30s), and the server
146
+ * silently never comes up — invisible to the user.
147
+ *
148
+ * So `init` now:
149
+ * 1. Probes the npm registry that WOULD serve `@buoy-gg/mcp`. If it's
150
+ * unreachable, it falls back to a **local install** automatically.
151
+ * 2. Local mode installs a pinned copy into a stable dir (`~/.buoy-mcp`) from
152
+ * the public npm registry (bypassing the broken `.npmrc`) and writes a
153
+ * `node <path>` config — taking the network off the editor-startup path
154
+ * entirely.
155
+ * 3. `--local` / `--npx` force a mode; `--registry <url>` overrides which
156
+ * registry the local install pulls from.
157
+ * 4. A `cliPath` (local-dev escape hatch) still writes a `node <path>` entry
158
+ * pointed at a working build, no publish needed.
140
159
  */
160
+ type InitMode = "auto" | "npx" | "local";
141
161
  interface InitOptions {
142
162
  /** Project root to install into. @default process.cwd() */
143
163
  cwd?: string;
144
164
  /**
145
165
  * Absolute path to a built cli.js. When set, the config launches that file
146
- * via `node` instead of `npx @latest` — for local development only.
166
+ * via `node` instead of `npx @latest` — for local development only. Takes
167
+ * precedence over `mode`.
147
168
  */
148
169
  cliPath?: string;
170
+ /**
171
+ * Launch strategy for the written config:
172
+ * - `"auto"` (default): npx `@latest`, but fall back to a local install if
173
+ * the npm registry for `@buoy-gg/mcp` is unreachable.
174
+ * - `"npx"`: always write the `npx -y @latest` entry (skip the probe).
175
+ * - `"local"`: always install a pinned copy to a stable dir and point the
176
+ * config at it (no network on editor startup).
177
+ * @default "auto"
178
+ */
179
+ mode?: InitMode;
180
+ /**
181
+ * Registry to pull the local install from. @default the public npm registry.
182
+ * Only used for `local` (or auto-fallback-to-local) installs.
183
+ */
184
+ registry?: string;
185
+ /**
186
+ * Stable dir the local install lives in.
187
+ * @default `$BUOY_MCP_HOME` or `~/.buoy-mcp`.
188
+ */
189
+ installDir?: string;
149
190
  /** Where the skill source lives. @default resolved from __dirname (../skill). */
150
191
  skillSrcDir?: string;
151
192
  log?: (msg: string) => void;
package/dist/index.js CHANGED
@@ -371,7 +371,7 @@ var import_node_os = __toESM(require("os"));
371
371
  var import_node_path = __toESM(require("path"));
372
372
 
373
373
  // src/version.ts
374
- var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
374
+ var VERSION = "7.0.4" ? "7.0.4" : "0.0.0-dev";
375
375
  var PACKAGE_NAME = "@buoy-gg/mcp";
376
376
 
377
377
  // src/update/checkForUpdate.ts
@@ -2682,13 +2682,17 @@ var deviceIdField = {
2682
2682
  };
2683
2683
 
2684
2684
  // src/init.ts
2685
+ var import_node_child_process2 = require("child_process");
2685
2686
  var import_node_fs3 = __toESM(require("fs"));
2687
+ var import_node_os3 = __toESM(require("os"));
2686
2688
  var import_node_path3 = __toESM(require("path"));
2689
+ var PUBLIC_REGISTRY = "https://registry.npmjs.org";
2690
+ var PROBE_TIMEOUT_MS = 3e3;
2687
2691
  function runInit(opts = {}) {
2688
2692
  const cwd = opts.cwd ?? process.cwd();
2689
2693
  const skillSrcDir = opts.skillSrcDir ?? import_node_path3.default.resolve(__dirname, "../skill");
2690
2694
  const log = opts.log ?? ((m) => process.stdout.write(m + "\n"));
2691
- const entry = opts.cliPath ? { type: "stdio", command: "node", args: [opts.cliPath] } : { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
2695
+ const entry = resolveEntry(opts, log);
2692
2696
  const done = [];
2693
2697
  upsertMcpConfig(import_node_path3.default.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
2694
2698
  if (import_node_fs3.default.existsSync(import_node_path3.default.join(cwd, ".cursor"))) {
@@ -2705,12 +2709,125 @@ function runInit(opts = {}) {
2705
2709
  log(" 1. Restart your editor (or reconnect the MCP server) to load `buoy`.");
2706
2710
  log(" 2. Open your app with Buoy devtools running on a device or simulator.");
2707
2711
  log(' 3. Say "buoy optimize" to start a guided performance pass.');
2708
- if (!opts.cliPath) {
2712
+ if (entry.command === "npx") {
2709
2713
  log("");
2710
2714
  log(`This launches the server via \`npx -y ${PACKAGE_NAME}@latest\`, so it stays`);
2711
2715
  log(`up to date automatically. To force-refresh, re-run \`npx -y ${PACKAGE_NAME}@latest init\`.`);
2716
+ } else if (!opts.cliPath) {
2717
+ log("");
2718
+ log(`This uses a pinned local install (no network on startup). To update it later,`);
2719
+ log(`re-run init (\`node ${entry.args[0]?.replace(/dist[/\\]cli\.js$/, "") ?? ""}...\` \u2014 or reinstall).`);
2720
+ }
2721
+ }
2722
+ function resolveEntry(opts, log) {
2723
+ if (opts.cliPath) {
2724
+ return { type: "stdio", command: "node", args: [opts.cliPath] };
2725
+ }
2726
+ const mode = opts.mode ?? "auto";
2727
+ if (mode === "npx") {
2728
+ return npxEntry();
2729
+ }
2730
+ if (mode === "local") {
2731
+ return localEntry(opts, log);
2732
+ }
2733
+ const probe = probeRegistry();
2734
+ if (probe.reachable) {
2735
+ return npxEntry();
2736
+ }
2737
+ log(
2738
+ `\u26A0 npm registry ${probe.registry ? `(${probe.registry}) ` : ""}looks unreachable \u2014 \`npx @latest\` would hang your editor at startup. Falling back to a pinned local install.`
2739
+ );
2740
+ return localEntry(opts, log);
2741
+ }
2742
+ function npxEntry() {
2743
+ return { type: "stdio", command: "npx", args: ["-y", `${PACKAGE_NAME}@latest`] };
2744
+ }
2745
+ function localEntry(opts, log) {
2746
+ const installDir = opts.installDir ?? process.env.BUOY_MCP_HOME ?? import_node_path3.default.join(import_node_os3.default.homedir(), ".buoy-mcp");
2747
+ const registry = opts.registry ?? PUBLIC_REGISTRY;
2748
+ const cliPath = installLocalCopy(installDir, registry, log);
2749
+ if (cliPath) {
2750
+ return { type: "stdio", command: "node", args: [cliPath] };
2712
2751
  }
2752
+ log("\u26A0 local install failed \u2014 writing the npx entry instead (see the error above).");
2753
+ return npxEntry();
2754
+ }
2755
+ function installLocalCopy(installDir, registry, log) {
2756
+ const spec = `${PACKAGE_NAME}@${VERSION === "0.0.0-dev" ? "latest" : VERSION}`;
2757
+ import_node_fs3.default.mkdirSync(installDir, { recursive: true });
2758
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2759
+ log(`Installing ${spec} into ${installDir} from ${registry} \u2026`);
2760
+ const res = (0, import_node_child_process2.spawnSync)(
2761
+ npm,
2762
+ [
2763
+ "install",
2764
+ spec,
2765
+ "--prefix",
2766
+ installDir,
2767
+ "--registry",
2768
+ registry,
2769
+ `--@buoy-gg:registry=${registry}`,
2770
+ "--no-audit",
2771
+ "--no-fund",
2772
+ "--loglevel=error"
2773
+ ],
2774
+ { stdio: ["ignore", "ignore", "inherit"], encoding: "utf8" }
2775
+ );
2776
+ if (res.status !== 0) {
2777
+ log(`\u26A0 npm install exited with code ${res.status ?? "?"}${res.error ? ` (${res.error.message})` : ""}.`);
2778
+ return null;
2779
+ }
2780
+ const cliPath = import_node_path3.default.join(installDir, "node_modules", PACKAGE_NAME, "dist", "cli.js");
2781
+ if (!import_node_fs3.default.existsSync(cliPath)) {
2782
+ log(`\u26A0 install finished but ${cliPath} is missing.`);
2783
+ return null;
2784
+ }
2785
+ return cliPath;
2713
2786
  }
2787
+ function probeRegistry() {
2788
+ const registry = effectiveRegistry();
2789
+ if (!registry) return { reachable: false, registry: null };
2790
+ if (/^https?:\/\/registry\.npmjs\.org\/?$/i.test(registry)) {
2791
+ return { reachable: true, registry };
2792
+ }
2793
+ return { reachable: hostReachable(registry), registry };
2794
+ }
2795
+ function effectiveRegistry() {
2796
+ const scoped = npmConfigGet("@buoy-gg:registry");
2797
+ if (scoped) return scoped;
2798
+ const global = npmConfigGet("registry");
2799
+ return global ?? PUBLIC_REGISTRY;
2800
+ }
2801
+ function npmConfigGet(key) {
2802
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2803
+ const res = (0, import_node_child_process2.spawnSync)(npm, ["config", "get", key], { encoding: "utf8", timeout: PROBE_TIMEOUT_MS });
2804
+ if (res.status !== 0 || typeof res.stdout !== "string") return null;
2805
+ const val = res.stdout.trim();
2806
+ if (!val || val === "undefined" || val === "null") return null;
2807
+ return val;
2808
+ }
2809
+ function hostReachable(registry) {
2810
+ const done = (0, import_node_child_process2.spawnSync)(
2811
+ process.execPath,
2812
+ ["-e", REACHABILITY_PROBE, registry, String(PROBE_TIMEOUT_MS)],
2813
+ { encoding: "utf8", timeout: PROBE_TIMEOUT_MS + 2e3 }
2814
+ );
2815
+ return done.status === 0;
2816
+ }
2817
+ var REACHABILITY_PROBE = `
2818
+ const url = process.argv[1];
2819
+ const ms = Number(process.argv[2]) || 3000;
2820
+ const c = new AbortController();
2821
+ const t = setTimeout(() => c.abort(), ms);
2822
+ fetch(url, { method: 'HEAD', signal: c.signal })
2823
+ .then(() => { clearTimeout(t); process.exit(0); })
2824
+ .catch(() => {
2825
+ // A HEAD may be rejected while the host is up; a bare TCP-level failure is
2826
+ // what we care about. fetch throwing a TypeError = network unreachable.
2827
+ clearTimeout(t);
2828
+ process.exit(1);
2829
+ });
2830
+ `;
2714
2831
  function upsertMcpConfig(file, key, entry, cwd, done) {
2715
2832
  let json3 = {};
2716
2833
  if (import_node_fs3.default.existsSync(file)) {
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  registerStorageTools,
28
28
  registerZustandTools,
29
29
  runInit
30
- } from "./chunk-MGZN6MJE.mjs";
30
+ } from "./chunk-EC6YNLYA.mjs";
31
31
  export {
32
32
  ALL_DEVICES,
33
33
  BROKER_PORT,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buoy-gg/mcp",
3
3
  "mcpName": "io.github.buoy-gg/mcp",
4
- "version": "7.0.3",
4
+ "version": "7.0.4",
5
5
  "description": "MCP server for Buoy devtools — lets any LLM copilot drive Buoy tools (perf-monitor benchmarks, etc.) over the devtool sync protocol",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -31,7 +31,7 @@
31
31
  "@modelcontextprotocol/sdk": "^1.29.0",
32
32
  "socket.io-client": "^4.8.1",
33
33
  "zod": "^3.25.0",
34
- "@buoy-gg/sync-broker": "7.0.3"
34
+ "@buoy-gg/sync-broker": "7.0.4"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",