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