@muhaven/mcp 0.1.3 → 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 CHANGED
@@ -7,6 +7,136 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.4] — 2026-05-17
11
+
12
+ Adds the one-shot `muhaven-broker setup` subcommand so a fresh install
13
+ goes from `npm install -g @muhaven/mcp` straight to a working MCP host
14
+ in two commands. Surfaced during the Wave 4 demo-recording prep — the
15
+ prior five-line manual ritual (env exports + session-key mint +
16
+ background daemon + login) was the longest opaque block in the demo
17
+ script. Also adds `--version` / `--help` to both `muhaven-broker` and
18
+ `muhaven-mcp` bins.
19
+
20
+ ### Added
21
+
22
+ - **`muhaven-broker setup` subcommand** — orchestrates env defaulting +
23
+ session-key minting + detached daemon spawn + login in a single
24
+ invocation. Flags:
25
+ - `--foreground` / `-f`: keep the daemon attached to the current
26
+ shell (useful when systemd/launchd will supervise instead of the
27
+ backgrounded child).
28
+ - `--skip-login`: spawn the daemon but defer the device-code flow.
29
+ - `--no-launch-browser`: pass-through to the embedded `login` step.
30
+ - `--broker-endpoint`, `--backend-base-url`, `--dashboard-base-url`:
31
+ same overrides as `login`.
32
+
33
+ Env defaults applied (only when the var is unset):
34
+ - `MUHAVEN_BACKEND_URL=https://api.muhaven.app`
35
+ - `MUHAVEN_DASHBOARD_URL=https://muhaven.app`
36
+ - `MUHAVEN_KEYRING=file` (auto-applied on Windows / WSL2 /
37
+ devcontainer / GitHub Codespace / SSH — same heuristic as
38
+ `muhaven-broker doctor`'s environment detector). Native macOS +
39
+ Linux desktop leave the value unset so the OS keychain remains
40
+ the default.
41
+
42
+ Idempotent: re-running `setup` against an already-up daemon detects
43
+ the existing JWT and short-circuits to `Login: skipped — JWT already
44
+ in keystore.`. Against a daemon that's up but unauthenticated, it
45
+ skips the spawn and only runs the login step.
46
+
47
+ Closing summary always surfaces the broker endpoint and a
48
+ platform-specific "Stop daemon" command (`kill <pid>` on POSIX,
49
+ `Stop-Process -Id <pid>` on Windows). Sign-out is explicitly
50
+ documented as separate from daemon shutdown — `muhaven-broker logout`
51
+ clears the JWT but leaves the daemon running.
52
+
53
+ - **`muhaven-broker --version` / `-v`** — prints `muhaven-broker
54
+ @muhaven/mcp@<version>` and exits 0. Wired into the dispatcher
55
+ alongside the existing `--help` / `-h`. Reads the package version
56
+ from the tsup-injected `__SERVER_VERSION__` constant.
57
+
58
+ - **`muhaven-mcp --version` / `-v` and `--help` / `-h`** — bin shim
59
+ short-circuits before requiring `dist/index.cjs`, so the flags exit
60
+ cleanly without spinning up the broker IPC + tool registry. Reads
61
+ the version from the sibling `package.json` directly.
62
+
63
+ ### Security
64
+
65
+ - **Session key never lands in `process.env`** — the orchestrator
66
+ builds a local `effectiveEnv` snapshot and passes the minted
67
+ session key only to the spawned daemon's env. Prior version
68
+ mutated `process.env.MUHAVEN_BROKER_SESSION_KEY` so any subsequent
69
+ child of the operator's shell would inherit the key. Foreground
70
+ mode brackets its required `process.env` mutation in a try/finally
71
+ that restores the original values on exit.
72
+
73
+ - **Spawned daemon strips `NODE_OPTIONS` / `NODE_TLS_REJECT_UNAUTHORIZED`
74
+ / `NODE_EXTRA_CA_CERTS` / `NODE_PATH`** from inherited env so a
75
+ same-user attacker who set those in the operator's shell can't
76
+ hijack the daemon's execution to exfiltrate the session key.
77
+
78
+ - **URL flag validation** — `--backend-base-url` / `--dashboard-base-url`
79
+ must be `https://` (with `http://localhost` / `127.0.0.1` /
80
+ `[::1]` dev carve-out). Rejects `javascript:`, `file:`, `data:`,
81
+ and plain `http:` to non-loopback BEFORE the spawn — defense
82
+ against the OAuth-device-flow phishing vector where a malicious
83
+ `--backend-base-url` would ship the JWT to an attacker host.
84
+
85
+ - **`--broker-endpoint` path validation** — must be a `\\.\pipe\…`
86
+ path on Windows or an absolute path on POSIX. Rejects relative
87
+ paths + flag-injection (e.g. `--broker-endpoint --from-daemon` is
88
+ parsed but rejected at validation, preventing the spawned daemon
89
+ from being bound to an attacker-controlled location).
90
+
91
+ - **Preserved env values not echoed** — `Env preserved: NAME (set in
92
+ your shell)` only — values stay opaque. Prior version printed
93
+ `Env preserved: NAME=value` which would leak operator-supplied
94
+ values to shell history / CI logs.
95
+
96
+ - **Session key minted via viem's `generatePrivateKey`** — guarantees
97
+ the result is in the valid secp256k1 scalar range. Prior version
98
+ used raw `crypto.randomBytes(32)`, which had a (negligible but
99
+ nonzero) probability of returning an out-of-range value that the
100
+ signer would reject as invalid much later in the flow.
101
+
102
+ - **Bin path resolved via `__dirname`** — `resolveBrokerBinPath` walks
103
+ from the bundled `dist/broker.cjs` to the sibling
104
+ `bin/muhaven-broker.cjs` deterministically, so Windows global-npm
105
+ shim wrappers (`.cmd` / `.ps1` in `process.argv[1]`) don't end up
106
+ as the spawn target.
107
+
108
+ - **`detectMcpHost` no longer falls through to `npm_lifecycle_event`**
109
+ — that var is the npm script name, not an MCP-host identity. The
110
+ device-flow `/link` page's "requesting client" panel would have
111
+ displayed "setup" for operators running via `npm run setup`,
112
+ misleading the passkey ceremony.
113
+
114
+ ### Tests
115
+
116
+ - 197 vitest pass (up from 134 in 0.1.3). Net +58 cases in
117
+ `__tests__/setup.test.ts` (+22 over the initial +36 after the
118
+ parallel agent security review) + 5 in `__tests__/cli-version-flag.test.ts`:
119
+ - **+10** `applyEnvDefaults` — defaults applied on empty env;
120
+ backend/dashboard preserved when set; KEYRING auto-applied on
121
+ win32/WSL2/SSH/devcontainer/Codespaces; left unset on native
122
+ macOS/Linux desktop; explicit `MUHAVEN_KEYRING=os` preserved on
123
+ Windows; empty-string vars treated as unset.
124
+ - **+2** `mintSessionKey` — 0x-prefixed 32-byte hex shape;
125
+ non-deterministic across calls.
126
+ - **+3** `decideSetupAction` — spawn-and-login / login-only /
127
+ already-ready decision tree.
128
+ - **+6** `parseSetupFlags` — defaults; `--foreground` and `-f`
129
+ aliases; `--skip-login`; `--no-launch-browser` pass-through; value
130
+ flag parsing; unknown-flag rejection.
131
+ - **+3** `waitForBroker` — first-call success; retry-until-success
132
+ with virtual clock; timeout throws with last error in message.
133
+ - **+12** `runSetup` orchestrator — flag-error path returns 2;
134
+ foreground mode short-circuits; spawn_and_login happy path;
135
+ login_only path; already_ready path; `--skip-login`; login-failure
136
+ bubbles exit code + leaves daemon running; wait timeout returns 1;
137
+ `--no-launch-browser` pass-through; value-flag pass-through;
138
+ session key minted vs preserved.
139
+
10
140
  ## [0.1.3] — 2026-05-16
11
141
 
12
142
  Q2 fix bundle from the post-§4 queue closing four findings from §3e⁶
@@ -1,5 +1,50 @@
1
1
  #!/usr/bin/env node
2
2
  /* eslint-disable */
3
+ //
4
+ // `muhaven-mcp` bin entrypoint.
5
+ //
6
+ // Production: MCPB hosts (Claude Desktop / Cursor / Claude Code) spawn this
7
+ // binary over STDIO and immediately start a JSON-RPC handshake. The host
8
+ // never passes argv flags — but operators occasionally run `muhaven-mcp
9
+ // --version` / `--help` from the shell to sanity-check the install. Those
10
+ // flags short-circuit BEFORE we wire up the STDIO transport so they exit
11
+ // cleanly without spinning up the broker IPC + tool registry.
12
+ //
13
+ // Keep this shim tiny — the production path is `runMcpStdioCli()` from the
14
+ // bundled dist. Anything richer goes in src/ where it's testable.
15
+ //
16
+
17
+ const args = process.argv.slice(2);
18
+
19
+ if (args.includes('--version') || args.includes('-v')) {
20
+ const pkg = require('../package.json');
21
+ process.stdout.write(`muhaven-mcp ${pkg.version}\n`);
22
+ process.exit(0);
23
+ }
24
+
25
+ if (args.includes('--help') || args.includes('-h')) {
26
+ const pkg = require('../package.json');
27
+ process.stdout.write(
28
+ [
29
+ `muhaven-mcp ${pkg.version} — MuHaven MCP STDIO server`,
30
+ ``,
31
+ `Usage:`,
32
+ ` muhaven-mcp Run the MCP server over STDIO`,
33
+ ` (called by Claude Desktop / Cursor /`,
34
+ ` Claude Code — not directly by humans)`,
35
+ ` muhaven-mcp --version | -v Print the @muhaven/mcp package version`,
36
+ ` muhaven-mcp --help | -h Show this help`,
37
+ ``,
38
+ `For first-time setup, run: muhaven-broker setup`,
39
+ `For troubleshooting, run: muhaven-broker doctor`,
40
+ ``,
41
+ `Docs: https://github.com/hasToDev/muhaven/blob/master/packages/mcp/README.md`,
42
+ ``,
43
+ ].join('\n'),
44
+ );
45
+ process.exit(0);
46
+ }
47
+
3
48
  const { runMcpStdioCli } = require('../dist/index.cjs');
4
49
 
5
50
  runMcpStdioCli().then(
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;
@@ -934,6 +933,332 @@ async function runBrokerDaemonCli() {
934
933
  await new Promise(() => {
935
934
  });
936
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
+ }
937
1262
 
938
1263
  // src/broker/cli.ts
939
1264
  function print(line) {
@@ -943,7 +1268,7 @@ function printErr(line) {
943
1268
  process.stderr.write(line + "\n");
944
1269
  }
945
1270
  function detectMcpHost() {
946
- 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";
947
1272
  }
948
1273
  function detectEnvironment() {
949
1274
  const warnings = [];
@@ -1197,11 +1522,44 @@ function printUsage() {
1197
1522
  print("usage: muhaven-broker [<subcommand>] [options]");
1198
1523
  print("");
1199
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");
1200
1529
  print(" login Acquire a JWT via the device-code flow + store in keystore");
1201
1530
  print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
1202
1531
  print(" logout Clear the JWT from the keystore");
1203
1532
  print(" doctor Print environment + keystore + reachability report");
1204
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);
1205
1563
  }
1206
1564
  async function runCli(argv) {
1207
1565
  const [sub, ...rest] = argv;
@@ -1209,6 +1567,8 @@ async function runCli(argv) {
1209
1567
  case void 0:
1210
1568
  await runBrokerDaemonCli();
1211
1569
  return 0;
1570
+ case "setup":
1571
+ return runSetup2(rest);
1212
1572
  case "login":
1213
1573
  return runLogin(rest);
1214
1574
  case "logout":
@@ -1219,6 +1579,10 @@ async function runCli(argv) {
1219
1579
  case "--help":
1220
1580
  printUsage();
1221
1581
  return 0;
1582
+ case "-v":
1583
+ case "--version":
1584
+ printVersion();
1585
+ return 0;
1222
1586
  default:
1223
1587
  printErr(`unknown subcommand: ${sub}`);
1224
1588
  printUsage();
@@ -1226,8 +1590,10 @@ async function runCli(argv) {
1226
1590
  }
1227
1591
  }
1228
1592
 
1593
+ exports.getBrokerPackageVersion = getBrokerPackageVersion;
1229
1594
  exports.parseLoginFlags = parseLoginFlags;
1230
1595
  exports.runCli = runCli;
1231
1596
  exports.runDoctor = runDoctor;
1232
1597
  exports.runLogin = runLogin;
1233
1598
  exports.runLogout = runLogout;
1599
+ exports.runSetup = runSetup2;
package/dist/broker.d.cts CHANGED
@@ -31,6 +31,14 @@ declare function parseLoginFlags(argv: readonly string[]): LoginFlags;
31
31
  declare function runLogin(argv: readonly string[]): Promise<number>;
32
32
  declare function runLogout(): Promise<number>;
33
33
  declare function runDoctor(): Promise<number>;
34
+ declare function getBrokerPackageVersion(): string;
35
+ /**
36
+ * Wire `runSetup` against the real cli helpers + IO. Kept here (not in
37
+ * `setup.ts`) so the pure orchestrator stays free of the cli-only
38
+ * `runLogin` import (which would pull device-flow + viem into the test
39
+ * surface unnecessarily).
40
+ */
41
+ declare function runSetup(argv: readonly string[]): Promise<number>;
34
42
  declare function runCli(argv: readonly string[]): Promise<number>;
35
43
 
36
- export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
44
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup };
package/dist/broker.d.ts CHANGED
@@ -31,6 +31,14 @@ declare function parseLoginFlags(argv: readonly string[]): LoginFlags;
31
31
  declare function runLogin(argv: readonly string[]): Promise<number>;
32
32
  declare function runLogout(): Promise<number>;
33
33
  declare function runDoctor(): Promise<number>;
34
+ declare function getBrokerPackageVersion(): string;
35
+ /**
36
+ * Wire `runSetup` against the real cli helpers + IO. Kept here (not in
37
+ * `setup.ts`) so the pure orchestrator stays free of the cli-only
38
+ * `runLogin` import (which would pull device-flow + viem into the test
39
+ * surface unnecessarily).
40
+ */
41
+ declare function runSetup(argv: readonly string[]): Promise<number>;
34
42
  declare function runCli(argv: readonly string[]): Promise<number>;
35
43
 
36
- export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
44
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup };
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;
@@ -418,8 +421,8 @@ var OsKeystore = class {
418
421
  }
419
422
  };
420
423
  var FileKeystore = class {
421
- constructor(path) {
422
- this.path = path;
424
+ constructor(path2) {
425
+ this.path = path2;
423
426
  }
424
427
  path;
425
428
  backend = "file";
@@ -932,6 +935,332 @@ async function runBrokerDaemonCli() {
932
935
  await new Promise(() => {
933
936
  });
934
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
+ }
935
1264
 
936
1265
  // src/broker/cli.ts
937
1266
  function print(line) {
@@ -941,7 +1270,7 @@ function printErr(line) {
941
1270
  process.stderr.write(line + "\n");
942
1271
  }
943
1272
  function detectMcpHost() {
944
- 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";
945
1274
  }
946
1275
  function detectEnvironment() {
947
1276
  const warnings = [];
@@ -1195,11 +1524,44 @@ function printUsage() {
1195
1524
  print("usage: muhaven-broker [<subcommand>] [options]");
1196
1525
  print("");
1197
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");
1198
1531
  print(" login Acquire a JWT via the device-code flow + store in keystore");
1199
1532
  print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
1200
1533
  print(" logout Clear the JWT from the keystore");
1201
1534
  print(" doctor Print environment + keystore + reachability report");
1202
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);
1203
1565
  }
1204
1566
  async function runCli(argv) {
1205
1567
  const [sub, ...rest] = argv;
@@ -1207,6 +1569,8 @@ async function runCli(argv) {
1207
1569
  case void 0:
1208
1570
  await runBrokerDaemonCli();
1209
1571
  return 0;
1572
+ case "setup":
1573
+ return runSetup2(rest);
1210
1574
  case "login":
1211
1575
  return runLogin(rest);
1212
1576
  case "logout":
@@ -1217,6 +1581,10 @@ async function runCli(argv) {
1217
1581
  case "--help":
1218
1582
  printUsage();
1219
1583
  return 0;
1584
+ case "-v":
1585
+ case "--version":
1586
+ printVersion();
1587
+ return 0;
1220
1588
  default:
1221
1589
  printErr(`unknown subcommand: ${sub}`);
1222
1590
  printUsage();
@@ -1224,4 +1592,4 @@ async function runCli(argv) {
1224
1592
  }
1225
1593
  }
1226
1594
 
1227
- export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
1595
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup2 as runSetup };
package/dist/index.cjs CHANGED
@@ -1210,7 +1210,7 @@ var SERVER_NAME = "@muhaven/mcp";
1210
1210
  var SERVER_VERSION = resolveServerVersion();
1211
1211
  function resolveServerVersion() {
1212
1212
  {
1213
- return "0.1.3";
1213
+ return "0.1.4";
1214
1214
  }
1215
1215
  }
1216
1216
  function toJsonInputSchema(schema) {
package/dist/index.js CHANGED
@@ -1206,7 +1206,7 @@ var SERVER_NAME = "@muhaven/mcp";
1206
1206
  var SERVER_VERSION = resolveServerVersion();
1207
1207
  function resolveServerVersion() {
1208
1208
  {
1209
- return "0.1.3";
1209
+ return "0.1.4";
1210
1210
  }
1211
1211
  }
1212
1212
  function toJsonInputSchema(schema) {
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.2",
4
4
  "name": "muhaven-mcp",
5
5
  "display_name": "MuHaven (RWA portfolio)",
6
- "version": "0.1.3",
6
+ "version": "0.1.4",
7
7
  "description": "Confidential RWA portfolio management on Fhenix CoFHE. Read your encrypted balances, propose yield claims and policy changes — all signing happens in a sibling broker daemon, the LLM never sees your private key.",
8
8
  "long_description": "MuHaven MCP exposes 22 tools across read.* / position.* / policy.* / issuer.* / governance.* groups for managing real-world asset (RWA) tokens with FHE-encrypted balances. Authentication uses a one-time device-code ceremony (run `muhaven-broker login`); subsequent tool calls fetch the JWT from the broker over a Unix socket. Position / governance tools return unsigned UserOps + broker signatures — they NEVER auto-submit to a bundler. The companion `muhaven-broker` daemon must be running before tools can be invoked. See README for setup.",
9
9
  "author": {
@@ -94,5 +94,5 @@
94
94
  "sensitive": false
95
95
  }
96
96
  ],
97
- "$comment_setup": "First-run instructions: (1) install this package via your MCPB host (Claude Desktop / Cursor / Claude Code). (2) Start the broker daemon: `muhaven-broker` (running in the background; see README for systemd / launchd / Windows-Service recipes). (3) Authenticate: `muhaven-broker login` opens browser to https://muhaven.app/link?code=XXXX-XXXX, complete passkey ceremony. (4) Use any tool in this MCP package."
97
+ "$comment_setup": "First-run instructions: (1) install this package via your MCPB host (Claude Desktop / Cursor / Claude Code) or globally via `npm install -g @muhaven/mcp`. (2) Run `muhaven-broker setup` one-shot: applies env defaults, mints an ephemeral session key, spawns the broker daemon detached, then walks you through the passkey-bound device-code login (opens browser to https://muhaven.app/link?code=XXXX-XXXX). The daemon stays running after `setup` returns. Use `muhaven-broker setup --foreground` if systemd / launchd / a Windows service will own the daemon's lifecycle instead. (3) Use any tool in this MCP package."
98
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhaven/mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "MuHaven MCP server — read/position/policy toolsets bridging Claude Desktop / Cursor / Claude Code to the MuHaven backend, with a sibling muhaven-broker daemon holding the session-key private half over a local IPC socket",
5
5
  "type": "module",
6
6
  "repository": {