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