@muhaven/mcp 0.1.5 → 0.1.7

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,161 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] — 2026-05-18
11
+
12
+ `position.*` tools can now drive real on-chain action via @muhaven/mcp
13
+ — Path C of MCP Option A (dashboard URL elicitation → existing passkey
14
+ ceremony). Pre-0.1.7, position tools returned a placeholder UserOp
15
+ envelope + broker signature that no host could submit; the path was
16
+ attestation-only despite implying buy/sell/claim. 0.1.7 swaps the
17
+ envelope for a pre-filled dashboard deep-link URL the user opens to
18
+ review + tap their passkey through the existing dashboard flow.
19
+
20
+ ### Added
21
+
22
+ - **`muhaven.cash.wrap`** — new tool. Returns a `/cash?amount=` deep-
23
+ link for USDC → mhUSDC conversion. Common LLM chain: `read.portfolio`
24
+ → notice 0 mhUSDC → `cash.wrap` → then `position.buy` (each is its
25
+ own user-confirmed deep-link). Input is human-readable USDC ("100" =
26
+ $100). 23 tools total now (was 22).
27
+
28
+ - **Token identifier accepts symbols OR addresses.** Every `position.*`
29
+ tool's `token` field used to require a 0x-address. Now accepts either
30
+ a symbol ("TBILL1") or a 0x-address. The dashboard pages resolve the
31
+ symbol via the marketplace store; unknown identifiers leave the form
32
+ blank for the user to fill in. Saves the LLM a round-trip through
33
+ `read.tokens` for the common "buy 5 of TBILL1" flow.
34
+
35
+ - **Exported pure helpers** (`buildPositionDeeplink`,
36
+ `formatUsdc6ToDecimal`) so third-party MCP servers + tests can reason
37
+ about the URL shape without spawning anything.
38
+
39
+ ### Changed
40
+
41
+ - **`position.buy/sell/claim` return shape** is now `{ dashboardUrl,
42
+ action, instructions, echo }` instead of `{ intentHash,
43
+ unsignedUserOp, brokerSignature, signerAddress }`. The `instructions`
44
+ field is a pre-formatted two-line string the LLM can show the user
45
+ verbatim ("Open this link to review and authorize..."). The `echo`
46
+ field mirrors input for LLM self-verification. **Breaking** for any
47
+ consumer that pinned the 0.1.6 response shape — the prior shape was
48
+ itself never end-to-end usable (placeholder envelope), so the
49
+ practical impact is "MCP buy now actually works" rather than
50
+ regression.
51
+
52
+ - **`position.rebalance`** returns `not_implemented` with a clear
53
+ next-step hint pointing at single-leg `position.buy` / `position.sell`
54
+ or the dashboard. Multi-leg `execute_plan` (one URL, one passkey,
55
+ one batched UserOp) lands in Wave 5 with composite preview UI.
56
+
57
+ - **Broker dep no longer required for position tools.** Previously, a
58
+ `position.buy` call without a running `muhaven-broker` daemon
59
+ returned `broker.unavailable`. Now position tools talk only to the
60
+ dashboard URL — the broker is still needed for `read.*` / governance
61
+ / issuer / policy tools (those use the JWT-authed path).
62
+
63
+ ### Removed
64
+
65
+ - `signEnvelope` + `PositionEnvelopeData` + the per-process
66
+ `hasSessionKey` probe cache + `__resetSessionKeyProbeCacheForTests`'s
67
+ cache (the function is retained as a no-op for back-compat with any
68
+ test harness importing it). The whole broker-attestation path for
69
+ position tools is gone — they don't need a signing key at all.
70
+
71
+ ### Internal
72
+
73
+ - 21 new vitest cases in `__tests__/position-deeplink.test.ts`,
74
+ replacing `session-key-required.test.ts` (deleted — covered a
75
+ removed code path). Total `@muhaven/mcp` suite: **262/262 passing**
76
+ (was 241). Tool-hash count: **23** (was 22).
77
+
78
+ ### Operator notes
79
+
80
+ - Once installed, the fresh-install ritual is unchanged: `muhaven-broker
81
+ setup --register claude-code` still wires the MCP server into Claude
82
+ Code via `claude mcp add-json`.
83
+ - Existing 0.1.6 installs: `npm install -g @muhaven/mcp@latest` picks up
84
+ the new bin; no setup re-run needed.
85
+ - `MUHAVEN_DASHBOARD_URL` env var (defaults to `https://muhaven.app`)
86
+ now drives the deep-link URL prefix; staging operators set it to
87
+ `https://muhaven-staging.example` and the URLs flow through.
88
+
89
+ ## [0.1.6] — 2026-05-17
90
+
91
+ Adds `muhaven-broker setup --register HOST` so a fresh install no longer
92
+ requires hand-writing a `.mcp.json` (or equivalent host-config file).
93
+ This closes the last manual step of the install ritual — operators run
94
+ one command end-to-end from `npm install -g @muhaven/mcp` to a working
95
+ MCP server registered with their host.
96
+
97
+ Initial host coverage: **Claude Code** (via `claude mcp add-json`).
98
+ `claude-desktop` and `cursor` are reserved as known host names — they
99
+ parse cleanly today but the registrar declines to act and points the
100
+ operator at the per-host JSON snippet in `docs.muhaven.app/mcp/install`.
101
+ Both ship in a Wave 5 follow-up (file-edit registrars need merge-then-
102
+ write semantics + dedicated tests).
103
+
104
+ ### Added
105
+
106
+ - **`muhaven-broker setup --register HOST[,HOST...]` flag** — auto-wire
107
+ the MCP server into one or more host configs after the login step:
108
+ - **claude-code** (live): probes `claude --version`, removes any
109
+ existing `muhaven` entry (idempotent), then runs
110
+ `claude mcp add-json muhaven '{"type":"stdio","command":"muhaven-mcp","env":{...}}' --scope <scope>`.
111
+ `env` carries `MUHAVEN_BACKEND_URL`, `MUHAVEN_DASHBOARD_URL`, and
112
+ `MUHAVEN_KEYRING` when set (the broker session key + endpoint stay
113
+ daemon-only — never baked into the host config).
114
+ - **claude-desktop / cursor**: reserved names. Parse cleanly; registrar
115
+ short-circuits with a "not implemented yet" hint pointing at the
116
+ docs snippet. Adding a host is a focused diff: implement the
117
+ registrar + extend `KNOWN_REGISTER_HOSTS`.
118
+ - Accepts comma-separated values (`--register claude-code,cursor`) and
119
+ repeated flags (`--register claude-code --register cursor`). Dedupes
120
+ across both forms.
121
+ - Unknown host names fail fast with exit code 2 + the allowlist in the
122
+ error message.
123
+
124
+ - **`--register-scope user|project|local` flag** — scope for the
125
+ `claude mcp add-json` call. Default `user` (every project on this
126
+ machine sees the server — matches the per-user broker model);
127
+ `project` writes `.mcp.json` at CWD (git-shared if you commit it);
128
+ `local` writes `~/.claude.json` as a per-project user-only entry
129
+ (Claude Code's `claude mcp add` default).
130
+
131
+ - **Pure helpers exported from `src/broker/setup.ts`** for testing +
132
+ third-party reuse: `buildRegisterEnv`, `buildClaudeMcpRegisterJson`,
133
+ `buildClaudeMcpAddJsonArgv`, `buildClaudeMcpRemoveArgv`,
134
+ `registerWithHost`. Plus type exports for `RegisterHost`,
135
+ `RegisterScope`, `RegisterHostOutcome`, `ShellResult`, and the
136
+ `KNOWN_REGISTER_HOSTS` + `KNOWN_REGISTER_SCOPES` constants.
137
+
138
+ - **`SetupDeps.shellOut`** seam — abstracts child-process execution so
139
+ tests can script the host-CLI responses without spawning real
140
+ binaries. Default implementation in `cli.ts` uses `node:child_process`
141
+ `spawn` (argv-safe — no shell interpolation of the JSON payload).
142
+
143
+ ### Operator UX
144
+
145
+ - Setup's exit code is **0 on register failure**. The broker daemon and
146
+ JWT (the load-bearing artifacts) are already in place; an opt-in
147
+ registration failure surfaces as a warning on stderr with the exact
148
+ re-run hint and a fallback link to the per-host JSON snippet. This
149
+ matches the existing pattern for `--skip-login` (operator can complete
150
+ the missing step in isolation later).
151
+
152
+ - A `cli_missing` outcome (claude binary not on PATH) is distinct from
153
+ a `failed` outcome (claude ran but errored). The error copy reflects
154
+ which: operators on a machine without Claude Code get an "install
155
+ Claude Code" prompt; operators with Claude Code installed get the
156
+ CLI's actual error message.
157
+
158
+ ### Tests
159
+
160
+ - **35 new vitest cases** covering `parseSetupFlags --register /
161
+ --register-scope` (11), pure helpers (12), and the `registerWithHost`
162
+ + `runSetup` integration (12). Total `__tests__/setup.test.ts` now
163
+ 93 cases. Full `@muhaven/mcp` suite: 241 cases passing.
164
+
10
165
  ## [0.1.5] — 2026-05-17
11
166
 
12
167
  Adds the `muhaven-broker stop` subcommand so operators can cleanly tear
package/dist/broker.cjs CHANGED
@@ -1047,6 +1047,12 @@ async function waitForBroker(options) {
1047
1047
  `muhaven-broker daemon did not become reachable within ${timeoutMs}ms: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
1048
1048
  );
1049
1049
  }
1050
+ var KNOWN_REGISTER_HOSTS = [
1051
+ "claude-code",
1052
+ "claude-desktop",
1053
+ "cursor"
1054
+ ];
1055
+ var KNOWN_REGISTER_SCOPES = ["user", "project", "local"];
1050
1056
  function parseSetupFlags(argv) {
1051
1057
  let foreground = false;
1052
1058
  let noLaunchBrowser = false;
@@ -1054,6 +1060,8 @@ function parseSetupFlags(argv) {
1054
1060
  let backendBaseUrl;
1055
1061
  let dashboardBaseUrl;
1056
1062
  let skipLogin = false;
1063
+ const register = [];
1064
+ let registerScope = "user";
1057
1065
  for (let i = 0; i < argv.length; i++) {
1058
1066
  const a = argv[i];
1059
1067
  if (a === "--foreground" || a === "-f") foreground = true;
@@ -1062,7 +1070,29 @@ function parseSetupFlags(argv) {
1062
1070
  else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
1063
1071
  else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
1064
1072
  else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
1065
- else throw new Error(`unknown flag: ${a}`);
1073
+ else if (a === "--register" && i + 1 < argv.length) {
1074
+ const value = argv[++i];
1075
+ for (const raw of value.split(",")) {
1076
+ const host = raw.trim().toLowerCase();
1077
+ if (host === "") continue;
1078
+ if (!KNOWN_REGISTER_HOSTS.includes(host)) {
1079
+ throw new Error(
1080
+ `unknown --register host: ${JSON.stringify(host)} (expected one of ${KNOWN_REGISTER_HOSTS.join(", ")})`
1081
+ );
1082
+ }
1083
+ if (!register.includes(host)) {
1084
+ register.push(host);
1085
+ }
1086
+ }
1087
+ } else if (a === "--register-scope" && i + 1 < argv.length) {
1088
+ const value = argv[++i];
1089
+ if (!KNOWN_REGISTER_SCOPES.includes(value)) {
1090
+ throw new Error(
1091
+ `unknown --register-scope: ${JSON.stringify(value)} (expected one of ${KNOWN_REGISTER_SCOPES.join(", ")})`
1092
+ );
1093
+ }
1094
+ registerScope = value;
1095
+ } else throw new Error(`unknown flag: ${a}`);
1066
1096
  }
1067
1097
  return {
1068
1098
  foreground,
@@ -1070,9 +1100,77 @@ function parseSetupFlags(argv) {
1070
1100
  brokerEndpoint,
1071
1101
  backendBaseUrl,
1072
1102
  dashboardBaseUrl,
1073
- skipLogin
1103
+ skipLogin,
1104
+ register,
1105
+ registerScope
1074
1106
  };
1075
1107
  }
1108
+ function buildRegisterEnv(effectiveEnv) {
1109
+ const env = {};
1110
+ if (effectiveEnv.MUHAVEN_BACKEND_URL) env.MUHAVEN_BACKEND_URL = effectiveEnv.MUHAVEN_BACKEND_URL;
1111
+ if (effectiveEnv.MUHAVEN_DASHBOARD_URL) env.MUHAVEN_DASHBOARD_URL = effectiveEnv.MUHAVEN_DASHBOARD_URL;
1112
+ if (effectiveEnv.MUHAVEN_KEYRING) env.MUHAVEN_KEYRING = effectiveEnv.MUHAVEN_KEYRING;
1113
+ return env;
1114
+ }
1115
+ function buildClaudeMcpRegisterJson(registerEnv) {
1116
+ const payload = {
1117
+ type: "stdio",
1118
+ command: "muhaven-mcp"
1119
+ };
1120
+ if (Object.keys(registerEnv).length > 0) {
1121
+ payload.env = registerEnv;
1122
+ }
1123
+ return JSON.stringify(payload);
1124
+ }
1125
+ function buildClaudeMcpAddJsonArgv(serverName, json, scope) {
1126
+ return ["mcp", "add-json", serverName, json, "--scope", scope];
1127
+ }
1128
+ function buildClaudeMcpRemoveArgv(serverName, scope) {
1129
+ return ["mcp", "remove", serverName, "--scope", scope];
1130
+ }
1131
+ async function registerWithHost(deps, options) {
1132
+ if (options.host === "claude-code") {
1133
+ return registerWithClaudeCode(deps, options);
1134
+ }
1135
+ return { status: "not_implemented", host: options.host };
1136
+ }
1137
+ async function registerWithClaudeCode(deps, options) {
1138
+ let probe;
1139
+ try {
1140
+ probe = await deps.shellOut("claude", ["--version"]);
1141
+ } catch (err) {
1142
+ return {
1143
+ status: "cli_missing",
1144
+ host: options.host,
1145
+ cmd: `claude --version (${err.message})`
1146
+ };
1147
+ }
1148
+ if (probe.exitCode !== 0) {
1149
+ return {
1150
+ status: "cli_missing",
1151
+ host: options.host,
1152
+ cmd: "claude --version"
1153
+ };
1154
+ }
1155
+ await deps.shellOut("claude", buildClaudeMcpRemoveArgv(options.serverName, options.scope));
1156
+ const json = buildClaudeMcpRegisterJson(options.registerEnv);
1157
+ const addArgv = buildClaudeMcpAddJsonArgv(options.serverName, json, options.scope);
1158
+ let add;
1159
+ try {
1160
+ add = await deps.shellOut("claude", addArgv);
1161
+ } catch (err) {
1162
+ return {
1163
+ status: "failed",
1164
+ host: options.host,
1165
+ reason: `spawn claude failed: ${err.message}`
1166
+ };
1167
+ }
1168
+ if (add.exitCode !== 0) {
1169
+ const reason = [add.stderr, add.stdout].map((s) => s.trim()).filter((s) => s.length > 0).join(" | ") || `exit ${add.exitCode}`;
1170
+ return { status: "failed", host: options.host, reason };
1171
+ }
1172
+ return { status: "registered", host: options.host, scope: options.scope };
1173
+ }
1076
1174
  async function runSetup(argv, deps) {
1077
1175
  let flags;
1078
1176
  try {
@@ -1080,7 +1178,7 @@ async function runSetup(argv, deps) {
1080
1178
  } catch (err) {
1081
1179
  deps.printErr(`error: ${err.message}`);
1082
1180
  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]"
1181
+ "usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]\n [--register HOST[,HOST...]] [--register-scope user|project|local]"
1084
1182
  );
1085
1183
  return 2;
1086
1184
  }
@@ -1243,6 +1341,39 @@ async function runSetup(argv, deps) {
1243
1341
  return code;
1244
1342
  }
1245
1343
  }
1344
+ if (flags.register.length > 0) {
1345
+ const registerEnv = buildRegisterEnv(effectiveEnv);
1346
+ for (const host of flags.register) {
1347
+ const outcome = await registerWithHost(deps, {
1348
+ host,
1349
+ scope: flags.registerScope,
1350
+ serverName: "muhaven",
1351
+ registerEnv
1352
+ });
1353
+ switch (outcome.status) {
1354
+ case "registered":
1355
+ deps.print(
1356
+ `Host register: ${outcome.host} wired (scope: ${outcome.scope}). Restart the host to pick up the new MCP server.`
1357
+ );
1358
+ break;
1359
+ case "cli_missing":
1360
+ deps.printErr(
1361
+ `Host register: ${outcome.host} CLI not found on PATH (${outcome.cmd}). Install Claude Code and re-run \`muhaven-broker setup --register ${outcome.host}\`, or copy the JSON snippet from https://docs.muhaven.app/mcp/install#step-3-wire-your-host`
1362
+ );
1363
+ break;
1364
+ case "not_implemented":
1365
+ deps.printErr(
1366
+ `Host register: ${outcome.host} registrar not implemented yet (Wave 5). Use the JSON snippet from https://docs.muhaven.app/mcp/install#step-3-wire-your-host for now.`
1367
+ );
1368
+ break;
1369
+ case "failed":
1370
+ deps.printErr(
1371
+ `Host register: ${outcome.host} failed \u2014 ${outcome.reason}. Setup continues; re-run \`muhaven-broker setup --register ${outcome.host}\` after fixing.`
1372
+ );
1373
+ break;
1374
+ }
1375
+ }
1376
+ }
1246
1377
  deps.print("");
1247
1378
  deps.print("================================");
1248
1379
  deps.print("Setup complete.");
@@ -1604,6 +1735,10 @@ function printUsage() {
1604
1735
  print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
1605
1736
  print(" [--skip-login] starts the daemon but lets you run login later");
1606
1737
  print(" [--no-launch-browser] pass-through to login");
1738
+ print(" [--register HOST[,HOST...]] auto-wire the MCP server into the named host");
1739
+ print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
1740
+ print(" [--register-scope user|project|local] scope for the host-config write");
1741
+ print(" (default: user \u2014 every project sees the server)");
1607
1742
  print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
1608
1743
  print(" after 5s). Also clears the keystore JWT as a best effort.");
1609
1744
  print(" login Acquire a JWT via the device-code flow + store in keystore");
@@ -1615,7 +1750,7 @@ function printUsage() {
1615
1750
  }
1616
1751
  function getBrokerPackageVersion() {
1617
1752
  {
1618
- return "0.1.5";
1753
+ return "0.1.7";
1619
1754
  }
1620
1755
  }
1621
1756
  function printVersion() {
@@ -1624,6 +1759,33 @@ function printVersion() {
1624
1759
  function resolveBrokerBinPath() {
1625
1760
  return path.resolve(__dirname, "..", "bin", "muhaven-broker.cjs");
1626
1761
  }
1762
+ function defaultShellOut(cmd, argv) {
1763
+ return new Promise((resolve, reject) => {
1764
+ const child = child_process.spawn(cmd, argv, {
1765
+ // Inherit env so PATH + npm-shim resolution work; explicitly NOT
1766
+ // forwarding stdio so the parent's transcript stays clean.
1767
+ stdio: ["ignore", "pipe", "pipe"],
1768
+ // Windows: .cmd / .ps1 shims under %APPDATA%\npm need cmd.exe
1769
+ // to interpret them. Node 18+ auto-routes through cmd.exe when
1770
+ // it sees a non-.exe extension, but explicitly setting
1771
+ // `shell: true` on Windows is safer for npm-global PATH entries.
1772
+ // On POSIX, `shell: false` (the default) is correct + safer.
1773
+ shell: process.platform === "win32"
1774
+ });
1775
+ let stdout = "";
1776
+ let stderr = "";
1777
+ child.stdout?.on("data", (chunk) => {
1778
+ stdout += chunk.toString("utf-8");
1779
+ });
1780
+ child.stderr?.on("data", (chunk) => {
1781
+ stderr += chunk.toString("utf-8");
1782
+ });
1783
+ child.on("error", (err) => reject(err));
1784
+ child.on("close", (code) => {
1785
+ resolve({ exitCode: code ?? 0, stdout, stderr });
1786
+ });
1787
+ });
1788
+ }
1627
1789
  async function runSetup2(argv) {
1628
1790
  const deps = {
1629
1791
  print,
@@ -1637,7 +1799,8 @@ async function runSetup2(argv) {
1637
1799
  resolveBinPath: resolveBrokerBinPath,
1638
1800
  env: process.env,
1639
1801
  platformId: process.platform,
1640
- osRelease: os.release()
1802
+ osRelease: os.release(),
1803
+ shellOut: defaultShellOut
1641
1804
  };
1642
1805
  return runSetup(argv, deps);
1643
1806
  }
package/dist/broker.js CHANGED
@@ -1049,6 +1049,12 @@ async function waitForBroker(options) {
1049
1049
  `muhaven-broker daemon did not become reachable within ${timeoutMs}ms: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
1050
1050
  );
1051
1051
  }
1052
+ var KNOWN_REGISTER_HOSTS = [
1053
+ "claude-code",
1054
+ "claude-desktop",
1055
+ "cursor"
1056
+ ];
1057
+ var KNOWN_REGISTER_SCOPES = ["user", "project", "local"];
1052
1058
  function parseSetupFlags(argv) {
1053
1059
  let foreground = false;
1054
1060
  let noLaunchBrowser = false;
@@ -1056,6 +1062,8 @@ function parseSetupFlags(argv) {
1056
1062
  let backendBaseUrl;
1057
1063
  let dashboardBaseUrl;
1058
1064
  let skipLogin = false;
1065
+ const register = [];
1066
+ let registerScope = "user";
1059
1067
  for (let i = 0; i < argv.length; i++) {
1060
1068
  const a = argv[i];
1061
1069
  if (a === "--foreground" || a === "-f") foreground = true;
@@ -1064,7 +1072,29 @@ function parseSetupFlags(argv) {
1064
1072
  else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
1065
1073
  else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
1066
1074
  else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
1067
- else throw new Error(`unknown flag: ${a}`);
1075
+ else if (a === "--register" && i + 1 < argv.length) {
1076
+ const value = argv[++i];
1077
+ for (const raw of value.split(",")) {
1078
+ const host = raw.trim().toLowerCase();
1079
+ if (host === "") continue;
1080
+ if (!KNOWN_REGISTER_HOSTS.includes(host)) {
1081
+ throw new Error(
1082
+ `unknown --register host: ${JSON.stringify(host)} (expected one of ${KNOWN_REGISTER_HOSTS.join(", ")})`
1083
+ );
1084
+ }
1085
+ if (!register.includes(host)) {
1086
+ register.push(host);
1087
+ }
1088
+ }
1089
+ } else if (a === "--register-scope" && i + 1 < argv.length) {
1090
+ const value = argv[++i];
1091
+ if (!KNOWN_REGISTER_SCOPES.includes(value)) {
1092
+ throw new Error(
1093
+ `unknown --register-scope: ${JSON.stringify(value)} (expected one of ${KNOWN_REGISTER_SCOPES.join(", ")})`
1094
+ );
1095
+ }
1096
+ registerScope = value;
1097
+ } else throw new Error(`unknown flag: ${a}`);
1068
1098
  }
1069
1099
  return {
1070
1100
  foreground,
@@ -1072,9 +1102,77 @@ function parseSetupFlags(argv) {
1072
1102
  brokerEndpoint,
1073
1103
  backendBaseUrl,
1074
1104
  dashboardBaseUrl,
1075
- skipLogin
1105
+ skipLogin,
1106
+ register,
1107
+ registerScope
1076
1108
  };
1077
1109
  }
1110
+ function buildRegisterEnv(effectiveEnv) {
1111
+ const env = {};
1112
+ if (effectiveEnv.MUHAVEN_BACKEND_URL) env.MUHAVEN_BACKEND_URL = effectiveEnv.MUHAVEN_BACKEND_URL;
1113
+ if (effectiveEnv.MUHAVEN_DASHBOARD_URL) env.MUHAVEN_DASHBOARD_URL = effectiveEnv.MUHAVEN_DASHBOARD_URL;
1114
+ if (effectiveEnv.MUHAVEN_KEYRING) env.MUHAVEN_KEYRING = effectiveEnv.MUHAVEN_KEYRING;
1115
+ return env;
1116
+ }
1117
+ function buildClaudeMcpRegisterJson(registerEnv) {
1118
+ const payload = {
1119
+ type: "stdio",
1120
+ command: "muhaven-mcp"
1121
+ };
1122
+ if (Object.keys(registerEnv).length > 0) {
1123
+ payload.env = registerEnv;
1124
+ }
1125
+ return JSON.stringify(payload);
1126
+ }
1127
+ function buildClaudeMcpAddJsonArgv(serverName, json, scope) {
1128
+ return ["mcp", "add-json", serverName, json, "--scope", scope];
1129
+ }
1130
+ function buildClaudeMcpRemoveArgv(serverName, scope) {
1131
+ return ["mcp", "remove", serverName, "--scope", scope];
1132
+ }
1133
+ async function registerWithHost(deps, options) {
1134
+ if (options.host === "claude-code") {
1135
+ return registerWithClaudeCode(deps, options);
1136
+ }
1137
+ return { status: "not_implemented", host: options.host };
1138
+ }
1139
+ async function registerWithClaudeCode(deps, options) {
1140
+ let probe;
1141
+ try {
1142
+ probe = await deps.shellOut("claude", ["--version"]);
1143
+ } catch (err) {
1144
+ return {
1145
+ status: "cli_missing",
1146
+ host: options.host,
1147
+ cmd: `claude --version (${err.message})`
1148
+ };
1149
+ }
1150
+ if (probe.exitCode !== 0) {
1151
+ return {
1152
+ status: "cli_missing",
1153
+ host: options.host,
1154
+ cmd: "claude --version"
1155
+ };
1156
+ }
1157
+ await deps.shellOut("claude", buildClaudeMcpRemoveArgv(options.serverName, options.scope));
1158
+ const json = buildClaudeMcpRegisterJson(options.registerEnv);
1159
+ const addArgv = buildClaudeMcpAddJsonArgv(options.serverName, json, options.scope);
1160
+ let add;
1161
+ try {
1162
+ add = await deps.shellOut("claude", addArgv);
1163
+ } catch (err) {
1164
+ return {
1165
+ status: "failed",
1166
+ host: options.host,
1167
+ reason: `spawn claude failed: ${err.message}`
1168
+ };
1169
+ }
1170
+ if (add.exitCode !== 0) {
1171
+ const reason = [add.stderr, add.stdout].map((s) => s.trim()).filter((s) => s.length > 0).join(" | ") || `exit ${add.exitCode}`;
1172
+ return { status: "failed", host: options.host, reason };
1173
+ }
1174
+ return { status: "registered", host: options.host, scope: options.scope };
1175
+ }
1078
1176
  async function runSetup(argv, deps) {
1079
1177
  let flags;
1080
1178
  try {
@@ -1082,7 +1180,7 @@ async function runSetup(argv, deps) {
1082
1180
  } catch (err) {
1083
1181
  deps.printErr(`error: ${err.message}`);
1084
1182
  deps.printErr(
1085
- "usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]"
1183
+ "usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]\n [--register HOST[,HOST...]] [--register-scope user|project|local]"
1086
1184
  );
1087
1185
  return 2;
1088
1186
  }
@@ -1245,6 +1343,39 @@ async function runSetup(argv, deps) {
1245
1343
  return code;
1246
1344
  }
1247
1345
  }
1346
+ if (flags.register.length > 0) {
1347
+ const registerEnv = buildRegisterEnv(effectiveEnv);
1348
+ for (const host of flags.register) {
1349
+ const outcome = await registerWithHost(deps, {
1350
+ host,
1351
+ scope: flags.registerScope,
1352
+ serverName: "muhaven",
1353
+ registerEnv
1354
+ });
1355
+ switch (outcome.status) {
1356
+ case "registered":
1357
+ deps.print(
1358
+ `Host register: ${outcome.host} wired (scope: ${outcome.scope}). Restart the host to pick up the new MCP server.`
1359
+ );
1360
+ break;
1361
+ case "cli_missing":
1362
+ deps.printErr(
1363
+ `Host register: ${outcome.host} CLI not found on PATH (${outcome.cmd}). Install Claude Code and re-run \`muhaven-broker setup --register ${outcome.host}\`, or copy the JSON snippet from https://docs.muhaven.app/mcp/install#step-3-wire-your-host`
1364
+ );
1365
+ break;
1366
+ case "not_implemented":
1367
+ deps.printErr(
1368
+ `Host register: ${outcome.host} registrar not implemented yet (Wave 5). Use the JSON snippet from https://docs.muhaven.app/mcp/install#step-3-wire-your-host for now.`
1369
+ );
1370
+ break;
1371
+ case "failed":
1372
+ deps.printErr(
1373
+ `Host register: ${outcome.host} failed \u2014 ${outcome.reason}. Setup continues; re-run \`muhaven-broker setup --register ${outcome.host}\` after fixing.`
1374
+ );
1375
+ break;
1376
+ }
1377
+ }
1378
+ }
1248
1379
  deps.print("");
1249
1380
  deps.print("================================");
1250
1381
  deps.print("Setup complete.");
@@ -1606,6 +1737,10 @@ function printUsage() {
1606
1737
  print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
1607
1738
  print(" [--skip-login] starts the daemon but lets you run login later");
1608
1739
  print(" [--no-launch-browser] pass-through to login");
1740
+ print(" [--register HOST[,HOST...]] auto-wire the MCP server into the named host");
1741
+ print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
1742
+ print(" [--register-scope user|project|local] scope for the host-config write");
1743
+ print(" (default: user \u2014 every project sees the server)");
1609
1744
  print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
1610
1745
  print(" after 5s). Also clears the keystore JWT as a best effort.");
1611
1746
  print(" login Acquire a JWT via the device-code flow + store in keystore");
@@ -1617,7 +1752,7 @@ function printUsage() {
1617
1752
  }
1618
1753
  function getBrokerPackageVersion() {
1619
1754
  {
1620
- return "0.1.5";
1755
+ return "0.1.7";
1621
1756
  }
1622
1757
  }
1623
1758
  function printVersion() {
@@ -1626,6 +1761,33 @@ function printVersion() {
1626
1761
  function resolveBrokerBinPath() {
1627
1762
  return resolve(__dirname$1, "..", "bin", "muhaven-broker.cjs");
1628
1763
  }
1764
+ function defaultShellOut(cmd, argv) {
1765
+ return new Promise((resolve, reject) => {
1766
+ const child = spawn(cmd, argv, {
1767
+ // Inherit env so PATH + npm-shim resolution work; explicitly NOT
1768
+ // forwarding stdio so the parent's transcript stays clean.
1769
+ stdio: ["ignore", "pipe", "pipe"],
1770
+ // Windows: .cmd / .ps1 shims under %APPDATA%\npm need cmd.exe
1771
+ // to interpret them. Node 18+ auto-routes through cmd.exe when
1772
+ // it sees a non-.exe extension, but explicitly setting
1773
+ // `shell: true` on Windows is safer for npm-global PATH entries.
1774
+ // On POSIX, `shell: false` (the default) is correct + safer.
1775
+ shell: process.platform === "win32"
1776
+ });
1777
+ let stdout = "";
1778
+ let stderr = "";
1779
+ child.stdout?.on("data", (chunk) => {
1780
+ stdout += chunk.toString("utf-8");
1781
+ });
1782
+ child.stderr?.on("data", (chunk) => {
1783
+ stderr += chunk.toString("utf-8");
1784
+ });
1785
+ child.on("error", (err) => reject(err));
1786
+ child.on("close", (code) => {
1787
+ resolve({ exitCode: code ?? 0, stdout, stderr });
1788
+ });
1789
+ });
1790
+ }
1629
1791
  async function runSetup2(argv) {
1630
1792
  const deps = {
1631
1793
  print,
@@ -1639,7 +1801,8 @@ async function runSetup2(argv) {
1639
1801
  resolveBinPath: resolveBrokerBinPath,
1640
1802
  env: process.env,
1641
1803
  platformId: process.platform,
1642
- osRelease: release()
1804
+ osRelease: release(),
1805
+ shellOut: defaultShellOut
1643
1806
  };
1644
1807
  return runSetup(argv, deps);
1645
1808
  }