@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 +1 -0
- package/bin/aiyoperps-mcp-installer.js +235 -16
- package/package.json +1 -1
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
|
|
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
|
|
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('\
|
|
505
|
-
|
|
506
|
-
|
|
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(
|
|
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
|
|
772
|
+
const bridgeArgs = [];
|
|
658
773
|
if (options.prewarm) {
|
|
659
|
-
|
|
774
|
+
bridgeArgs.push('--help');
|
|
775
|
+
} else if (options.healthCheck) {
|
|
776
|
+
bridgeArgs.push('--health-check', '--quiet', '--url', url);
|
|
660
777
|
} else {
|
|
661
|
-
|
|
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
|
}
|