@muhaven/mcp 0.1.2 → 0.1.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/CHANGELOG.md +201 -1
- package/bin/muhaven-mcp.cjs +45 -0
- package/dist/broker.cjs +506 -24
- package/dist/broker.d.cts +29 -1
- package/dist/broker.d.ts +29 -1
- package/dist/broker.js +513 -30
- package/dist/index.cjs +124 -20
- package/dist/index.d.cts +143 -13
- package/dist/index.d.ts +143 -13
- package/dist/index.js +120 -21
- package/manifest.json +2 -2
- package/package.json +1 -1
package/dist/broker.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import path, { join, dirname, resolve } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
1
3
|
import { platform, release, hostname, homedir } from 'os';
|
|
2
|
-
import { exec } from 'child_process';
|
|
3
|
-
import { join, dirname } from 'path';
|
|
4
|
+
import { exec, spawn } from 'child_process';
|
|
4
5
|
import { connect, createServer } from 'net';
|
|
5
6
|
import { mkdir, chmod, writeFile, readFile, unlink, stat } from 'fs/promises';
|
|
6
|
-
import { privateKeyToAccount } from 'viem/accounts';
|
|
7
|
+
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
10
|
+
var getDirname = () => path.dirname(getFilename());
|
|
11
|
+
var __dirname$1 = /* @__PURE__ */ getDirname();
|
|
9
12
|
var DEFAULT_BACKEND_URL = "https://api.muhaven.app";
|
|
10
13
|
var DEFAULT_DASHBOARD_URL = "https://muhaven.app";
|
|
11
14
|
var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
@@ -71,23 +74,26 @@ function loadMcpConfig(env = process.env) {
|
|
|
71
74
|
}
|
|
72
75
|
var PRIVKEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
73
76
|
function loadBrokerConfig(env = process.env) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
throw new Error("MUHAVEN_BROKER_SESSION_KEY must be a 0x-prefixed 32-byte hex string");
|
|
77
|
+
const sessionKeyHexRaw = env.MUHAVEN_BROKER_SESSION_KEY;
|
|
78
|
+
let sessionKeyHex;
|
|
79
|
+
if (sessionKeyHexRaw && sessionKeyHexRaw.length > 0) {
|
|
80
|
+
if (!PRIVKEY_HEX_RE.test(sessionKeyHexRaw)) {
|
|
81
|
+
throw new Error("MUHAVEN_BROKER_SESSION_KEY must be a 0x-prefixed 32-byte hex string");
|
|
82
|
+
}
|
|
83
|
+
sessionKeyHex = sessionKeyHexRaw;
|
|
82
84
|
}
|
|
83
85
|
const endpoint = env.MUHAVEN_BROKER_ENDPOINT ?? defaultBrokerEndpoint();
|
|
84
86
|
const maxRequestBytes = readEnvInt("MUHAVEN_BROKER_MAX_BYTES", DEFAULT_BROKER_MAX_BYTES, env);
|
|
85
87
|
const requestTimeoutMs = readEnvInt("MUHAVEN_BROKER_TIMEOUT_MS", DEFAULT_BROKER_TIMEOUT_MS, env);
|
|
88
|
+
const backendBaseUrl = trimTrailingSlash(env.MUHAVEN_BACKEND_URL ?? DEFAULT_BACKEND_URL);
|
|
89
|
+
const dashboardBaseUrl = trimTrailingSlash(env.MUHAVEN_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL);
|
|
86
90
|
return {
|
|
87
91
|
endpoint,
|
|
88
92
|
sessionKeyHex,
|
|
89
93
|
maxRequestBytes,
|
|
90
|
-
requestTimeoutMs
|
|
94
|
+
requestTimeoutMs,
|
|
95
|
+
backendBaseUrl,
|
|
96
|
+
dashboardBaseUrl
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
99
|
var BrokerClientError = class extends Error {
|
|
@@ -415,8 +421,8 @@ var OsKeystore = class {
|
|
|
415
421
|
}
|
|
416
422
|
};
|
|
417
423
|
var FileKeystore = class {
|
|
418
|
-
constructor(
|
|
419
|
-
this.path =
|
|
424
|
+
constructor(path2) {
|
|
425
|
+
this.path = path2;
|
|
420
426
|
}
|
|
421
427
|
path;
|
|
422
428
|
backend = "file";
|
|
@@ -520,7 +526,7 @@ async function openKeystore(options = {}) {
|
|
|
520
526
|
}
|
|
521
527
|
|
|
522
528
|
// src/broker/protocol.ts
|
|
523
|
-
var BROKER_PROTOCOL_VERSION = "0.
|
|
529
|
+
var BROKER_PROTOCOL_VERSION = "0.3.0";
|
|
524
530
|
var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
525
531
|
var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
526
532
|
function isHashHex(value) {
|
|
@@ -606,6 +612,21 @@ function parseBrokerRequest(line) {
|
|
|
606
612
|
function serializeResponse(res) {
|
|
607
613
|
return JSON.stringify(res) + "\n";
|
|
608
614
|
}
|
|
615
|
+
var MissingSessionKeyError = class extends Error {
|
|
616
|
+
constructor() {
|
|
617
|
+
super(
|
|
618
|
+
"session_key_unavailable: daemon booted in read-only posture (no MUHAVEN_BROKER_SESSION_KEY at env-load time). Mint a session key via the dashboard /agent/policy/transition flow, set MUHAVEN_BROKER_SESSION_KEY, and restart the daemon. (Note: `muhaven-broker login` mints a JWT, NOT a session key \u2014 do not loop on that command for this error.)"
|
|
619
|
+
);
|
|
620
|
+
this.name = "MissingSessionKeyError";
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
624
|
+
var NullSigner = class {
|
|
625
|
+
address = ZERO_ADDRESS;
|
|
626
|
+
async signHash(_hash) {
|
|
627
|
+
throw new MissingSessionKeyError();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
609
630
|
var ViemSigner = class {
|
|
610
631
|
account;
|
|
611
632
|
constructor(privateKey) {
|
|
@@ -622,7 +643,7 @@ var ViemSigner = class {
|
|
|
622
643
|
// src/broker/daemon.ts
|
|
623
644
|
var noopLogger = (_e) => {
|
|
624
645
|
};
|
|
625
|
-
async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3)) {
|
|
646
|
+
async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}) {
|
|
626
647
|
switch (req.type) {
|
|
627
648
|
case "hello": {
|
|
628
649
|
let hasJwt = false;
|
|
@@ -632,16 +653,26 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
632
653
|
} catch {
|
|
633
654
|
hasJwt = false;
|
|
634
655
|
}
|
|
656
|
+
const hasSessionKey = options.hasSessionKey ?? true;
|
|
635
657
|
return {
|
|
636
658
|
type: "hello",
|
|
637
659
|
version: BROKER_PROTOCOL_VERSION,
|
|
638
660
|
sessionKeyAddress: signer.address,
|
|
639
|
-
hasJwt
|
|
661
|
+
hasJwt,
|
|
662
|
+
hasSessionKey,
|
|
663
|
+
...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
|
|
640
664
|
};
|
|
641
665
|
}
|
|
642
666
|
case "sign_hash": {
|
|
643
|
-
|
|
644
|
-
|
|
667
|
+
try {
|
|
668
|
+
const signature = await signer.signHash(req.hash);
|
|
669
|
+
return { type: "sign_hash", signature, signerAddress: signer.address };
|
|
670
|
+
} catch (err) {
|
|
671
|
+
if (err instanceof MissingSessionKeyError) {
|
|
672
|
+
return errorResponse("session_key_unavailable", err.message);
|
|
673
|
+
}
|
|
674
|
+
throw err;
|
|
675
|
+
}
|
|
645
676
|
}
|
|
646
677
|
case "store_jwt": {
|
|
647
678
|
try {
|
|
@@ -713,9 +744,24 @@ var BrokerDaemon = class {
|
|
|
713
744
|
log;
|
|
714
745
|
config;
|
|
715
746
|
keystore;
|
|
747
|
+
/**
|
|
748
|
+
* Whether a session-key private half is actually loaded. `false` =
|
|
749
|
+
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
750
|
+
* at env-load time) and uses a `NullSigner` whose `signHash` throws.
|
|
751
|
+
*/
|
|
752
|
+
hasSessionKey;
|
|
716
753
|
constructor(options) {
|
|
717
754
|
this.config = options.config;
|
|
718
|
-
|
|
755
|
+
if (options.signer) {
|
|
756
|
+
this.signer = options.signer;
|
|
757
|
+
this.hasSessionKey = true;
|
|
758
|
+
} else if (options.config.sessionKeyHex) {
|
|
759
|
+
this.signer = new ViemSigner(options.config.sessionKeyHex);
|
|
760
|
+
this.hasSessionKey = true;
|
|
761
|
+
} else {
|
|
762
|
+
this.signer = new NullSigner();
|
|
763
|
+
this.hasSessionKey = false;
|
|
764
|
+
}
|
|
719
765
|
this.keystore = options.keystore ?? null;
|
|
720
766
|
this.log = options.logger ?? noopLogger;
|
|
721
767
|
this.server = createServer((socket) => this.onConnection(socket));
|
|
@@ -753,6 +799,7 @@ var BrokerDaemon = class {
|
|
|
753
799
|
meta: {
|
|
754
800
|
endpoint: this.config.endpoint,
|
|
755
801
|
signer: this.signer.address,
|
|
802
|
+
hasSessionKey: this.hasSessionKey,
|
|
756
803
|
keystore: this.keystore.backend,
|
|
757
804
|
version: BROKER_PROTOCOL_VERSION
|
|
758
805
|
}
|
|
@@ -834,7 +881,19 @@ var BrokerDaemon = class {
|
|
|
834
881
|
return;
|
|
835
882
|
}
|
|
836
883
|
try {
|
|
837
|
-
const res = await handleBrokerRequest(
|
|
884
|
+
const res = await handleBrokerRequest(
|
|
885
|
+
parsed,
|
|
886
|
+
this.signer,
|
|
887
|
+
this.keystore,
|
|
888
|
+
void 0,
|
|
889
|
+
{
|
|
890
|
+
hasSessionKey: this.hasSessionKey,
|
|
891
|
+
effectiveConfig: {
|
|
892
|
+
backendBaseUrl: this.config.backendBaseUrl,
|
|
893
|
+
dashboardBaseUrl: this.config.dashboardBaseUrl
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
);
|
|
838
897
|
socket.end(serializeResponse(res));
|
|
839
898
|
} catch (err) {
|
|
840
899
|
this.log({
|
|
@@ -850,6 +909,15 @@ var BrokerDaemon = class {
|
|
|
850
909
|
};
|
|
851
910
|
async function runBrokerDaemonCli() {
|
|
852
911
|
const config = loadBrokerConfig();
|
|
912
|
+
if (!config.sessionKeyHex) {
|
|
913
|
+
process.stderr.write(
|
|
914
|
+
JSON.stringify({
|
|
915
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
916
|
+
level: "info",
|
|
917
|
+
msg: "broker booting in read-only posture (no MUHAVEN_BROKER_SESSION_KEY)"
|
|
918
|
+
}) + "\n"
|
|
919
|
+
);
|
|
920
|
+
}
|
|
853
921
|
const daemon = new BrokerDaemon({
|
|
854
922
|
config,
|
|
855
923
|
logger: (e) => {
|
|
@@ -867,6 +935,332 @@ async function runBrokerDaemonCli() {
|
|
|
867
935
|
await new Promise(() => {
|
|
868
936
|
});
|
|
869
937
|
}
|
|
938
|
+
var DANGEROUS_NODE_ENV_VARS = [
|
|
939
|
+
"NODE_OPTIONS",
|
|
940
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
941
|
+
"NODE_EXTRA_CA_CERTS",
|
|
942
|
+
"NODE_PATH"
|
|
943
|
+
];
|
|
944
|
+
function applyEnvDefaults(input) {
|
|
945
|
+
const { env } = input;
|
|
946
|
+
const platformId = input.platformId ?? process.platform;
|
|
947
|
+
const osRelease = input.osRelease ?? release();
|
|
948
|
+
const toSet = {};
|
|
949
|
+
const preserved = [];
|
|
950
|
+
const defaultIfUnset = (name, value) => {
|
|
951
|
+
if (env[name] && env[name].length > 0) {
|
|
952
|
+
preserved.push(name);
|
|
953
|
+
} else {
|
|
954
|
+
toSet[name] = value;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
defaultIfUnset("MUHAVEN_BACKEND_URL", "https://api.muhaven.app");
|
|
958
|
+
defaultIfUnset("MUHAVEN_DASHBOARD_URL", "https://muhaven.app");
|
|
959
|
+
const wantFileKeyring = platformId === "win32" || platformId === "linux" && (env.WSL_DISTRO_NAME !== void 0 || /microsoft/i.test(osRelease)) || env.REMOTE_CONTAINERS === "true" || env.CODESPACES === "true" || env.SSH_CONNECTION !== void 0;
|
|
960
|
+
if (wantFileKeyring) {
|
|
961
|
+
defaultIfUnset("MUHAVEN_KEYRING", "file");
|
|
962
|
+
} else if (env.MUHAVEN_KEYRING) {
|
|
963
|
+
preserved.push("MUHAVEN_KEYRING");
|
|
964
|
+
}
|
|
965
|
+
return { toSet, preserved };
|
|
966
|
+
}
|
|
967
|
+
function mintSessionKey() {
|
|
968
|
+
return generatePrivateKey();
|
|
969
|
+
}
|
|
970
|
+
function decideSetupAction(input) {
|
|
971
|
+
if (input.hello === null) return "spawn_and_login";
|
|
972
|
+
if (!input.hello.hasJwt) return "login_only";
|
|
973
|
+
return "already_ready";
|
|
974
|
+
}
|
|
975
|
+
function spawnDaemon(options) {
|
|
976
|
+
const sanitized = {};
|
|
977
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
978
|
+
if (!DANGEROUS_NODE_ENV_VARS.includes(k)) {
|
|
979
|
+
sanitized[k] = v;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const merged = { ...sanitized, ...options.env };
|
|
983
|
+
const child = spawn(process.execPath, [options.binPath], {
|
|
984
|
+
detached: true,
|
|
985
|
+
stdio: "ignore",
|
|
986
|
+
windowsHide: true,
|
|
987
|
+
env: merged
|
|
988
|
+
});
|
|
989
|
+
child.unref();
|
|
990
|
+
if (child.pid === void 0) {
|
|
991
|
+
throw new Error("failed to spawn muhaven-broker daemon \u2014 child pid is undefined");
|
|
992
|
+
}
|
|
993
|
+
return child.pid;
|
|
994
|
+
}
|
|
995
|
+
function validateHttpUrlFlag(name, value) {
|
|
996
|
+
let parsed;
|
|
997
|
+
try {
|
|
998
|
+
parsed = new URL(value);
|
|
999
|
+
} catch {
|
|
1000
|
+
return `${name} is not a valid URL: ${value}`;
|
|
1001
|
+
}
|
|
1002
|
+
if (parsed.protocol === "https:") return null;
|
|
1003
|
+
if (parsed.protocol === "http:") {
|
|
1004
|
+
const host = parsed.hostname;
|
|
1005
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") return null;
|
|
1006
|
+
return `${name} must use https:// (got http:// to ${host} \u2014 refusing to ship JWT cleartext)`;
|
|
1007
|
+
}
|
|
1008
|
+
return `${name} must use https:// (got ${parsed.protocol})`;
|
|
1009
|
+
}
|
|
1010
|
+
function validateBrokerEndpointFlag(value, platformId) {
|
|
1011
|
+
if (!value || value.length === 0) {
|
|
1012
|
+
return "--broker-endpoint cannot be empty";
|
|
1013
|
+
}
|
|
1014
|
+
if (platformId === "win32") {
|
|
1015
|
+
if (value.startsWith("\\\\.\\pipe\\") || value.startsWith("//./pipe/")) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
return "--broker-endpoint on Windows must be a named pipe path (\\\\.\\pipe\\...)";
|
|
1019
|
+
}
|
|
1020
|
+
if (!value.startsWith("/")) {
|
|
1021
|
+
return "--broker-endpoint on POSIX must be an absolute path (e.g. /run/muhaven/broker.sock)";
|
|
1022
|
+
}
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1026
|
+
async function waitForBroker(options) {
|
|
1027
|
+
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
1028
|
+
const intervalMs = options.intervalMs ?? 200;
|
|
1029
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
1030
|
+
const now = options.now ?? Date.now;
|
|
1031
|
+
const deadline = now() + timeoutMs;
|
|
1032
|
+
let lastErr = null;
|
|
1033
|
+
while (now() < deadline) {
|
|
1034
|
+
try {
|
|
1035
|
+
const hello = await options.broker.hello();
|
|
1036
|
+
return { hasJwt: hello.hasJwt };
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
lastErr = err;
|
|
1039
|
+
if (now() + intervalMs < deadline) {
|
|
1040
|
+
await sleep(intervalMs);
|
|
1041
|
+
} else {
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
`muhaven-broker daemon did not become reachable within ${timeoutMs}ms: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
function parseSetupFlags(argv) {
|
|
1051
|
+
let foreground = false;
|
|
1052
|
+
let noLaunchBrowser = false;
|
|
1053
|
+
let brokerEndpoint;
|
|
1054
|
+
let backendBaseUrl;
|
|
1055
|
+
let dashboardBaseUrl;
|
|
1056
|
+
let skipLogin = false;
|
|
1057
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1058
|
+
const a = argv[i];
|
|
1059
|
+
if (a === "--foreground" || a === "-f") foreground = true;
|
|
1060
|
+
else if (a === "--no-launch-browser") noLaunchBrowser = true;
|
|
1061
|
+
else if (a === "--skip-login") skipLogin = true;
|
|
1062
|
+
else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
|
|
1063
|
+
else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
|
|
1064
|
+
else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
|
|
1065
|
+
else throw new Error(`unknown flag: ${a}`);
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
foreground,
|
|
1069
|
+
noLaunchBrowser,
|
|
1070
|
+
brokerEndpoint,
|
|
1071
|
+
backendBaseUrl,
|
|
1072
|
+
dashboardBaseUrl,
|
|
1073
|
+
skipLogin
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async function runSetup(argv, deps) {
|
|
1077
|
+
let flags;
|
|
1078
|
+
try {
|
|
1079
|
+
flags = parseSetupFlags(argv);
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
deps.printErr(`error: ${err.message}`);
|
|
1082
|
+
deps.printErr(
|
|
1083
|
+
"usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]"
|
|
1084
|
+
);
|
|
1085
|
+
return 2;
|
|
1086
|
+
}
|
|
1087
|
+
if (flags.backendBaseUrl) {
|
|
1088
|
+
const err = validateHttpUrlFlag("--backend-base-url", flags.backendBaseUrl);
|
|
1089
|
+
if (err) {
|
|
1090
|
+
deps.printErr(`error: ${err}`);
|
|
1091
|
+
return 2;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (flags.dashboardBaseUrl) {
|
|
1095
|
+
const err = validateHttpUrlFlag("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
1096
|
+
if (err) {
|
|
1097
|
+
deps.printErr(`error: ${err}`);
|
|
1098
|
+
return 2;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (flags.brokerEndpoint) {
|
|
1102
|
+
const err = validateBrokerEndpointFlag(flags.brokerEndpoint, deps.platformId);
|
|
1103
|
+
if (err) {
|
|
1104
|
+
deps.printErr(`error: ${err}`);
|
|
1105
|
+
return 2;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const overrides = applyEnvDefaults({
|
|
1109
|
+
env: deps.env,
|
|
1110
|
+
platformId: deps.platformId,
|
|
1111
|
+
osRelease: deps.osRelease
|
|
1112
|
+
});
|
|
1113
|
+
const effectiveEnv = {};
|
|
1114
|
+
for (const [k, v] of Object.entries(deps.env)) {
|
|
1115
|
+
if (typeof v === "string") effectiveEnv[k] = v;
|
|
1116
|
+
}
|
|
1117
|
+
for (const [k, v] of Object.entries(overrides.toSet)) {
|
|
1118
|
+
effectiveEnv[k] = v;
|
|
1119
|
+
}
|
|
1120
|
+
if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
|
|
1121
|
+
if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
|
|
1122
|
+
if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
|
|
1123
|
+
for (const name of overrides.preserved) {
|
|
1124
|
+
deps.print(`Env preserved: ${name} (set in your shell)`);
|
|
1125
|
+
}
|
|
1126
|
+
for (const [k, v] of Object.entries(overrides.toSet)) {
|
|
1127
|
+
deps.print(`Env defaulted: ${k}=${v}`);
|
|
1128
|
+
}
|
|
1129
|
+
let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
|
|
1130
|
+
let mintedKey = false;
|
|
1131
|
+
if (!sessionKey || sessionKey === "") {
|
|
1132
|
+
sessionKey = deps.mintSessionKey();
|
|
1133
|
+
mintedKey = true;
|
|
1134
|
+
deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
|
|
1135
|
+
} else {
|
|
1136
|
+
deps.print("Session key: using MUHAVEN_BROKER_SESSION_KEY from env.");
|
|
1137
|
+
}
|
|
1138
|
+
effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
|
|
1139
|
+
if (flags.foreground) {
|
|
1140
|
+
deps.print("Foreground mode \u2014 running daemon attached to this shell. Ctrl-C to stop.");
|
|
1141
|
+
const restorationKeys = [
|
|
1142
|
+
...Object.keys(overrides.toSet),
|
|
1143
|
+
"MUHAVEN_BROKER_SESSION_KEY",
|
|
1144
|
+
...flags.brokerEndpoint ? ["MUHAVEN_BROKER_ENDPOINT"] : [],
|
|
1145
|
+
...flags.backendBaseUrl ? ["MUHAVEN_BACKEND_URL"] : [],
|
|
1146
|
+
...flags.dashboardBaseUrl ? ["MUHAVEN_DASHBOARD_URL"] : []
|
|
1147
|
+
];
|
|
1148
|
+
const originalValues = {};
|
|
1149
|
+
for (const k of restorationKeys) {
|
|
1150
|
+
originalValues[k] = process.env[k];
|
|
1151
|
+
process.env[k] = effectiveEnv[k];
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
await deps.runForegroundDaemon();
|
|
1155
|
+
} finally {
|
|
1156
|
+
for (const k of restorationKeys) {
|
|
1157
|
+
if (originalValues[k] === void 0) delete process.env[k];
|
|
1158
|
+
else process.env[k] = originalValues[k];
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return 0;
|
|
1162
|
+
}
|
|
1163
|
+
const config = loadMcpConfig(effectiveEnv);
|
|
1164
|
+
const broker = deps.newBrokerClient(config.brokerEndpoint, config.brokerTimeoutMs);
|
|
1165
|
+
let helloProbe = null;
|
|
1166
|
+
try {
|
|
1167
|
+
helloProbe = await broker.hello();
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
const action = decideSetupAction({ hello: helloProbe });
|
|
1171
|
+
let daemonPid = null;
|
|
1172
|
+
if (action === "spawn_and_login") {
|
|
1173
|
+
deps.print("Broker daemon: not running, starting one (detached) ...");
|
|
1174
|
+
daemonPid = deps.spawnDaemon({
|
|
1175
|
+
binPath: deps.resolveBinPath(),
|
|
1176
|
+
env: {
|
|
1177
|
+
// Explicit env for the spawned daemon. Includes every var that the
|
|
1178
|
+
// daemon's loadBrokerConfig will read, sourced from our resolved
|
|
1179
|
+
// effectiveEnv (NOT from process.env). spawnDaemon will sanitize
|
|
1180
|
+
// process.env-inherited values further (strips NODE_OPTIONS etc.).
|
|
1181
|
+
...overrides.toSet,
|
|
1182
|
+
MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
|
|
1183
|
+
MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
|
|
1184
|
+
MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
|
|
1185
|
+
MUHAVEN_BROKER_SESSION_KEY: sessionKey
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
try {
|
|
1189
|
+
const readyHello = await deps.waitForBroker({ broker });
|
|
1190
|
+
helloProbe = readyHello;
|
|
1191
|
+
deps.print(`Broker daemon: ready (PID ${daemonPid}, endpoint ${config.brokerEndpoint}).`);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
deps.printErr(err.message);
|
|
1194
|
+
deps.printErr(
|
|
1195
|
+
" hint: re-run `muhaven-broker setup` after checking that no other broker is bound to the same endpoint."
|
|
1196
|
+
);
|
|
1197
|
+
return 1;
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
deps.print(`Broker daemon: already reachable at ${config.brokerEndpoint}.`);
|
|
1201
|
+
}
|
|
1202
|
+
const needsLogin = !flags.skipLogin && !(helloProbe && helloProbe.hasJwt);
|
|
1203
|
+
if (flags.skipLogin) {
|
|
1204
|
+
deps.print("Login: skipped per --skip-login.");
|
|
1205
|
+
} else if (helloProbe && helloProbe.hasJwt) {
|
|
1206
|
+
deps.print("Login: skipped \u2014 JWT already in keystore.");
|
|
1207
|
+
}
|
|
1208
|
+
if (needsLogin) {
|
|
1209
|
+
const loginArgv = [];
|
|
1210
|
+
if (flags.noLaunchBrowser) loginArgv.push("--no-launch-browser");
|
|
1211
|
+
if (flags.brokerEndpoint) {
|
|
1212
|
+
loginArgv.push("--broker-endpoint", flags.brokerEndpoint);
|
|
1213
|
+
}
|
|
1214
|
+
if (flags.backendBaseUrl) {
|
|
1215
|
+
loginArgv.push("--backend-base-url", flags.backendBaseUrl);
|
|
1216
|
+
}
|
|
1217
|
+
if (flags.dashboardBaseUrl) {
|
|
1218
|
+
loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
1219
|
+
}
|
|
1220
|
+
const restorationKeys = ["MUHAVEN_BACKEND_URL", "MUHAVEN_DASHBOARD_URL", "MUHAVEN_BROKER_ENDPOINT"];
|
|
1221
|
+
const originalValues = {};
|
|
1222
|
+
for (const k of restorationKeys) {
|
|
1223
|
+
originalValues[k] = process.env[k];
|
|
1224
|
+
if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
|
|
1225
|
+
}
|
|
1226
|
+
let code;
|
|
1227
|
+
try {
|
|
1228
|
+
code = await deps.runLogin(loginArgv);
|
|
1229
|
+
} finally {
|
|
1230
|
+
for (const k of restorationKeys) {
|
|
1231
|
+
if (originalValues[k] === void 0) delete process.env[k];
|
|
1232
|
+
else process.env[k] = originalValues[k];
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (code !== 0) {
|
|
1236
|
+
deps.printErr(
|
|
1237
|
+
"Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
|
|
1238
|
+
);
|
|
1239
|
+
if (daemonPid !== null) {
|
|
1240
|
+
const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
|
|
1241
|
+
deps.printErr(` (daemon PID ${daemonPid}; stop with: ${killCmd})`);
|
|
1242
|
+
}
|
|
1243
|
+
return code;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
deps.print("");
|
|
1247
|
+
deps.print("================================");
|
|
1248
|
+
deps.print("Setup complete.");
|
|
1249
|
+
if (daemonPid !== null) {
|
|
1250
|
+
deps.print(` Daemon PID : ${daemonPid}`);
|
|
1251
|
+
const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
|
|
1252
|
+
deps.print(` Stop daemon: ${killCmd}`);
|
|
1253
|
+
} else {
|
|
1254
|
+
deps.print(" Daemon : already running");
|
|
1255
|
+
}
|
|
1256
|
+
deps.print(` Endpoint : ${config.brokerEndpoint}`);
|
|
1257
|
+
deps.print(" Sign out : muhaven-broker logout (clears JWT, leaves daemon running)");
|
|
1258
|
+
if (mintedKey) {
|
|
1259
|
+
deps.print(" Session key: ephemeral \u2014 minted by setup, lives only in the daemon process.");
|
|
1260
|
+
}
|
|
1261
|
+
deps.print("================================");
|
|
1262
|
+
return 0;
|
|
1263
|
+
}
|
|
870
1264
|
|
|
871
1265
|
// src/broker/cli.ts
|
|
872
1266
|
function print(line) {
|
|
@@ -876,7 +1270,7 @@ function printErr(line) {
|
|
|
876
1270
|
process.stderr.write(line + "\n");
|
|
877
1271
|
}
|
|
878
1272
|
function detectMcpHost() {
|
|
879
|
-
return process.env.MCP_HOST_NAME ?? process.env.CLAUDE_CODE_HOST ??
|
|
1273
|
+
return process.env.MCP_HOST_NAME ?? process.env.CLAUDE_CODE_HOST ?? "muhaven-broker-cli";
|
|
880
1274
|
}
|
|
881
1275
|
function detectEnvironment() {
|
|
882
1276
|
const warnings = [];
|
|
@@ -902,9 +1296,11 @@ function parseLoginFlags(argv) {
|
|
|
902
1296
|
let brokerEndpoint;
|
|
903
1297
|
let backendBaseUrl;
|
|
904
1298
|
let dashboardBaseUrl;
|
|
1299
|
+
let fromDaemon = false;
|
|
905
1300
|
for (let i = 0; i < argv.length; i++) {
|
|
906
1301
|
const a = argv[i];
|
|
907
1302
|
if (a === "--no-launch-browser") noLaunchBrowser = true;
|
|
1303
|
+
else if (a === "--from-daemon") fromDaemon = true;
|
|
908
1304
|
else if (a === "--broker-endpoint" && i + 1 < argv.length) {
|
|
909
1305
|
brokerEndpoint = argv[++i];
|
|
910
1306
|
} else if (a === "--backend-base-url" && i + 1 < argv.length) {
|
|
@@ -915,7 +1311,12 @@ function parseLoginFlags(argv) {
|
|
|
915
1311
|
throw new Error(`unknown flag: ${a}`);
|
|
916
1312
|
}
|
|
917
1313
|
}
|
|
918
|
-
|
|
1314
|
+
if (fromDaemon && (backendBaseUrl || dashboardBaseUrl)) {
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
"--from-daemon is mutually exclusive with --backend-base-url / --dashboard-base-url"
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
return { noLaunchBrowser, brokerEndpoint, backendBaseUrl, dashboardBaseUrl, fromDaemon };
|
|
919
1320
|
}
|
|
920
1321
|
async function tryLaunchBrowser(url) {
|
|
921
1322
|
return new Promise((resolve) => {
|
|
@@ -929,7 +1330,9 @@ async function runLogin(argv) {
|
|
|
929
1330
|
flags = parseLoginFlags(argv);
|
|
930
1331
|
} catch (err) {
|
|
931
1332
|
printErr(`error: ${err.message}`);
|
|
932
|
-
printErr(
|
|
1333
|
+
printErr(
|
|
1334
|
+
"usage: muhaven-broker login [--no-launch-browser] [--broker-endpoint PATH] [--from-daemon | (--backend-base-url URL --dashboard-base-url URL)]"
|
|
1335
|
+
);
|
|
933
1336
|
return 2;
|
|
934
1337
|
}
|
|
935
1338
|
const env = process.env;
|
|
@@ -943,8 +1346,9 @@ async function runLogin(argv) {
|
|
|
943
1346
|
endpoint: config.brokerEndpoint,
|
|
944
1347
|
timeoutMs: config.brokerTimeoutMs
|
|
945
1348
|
});
|
|
1349
|
+
let helloResult;
|
|
946
1350
|
try {
|
|
947
|
-
await broker.hello();
|
|
1351
|
+
helloResult = await broker.hello();
|
|
948
1352
|
} catch (err) {
|
|
949
1353
|
printErr(
|
|
950
1354
|
`cannot reach muhaven-broker daemon at ${config.brokerEndpoint}: ${err.message}`
|
|
@@ -952,9 +1356,42 @@ async function runLogin(argv) {
|
|
|
952
1356
|
printErr("hint: start the daemon first (`muhaven-broker` with no subcommand).");
|
|
953
1357
|
return 1;
|
|
954
1358
|
}
|
|
1359
|
+
let backendBaseUrl = config.backendBaseUrl;
|
|
1360
|
+
let dashboardBaseUrl = config.dashboardBaseUrl;
|
|
1361
|
+
if (flags.fromDaemon) {
|
|
1362
|
+
if (!helloResult.effectiveConfig) {
|
|
1363
|
+
printErr(
|
|
1364
|
+
"--from-daemon requested but broker did not return effectiveConfig (daemon is older than protocol 0.3.0). Upgrade the daemon (`@muhaven/mcp@0.1.3+`) or drop the flag."
|
|
1365
|
+
);
|
|
1366
|
+
return 1;
|
|
1367
|
+
}
|
|
1368
|
+
const daemonBackend = helloResult.effectiveConfig.backendBaseUrl;
|
|
1369
|
+
const daemonDashboard = helloResult.effectiveConfig.dashboardBaseUrl;
|
|
1370
|
+
if (!daemonBackend || !daemonDashboard) {
|
|
1371
|
+
printErr(
|
|
1372
|
+
"--from-daemon: daemon returned an empty backend/dashboard URL \u2014 refusing to proceed."
|
|
1373
|
+
);
|
|
1374
|
+
return 1;
|
|
1375
|
+
}
|
|
1376
|
+
if (daemonBackend !== config.backendBaseUrl) {
|
|
1377
|
+
print(
|
|
1378
|
+
`\u26A0 daemon backend (${daemonBackend}) differs from CLI env (${config.backendBaseUrl}). Using daemon's value per --from-daemon.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
if (daemonDashboard !== config.dashboardBaseUrl) {
|
|
1382
|
+
print(
|
|
1383
|
+
`\u26A0 daemon dashboard (${daemonDashboard}) differs from CLI env (${config.dashboardBaseUrl}). Using daemon's value per --from-daemon.`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
backendBaseUrl = daemonBackend;
|
|
1387
|
+
dashboardBaseUrl = daemonDashboard;
|
|
1388
|
+
print(`Using daemon's effective config:`);
|
|
1389
|
+
print(` backend: ${backendBaseUrl}`);
|
|
1390
|
+
print(` dashboard: ${dashboardBaseUrl}`);
|
|
1391
|
+
}
|
|
955
1392
|
const flow = new DeviceFlowClient({
|
|
956
|
-
backendBaseUrl
|
|
957
|
-
dashboardBaseUrl
|
|
1393
|
+
backendBaseUrl,
|
|
1394
|
+
dashboardBaseUrl,
|
|
958
1395
|
requesterMetadata: {
|
|
959
1396
|
processName: detectMcpHost(),
|
|
960
1397
|
hostname: hostname(),
|
|
@@ -1069,7 +1506,13 @@ async function runDoctor() {
|
|
|
1069
1506
|
});
|
|
1070
1507
|
try {
|
|
1071
1508
|
const h = await broker.hello();
|
|
1072
|
-
|
|
1509
|
+
const hasKey = h.hasSessionKey ?? true;
|
|
1510
|
+
const keyTag = hasKey ? `signer ${h.sessionKeyAddress}` : "NO SESSION KEY (read-only posture)";
|
|
1511
|
+
print(`Broker daemon : reachable (proto v${h.version}, ${keyTag}, hasJwt=${h.hasJwt})`);
|
|
1512
|
+
if (h.effectiveConfig) {
|
|
1513
|
+
print(`Daemon backend URL: ${h.effectiveConfig.backendBaseUrl}`);
|
|
1514
|
+
print(`Daemon dashboard : ${h.effectiveConfig.dashboardBaseUrl}`);
|
|
1515
|
+
}
|
|
1073
1516
|
return 0;
|
|
1074
1517
|
} catch (err) {
|
|
1075
1518
|
print(`Broker daemon : NOT reachable (${err.message})`);
|
|
@@ -1081,10 +1524,44 @@ function printUsage() {
|
|
|
1081
1524
|
print("usage: muhaven-broker [<subcommand>] [options]");
|
|
1082
1525
|
print("");
|
|
1083
1526
|
print(" (no subcommand) Run the daemon (production mode)");
|
|
1527
|
+
print(" setup One-shot install: env defaults + session key + detached daemon + login");
|
|
1528
|
+
print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
|
|
1529
|
+
print(" [--skip-login] starts the daemon but lets you run login later");
|
|
1530
|
+
print(" [--no-launch-browser] pass-through to login");
|
|
1084
1531
|
print(" login Acquire a JWT via the device-code flow + store in keystore");
|
|
1532
|
+
print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
|
|
1085
1533
|
print(" logout Clear the JWT from the keystore");
|
|
1086
1534
|
print(" doctor Print environment + keystore + reachability report");
|
|
1087
1535
|
print(" -h, --help Show this help");
|
|
1536
|
+
print(" -v, --version Print the @muhaven/mcp package version");
|
|
1537
|
+
}
|
|
1538
|
+
function getBrokerPackageVersion() {
|
|
1539
|
+
{
|
|
1540
|
+
return "0.1.4";
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
function printVersion() {
|
|
1544
|
+
print(`muhaven-broker @muhaven/mcp@${getBrokerPackageVersion()}`);
|
|
1545
|
+
}
|
|
1546
|
+
function resolveBrokerBinPath() {
|
|
1547
|
+
return resolve(__dirname$1, "..", "bin", "muhaven-broker.cjs");
|
|
1548
|
+
}
|
|
1549
|
+
async function runSetup2(argv) {
|
|
1550
|
+
const deps = {
|
|
1551
|
+
print,
|
|
1552
|
+
printErr,
|
|
1553
|
+
mintSessionKey,
|
|
1554
|
+
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
1555
|
+
spawnDaemon,
|
|
1556
|
+
waitForBroker,
|
|
1557
|
+
runLogin,
|
|
1558
|
+
runForegroundDaemon: runBrokerDaemonCli,
|
|
1559
|
+
resolveBinPath: resolveBrokerBinPath,
|
|
1560
|
+
env: process.env,
|
|
1561
|
+
platformId: process.platform,
|
|
1562
|
+
osRelease: release()
|
|
1563
|
+
};
|
|
1564
|
+
return runSetup(argv, deps);
|
|
1088
1565
|
}
|
|
1089
1566
|
async function runCli(argv) {
|
|
1090
1567
|
const [sub, ...rest] = argv;
|
|
@@ -1092,6 +1569,8 @@ async function runCli(argv) {
|
|
|
1092
1569
|
case void 0:
|
|
1093
1570
|
await runBrokerDaemonCli();
|
|
1094
1571
|
return 0;
|
|
1572
|
+
case "setup":
|
|
1573
|
+
return runSetup2(rest);
|
|
1095
1574
|
case "login":
|
|
1096
1575
|
return runLogin(rest);
|
|
1097
1576
|
case "logout":
|
|
@@ -1102,6 +1581,10 @@ async function runCli(argv) {
|
|
|
1102
1581
|
case "--help":
|
|
1103
1582
|
printUsage();
|
|
1104
1583
|
return 0;
|
|
1584
|
+
case "-v":
|
|
1585
|
+
case "--version":
|
|
1586
|
+
printVersion();
|
|
1587
|
+
return 0;
|
|
1105
1588
|
default:
|
|
1106
1589
|
printErr(`unknown subcommand: ${sub}`);
|
|
1107
1590
|
printUsage();
|
|
@@ -1109,4 +1592,4 @@ async function runCli(argv) {
|
|
|
1109
1592
|
}
|
|
1110
1593
|
}
|
|
1111
1594
|
|
|
1112
|
-
export { runCli, runDoctor, runLogin, runLogout };
|
|
1595
|
+
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup2 as runSetup };
|