@phidiassj/aiyoperps-mcp-installer 0.5.4 → 0.5.6

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
@@ -28,3 +28,4 @@ npx -y @phidiassj/aiyoperps-mcp-installer --url http://127.0.0.1:5078/mcp
28
28
  - For Codex, the installer writes `startup_timeout_sec = 60` because the first `npx` launch may take longer than the default 10-second startup timeout.
29
29
  - Before writing host config, the installer prewarms the bridge package with `npx ... --help` to reduce first-run startup latency.
30
30
  - The generated MCP entry includes `--startup-ping` so the bridge verifies the AiyoPerps MCP endpoint before entering stdio mode.
31
+ - Before installation, the installer probes candidate MCP URLs and only writes config if it finds a reachable endpoint.
@@ -9,6 +9,17 @@ import { parse as parseToml } from 'smol-toml';
9
9
 
10
10
  const SERVER_NAME = 'aiyoperps';
11
11
  const DEFAULT_URL = 'http://127.0.0.1:5078/mcp';
12
+ const BRIDGE_PACKAGE_NAME = '@phidiassj/aiyoperps-mcp-bridge';
13
+ const BRIDGE_SCRIPT_RELATIVE_PATH = path.join(
14
+ 'node_modules',
15
+ '@phidiassj',
16
+ 'aiyoperps-mcp-bridge',
17
+ 'bin',
18
+ 'aiyoperps-mcp-bridge.js');
19
+ const EXECUTABLE_CACHE = new Map();
20
+ let bridgeRuntimePrepared = false;
21
+ let bridgeRuntimeAttempted = false;
22
+ let bridgeRuntimeWarning = '';
12
23
 
13
24
  const cli = parseCliArgs(argv.slice(2));
14
25
  const targetUrl = cli.url;
@@ -229,7 +240,13 @@ async function installAll(hosts, url) {
229
240
  return;
230
241
  }
231
242
 
232
- await prewarmBridgePackage(url);
243
+ const resolvedUrl = await resolveInstallUrl(url, cli.urlExplicit);
244
+ if (!resolvedUrl) {
245
+ return;
246
+ }
247
+
248
+ bindInstallActions(selected, resolvedUrl);
249
+ await prewarmBridgePackage(resolvedUrl);
233
250
  await runForHosts(selected, 'install');
234
251
  }
235
252
 
@@ -252,7 +269,13 @@ async function installAnyOf(rl, hosts, url) {
252
269
  return;
253
270
  }
254
271
 
255
- await prewarmBridgePackage(url);
272
+ const resolvedUrl = await resolveInstallUrl(url, cli.urlExplicit);
273
+ if (!resolvedUrl) {
274
+ return;
275
+ }
276
+
277
+ bindInstallActions(selected, resolvedUrl);
278
+ await prewarmBridgePackage(resolvedUrl);
256
279
  await runForHosts(selected, 'install');
257
280
  }
258
281
 
@@ -317,6 +340,13 @@ async function runNonInteractive(cliArgs, hosts, url) {
317
340
  requireSupported: true,
318
341
  requireInstalled: false
319
342
  });
343
+ const resolvedUrl = await resolveInstallUrl(url, cliArgs.urlExplicit);
344
+ if (!resolvedUrl) {
345
+ return;
346
+ }
347
+
348
+ bindInstallActions(selected, resolvedUrl);
349
+ await prewarmBridgePackage(resolvedUrl);
320
350
  await runForHosts(selected, 'install');
321
351
  return;
322
352
  }
@@ -358,6 +388,27 @@ function selectHostsByIds(hosts, ids, requirements) {
358
388
  });
359
389
  }
360
390
 
391
+ function bindInstallActions(hosts, url) {
392
+ for (const host of hosts) {
393
+ host.install = createInstallAction(host, url);
394
+ }
395
+ }
396
+
397
+ function createInstallAction(host, url) {
398
+ switch (host.id) {
399
+ case 'codex':
400
+ return () => installCodex(host.path, url);
401
+ case 'claude-code':
402
+ return () => installClaudeCode(url);
403
+ case 'claude-desktop':
404
+ return () => installClaudeDesktop(host.path, url);
405
+ case 'openclaw':
406
+ return () => installOpenClaw(url);
407
+ default:
408
+ return async () => { };
409
+ }
410
+ }
411
+
361
412
  function installCodex(configPath, url) {
362
413
  ensureParentDir(configPath);
363
414
 
@@ -501,17 +552,73 @@ function buildJsonServerConfig(url) {
501
552
  }
502
553
 
503
554
  async function prewarmBridgePackage(url) {
504
- stdout.write('\nPrewarming npm bridge package...\n');
505
- const commandLine = resolveCommandLineSpec(process.platform, url, {
506
- prewarm: true
507
- });
508
- const result = runCommand(commandLine.command, commandLine.args);
509
- if (!result.ok) {
510
- stdout.write(` warning: prewarm failed: ${result.stderr || result.error?.message || 'unknown error'}\n`);
555
+ stdout.write('\nPreparing bridge runtime...\n');
556
+ await ensureBridgeRuntimePrepared();
557
+ if (bridgeRuntimePrepared) {
558
+ stdout.write(` ready: ${resolveInstalledBridgeScriptPath()}\n`);
511
559
  return;
512
560
  }
513
561
 
514
- stdout.write(' prewarm complete\n');
562
+ stdout.write(` warning: ${bridgeRuntimeWarning || 'falling back to npx'}\n`);
563
+ }
564
+
565
+ async function resolveInstallUrl(defaultUrl, urlExplicit) {
566
+ await ensureBridgeRuntimePrepared();
567
+ const candidates = urlExplicit
568
+ ? [defaultUrl]
569
+ : buildCandidateUrls(defaultUrl);
570
+
571
+ stdout.write('\nProbing MCP endpoint candidates...\n');
572
+ for (const candidate of candidates) {
573
+ const ok = await canReachMcpEndpoint(candidate);
574
+ stdout.write(` ${ok ? 'ok' : 'fail'} ${candidate}\n`);
575
+ if (ok) {
576
+ return candidate;
577
+ }
578
+ }
579
+
580
+ stdout.write(
581
+ 'No reachable AiyoPerps MCP endpoint was detected. ' +
582
+ 'Start the AiyoPerps HTTP API first or pass a valid --url, then retry.\n');
583
+ return null;
584
+ }
585
+
586
+ function buildCandidateUrls(defaultUrl) {
587
+ const urls = [
588
+ defaultUrl,
589
+ 'http://localhost:5078/mcp',
590
+ 'http://winhost:5078/mcp',
591
+ 'http://host.docker.internal:5078/mcp'
592
+ ];
593
+
594
+ const wslGatewayUrl = detectWslGatewayUrl();
595
+ if (wslGatewayUrl) {
596
+ urls.push(wslGatewayUrl);
597
+ }
598
+
599
+ return [...new Set(urls)];
600
+ }
601
+
602
+ function detectWslGatewayUrl() {
603
+ if (!isWsl()) {
604
+ return null;
605
+ }
606
+
607
+ try {
608
+ const content = fs.readFileSync('/etc/resolv.conf', 'utf8');
609
+ const match = content.match(/^nameserver\s+([0-9.]+)\s*$/m);
610
+ return match ? `http://${match[1]}:5078/mcp` : null;
611
+ } catch {
612
+ return null;
613
+ }
614
+ }
615
+
616
+ async function canReachMcpEndpoint(url) {
617
+ const commandLine = resolveCommandLineSpec(process.platform, url, {
618
+ healthCheck: true
619
+ });
620
+ const result = runCommand(commandLine.command, commandLine.args);
621
+ return result.ok;
515
622
  }
516
623
 
517
624
  function hasCodexBlock(content) {
@@ -570,6 +677,10 @@ function resolveTargetUrl(args) {
570
677
  return env.AIYOPERPS_MCP_URL || DEFAULT_URL;
571
678
  }
572
679
 
680
+ function isWsl() {
681
+ return Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
682
+ }
683
+
573
684
  function parseCliArgs(args) {
574
685
  const install = [];
575
686
  const uninstall = [];
@@ -577,6 +688,7 @@ function parseCliArgs(args) {
577
688
  let statusOnly = false;
578
689
  let nonInteractive = false;
579
690
  let url = env.AIYOPERPS_MCP_URL || DEFAULT_URL;
691
+ let urlExplicit = Boolean(env.AIYOPERPS_MCP_URL);
580
692
 
581
693
  for (let index = 0; index < args.length; index += 1) {
582
694
  const arg = args[index];
@@ -626,11 +738,13 @@ function parseCliArgs(args) {
626
738
 
627
739
  if (arg.startsWith('--url=')) {
628
740
  url = arg.slice('--url='.length);
741
+ urlExplicit = true;
629
742
  continue;
630
743
  }
631
744
 
632
745
  if (arg === '--url' && index + 1 < args.length) {
633
746
  url = args[index + 1];
747
+ urlExplicit = true;
634
748
  index += 1;
635
749
  continue;
636
750
  }
@@ -642,7 +756,8 @@ function parseCliArgs(args) {
642
756
  installAll,
643
757
  statusOnly,
644
758
  nonInteractive,
645
- url
759
+ url,
760
+ urlExplicit
646
761
  };
647
762
  }
648
763
 
@@ -654,26 +769,101 @@ function splitCsvArg(value) {
654
769
  }
655
770
 
656
771
  function resolveCommandLineSpec(platform, url, options = {}) {
657
- const packageArgs = ['-y', '@phidiassj/aiyoperps-mcp-bridge'];
772
+ const bridgeArgs = [];
658
773
  if (options.prewarm) {
659
- packageArgs.push('--help');
774
+ bridgeArgs.push('--help');
775
+ } else if (options.healthCheck) {
776
+ bridgeArgs.push('--health-check', '--quiet', '--url', url);
660
777
  } else {
661
- packageArgs.push('--startup-ping', '--url', url);
778
+ bridgeArgs.push('--startup-ping', '--url', url);
779
+ }
780
+
781
+ const installedScriptPath = resolveInstalledBridgeScriptPath();
782
+ if (installedScriptPath) {
783
+ return {
784
+ command: process.execPath,
785
+ args: [installedScriptPath, ...bridgeArgs]
786
+ };
662
787
  }
663
788
 
789
+ const packageArgs = ['-y', BRIDGE_PACKAGE_NAME, ...bridgeArgs];
790
+ const resolvedNpx = resolveExecutablePath(platform === 'win32' ? 'npx.cmd' : 'npx');
664
791
  if (platform === 'win32') {
665
792
  return {
666
- command: 'npx.cmd',
793
+ command: resolvedNpx || 'npx.cmd',
667
794
  args: packageArgs
668
795
  };
669
796
  }
670
797
 
671
798
  return {
672
- command: 'npx',
799
+ command: resolvedNpx || 'npx',
673
800
  args: packageArgs
674
801
  };
675
802
  }
676
803
 
804
+ async function ensureBridgeRuntimePrepared() {
805
+ if (bridgeRuntimeAttempted) {
806
+ return bridgeRuntimePrepared;
807
+ }
808
+
809
+ bridgeRuntimeAttempted = true;
810
+
811
+ if (resolveInstalledBridgeScriptPath()) {
812
+ bridgeRuntimePrepared = true;
813
+ return true;
814
+ }
815
+
816
+ const npmCommand = resolveExecutablePath(process.platform === 'win32' ? 'npm.cmd' : 'npm');
817
+ if (!npmCommand) {
818
+ bridgeRuntimeWarning = 'npm was not found in PATH, falling back to npx.';
819
+ return false;
820
+ }
821
+
822
+ const installRoot = resolveBridgeInstallRoot();
823
+ ensureParentDir(path.join(installRoot, 'placeholder'));
824
+
825
+ const result = runCommand(npmCommand, [
826
+ 'install',
827
+ '--silent',
828
+ '--no-audit',
829
+ '--no-fund',
830
+ '--prefix',
831
+ installRoot,
832
+ BRIDGE_PACKAGE_NAME
833
+ ]);
834
+
835
+ if (!result.ok) {
836
+ bridgeRuntimeWarning = result.stderr || result.error?.message || 'npm install failed, falling back to npx.';
837
+ return false;
838
+ }
839
+
840
+ if (!resolveInstalledBridgeScriptPath()) {
841
+ bridgeRuntimeWarning = 'bridge package installed, but launcher script was not found; falling back to npx.';
842
+ return false;
843
+ }
844
+
845
+ bridgeRuntimePrepared = true;
846
+ return true;
847
+ }
848
+
849
+ function resolveBridgeInstallRoot() {
850
+ if (env.AIYOPERPS_MCP_BRIDGE_DIR) {
851
+ return env.AIYOPERPS_MCP_BRIDGE_DIR;
852
+ }
853
+
854
+ if (process.platform === 'win32') {
855
+ const appData = env.LOCALAPPDATA || path.join(env.USERPROFILE || os.homedir(), 'AppData', 'Local');
856
+ return path.join(appData, 'AiyoPerps', 'mcp-bridge');
857
+ }
858
+
859
+ return path.join(os.homedir(), '.aiyoperps', 'mcp-bridge');
860
+ }
861
+
862
+ function resolveInstalledBridgeScriptPath() {
863
+ const scriptPath = path.join(resolveBridgeInstallRoot(), BRIDGE_SCRIPT_RELATIVE_PATH);
864
+ return fileExists(scriptPath) ? scriptPath : null;
865
+ }
866
+
677
867
  function resolveCodexConfigPath() {
678
868
  if (env.CODEX_CONFIG_PATH) {
679
869
  return env.CODEX_CONFIG_PATH;
@@ -738,6 +928,35 @@ function runCommand(command, args) {
738
928
  };
739
929
  }
740
930
 
931
+ function resolveExecutablePath(name) {
932
+ if (EXECUTABLE_CACHE.has(name)) {
933
+ return EXECUTABLE_CACHE.get(name);
934
+ }
935
+
936
+ const pathValue = env.PATH || '';
937
+ const separator = process.platform === 'win32' ? ';' : ':';
938
+ const extensions = process.platform === 'win32'
939
+ ? ['', '.cmd', '.exe', '.bat']
940
+ : [''];
941
+
942
+ for (const directory of pathValue.split(separator)) {
943
+ if (!directory) {
944
+ continue;
945
+ }
946
+
947
+ for (const extension of extensions) {
948
+ const candidate = path.join(directory, name.endsWith(extension) ? name : `${name}${extension}`);
949
+ if (fileExists(candidate)) {
950
+ EXECUTABLE_CACHE.set(name, candidate);
951
+ return candidate;
952
+ }
953
+ }
954
+ }
955
+
956
+ EXECUTABLE_CACHE.set(name, null);
957
+ return null;
958
+ }
959
+
741
960
  function ensureParentDir(filePath) {
742
961
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
743
962
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phidiassj/aiyoperps-mcp-installer",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Interactive installer for registering AiyoPerps MCP with supported AI agent hosts.",
5
5
  "license": "MIT",
6
6
  "type": "module",