@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/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
- // src/broker/cli.ts
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 sessionKeyHex = env.MUHAVEN_BROKER_SESSION_KEY;
75
- if (!sessionKeyHex) {
76
- throw new Error(
77
- "MUHAVEN_BROKER_SESSION_KEY is required (0x-prefixed 32-byte hex). Mint a session key via the dashboard policy-template install flow."
78
- );
79
- }
80
- if (!PRIVKEY_HEX_RE.test(sessionKeyHex)) {
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(path) {
419
- this.path = 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.2.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
- const signature = await signer.signHash(req.hash);
644
- return { type: "sign_hash", signature, signerAddress: signer.address };
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
- this.signer = options.signer ?? new ViemSigner(options.config.sessionKeyHex);
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(parsed, this.signer, this.keystore);
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 ?? process.env.npm_lifecycle_event ?? "muhaven-broker-cli";
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
- return { noLaunchBrowser, brokerEndpoint, backendBaseUrl, dashboardBaseUrl };
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("usage: muhaven-broker login [--no-launch-browser] [--broker-endpoint PATH] [--backend-base-url URL] [--dashboard-base-url URL]");
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: config.backendBaseUrl,
957
- dashboardBaseUrl: config.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
- print(`Broker daemon : reachable (proto v${h.version}, signer ${h.sessionKeyAddress}, hasJwt=${h.hasJwt})`);
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 };