@muhaven/mcp 0.1.4 → 0.1.6

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,129 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.6] — 2026-05-17
11
+
12
+ Adds `muhaven-broker setup --register HOST` so a fresh install no longer
13
+ requires hand-writing a `.mcp.json` (or equivalent host-config file).
14
+ This closes the last manual step of the install ritual — operators run
15
+ one command end-to-end from `npm install -g @muhaven/mcp` to a working
16
+ MCP server registered with their host.
17
+
18
+ Initial host coverage: **Claude Code** (via `claude mcp add-json`).
19
+ `claude-desktop` and `cursor` are reserved as known host names — they
20
+ parse cleanly today but the registrar declines to act and points the
21
+ operator at the per-host JSON snippet in `docs.muhaven.app/mcp/install`.
22
+ Both ship in a Wave 5 follow-up (file-edit registrars need merge-then-
23
+ write semantics + dedicated tests).
24
+
25
+ ### Added
26
+
27
+ - **`muhaven-broker setup --register HOST[,HOST...]` flag** — auto-wire
28
+ the MCP server into one or more host configs after the login step:
29
+ - **claude-code** (live): probes `claude --version`, removes any
30
+ existing `muhaven` entry (idempotent), then runs
31
+ `claude mcp add-json muhaven '{"type":"stdio","command":"muhaven-mcp","env":{...}}' --scope <scope>`.
32
+ `env` carries `MUHAVEN_BACKEND_URL`, `MUHAVEN_DASHBOARD_URL`, and
33
+ `MUHAVEN_KEYRING` when set (the broker session key + endpoint stay
34
+ daemon-only — never baked into the host config).
35
+ - **claude-desktop / cursor**: reserved names. Parse cleanly; registrar
36
+ short-circuits with a "not implemented yet" hint pointing at the
37
+ docs snippet. Adding a host is a focused diff: implement the
38
+ registrar + extend `KNOWN_REGISTER_HOSTS`.
39
+ - Accepts comma-separated values (`--register claude-code,cursor`) and
40
+ repeated flags (`--register claude-code --register cursor`). Dedupes
41
+ across both forms.
42
+ - Unknown host names fail fast with exit code 2 + the allowlist in the
43
+ error message.
44
+
45
+ - **`--register-scope user|project|local` flag** — scope for the
46
+ `claude mcp add-json` call. Default `user` (every project on this
47
+ machine sees the server — matches the per-user broker model);
48
+ `project` writes `.mcp.json` at CWD (git-shared if you commit it);
49
+ `local` writes `~/.claude.json` as a per-project user-only entry
50
+ (Claude Code's `claude mcp add` default).
51
+
52
+ - **Pure helpers exported from `src/broker/setup.ts`** for testing +
53
+ third-party reuse: `buildRegisterEnv`, `buildClaudeMcpRegisterJson`,
54
+ `buildClaudeMcpAddJsonArgv`, `buildClaudeMcpRemoveArgv`,
55
+ `registerWithHost`. Plus type exports for `RegisterHost`,
56
+ `RegisterScope`, `RegisterHostOutcome`, `ShellResult`, and the
57
+ `KNOWN_REGISTER_HOSTS` + `KNOWN_REGISTER_SCOPES` constants.
58
+
59
+ - **`SetupDeps.shellOut`** seam — abstracts child-process execution so
60
+ tests can script the host-CLI responses without spawning real
61
+ binaries. Default implementation in `cli.ts` uses `node:child_process`
62
+ `spawn` (argv-safe — no shell interpolation of the JSON payload).
63
+
64
+ ### Operator UX
65
+
66
+ - Setup's exit code is **0 on register failure**. The broker daemon and
67
+ JWT (the load-bearing artifacts) are already in place; an opt-in
68
+ registration failure surfaces as a warning on stderr with the exact
69
+ re-run hint and a fallback link to the per-host JSON snippet. This
70
+ matches the existing pattern for `--skip-login` (operator can complete
71
+ the missing step in isolation later).
72
+
73
+ - A `cli_missing` outcome (claude binary not on PATH) is distinct from
74
+ a `failed` outcome (claude ran but errored). The error copy reflects
75
+ which: operators on a machine without Claude Code get an "install
76
+ Claude Code" prompt; operators with Claude Code installed get the
77
+ CLI's actual error message.
78
+
79
+ ### Tests
80
+
81
+ - **35 new vitest cases** covering `parseSetupFlags --register /
82
+ --register-scope` (11), pure helpers (12), and the `registerWithHost`
83
+ + `runSetup` integration (12). Total `__tests__/setup.test.ts` now
84
+ 93 cases. Full `@muhaven/mcp` suite: 241 cases passing.
85
+
86
+ ## [0.1.5] — 2026-05-17
87
+
88
+ Adds the `muhaven-broker stop` subcommand so operators can cleanly tear
89
+ down a detached daemon spawned by `muhaven-broker setup` without
90
+ hunting for PIDs in `ps` output or hand-rolling `taskkill` recipes.
91
+ Surfaced after `muhaven-broker setup` lands in 0.1.4 — operators
92
+ naturally asked "how do I stop this?" and the answer (`muhaven-broker
93
+ logout` + manual `kill`) was non-obvious.
94
+
95
+ ### Added
96
+
97
+ - **`muhaven-broker stop` subcommand** — clean shutdown:
98
+ - Probes the broker via `hello()`. Unreachable → "not running,
99
+ nothing to stop." exit 0.
100
+ - Best-effort `clearJwt()` so the OS keychain doesn't keep a stale
101
+ JWT after shutdown. Warning + continue on failure (don't abort
102
+ the kill).
103
+ - Reads `hello.pid` (new optional field — see below). On pre-0.1.5
104
+ daemons that omit the field, prints a manual-kill hint with
105
+ cross-platform commands and exits 1.
106
+ - `process.kill(pid, 'SIGTERM')` → polls `hello()` until it fails
107
+ (clean exit) or 5s elapses, then `process.kill(pid, 'SIGKILL')`.
108
+ - Pure orchestrator (`runStop` in `src/broker/stop.ts`) with
109
+ injectable IO so every branch is unit-testable without spawning
110
+ real processes.
111
+
112
+ ### Changed
113
+
114
+ - **`hello` response gains optional `pid?: number`** field (broker
115
+ protocol stays at 0.3.0 — additive optional field, back-compat with
116
+ pre-0.1.5 daemons). Populated from `process.pid` at request-handle
117
+ time; consumers MUST handle `undefined` for older daemons.
118
+
119
+ ### Tests
120
+
121
+ - 206 vitest pass (up from 197 in 0.1.4). Net +9 cases in new
122
+ `__tests__/stop.test.ts`:
123
+ - `runStop` not-running short-circuit
124
+ - happy path (hello → clearJwt → SIGTERM → exit-detected → 0)
125
+ - pre-0.1.5 daemon (no `pid` in hello) returns 1 with manual hint
126
+ - SIGKILL fallback after gracefulShutdownMs timeout
127
+ - SIGTERM permission error returns 1
128
+ - SIGKILL permission error returns 1 with "may be orphaned" hint
129
+ - `clearJwt` failure does NOT abort the kill (warning + continue)
130
+ - `defaultKillProcess` returns false on ESRCH (process gone)
131
+ - `defaultKillProcess` rethrows non-ESRCH errors (POSIX-only test)
132
+
10
133
  ## [0.1.4] — 2026-05-17
11
134
 
12
135
  Adds the one-shot `muhaven-broker setup` subcommand so a fresh install
package/dist/broker.cjs CHANGED
@@ -658,7 +658,8 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
658
658
  sessionKeyAddress: signer.address,
659
659
  hasJwt,
660
660
  hasSessionKey,
661
- ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
661
+ ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {},
662
+ ...options.pid !== void 0 ? { pid: options.pid } : {}
662
663
  };
663
664
  }
664
665
  case "sign_hash": {
@@ -889,7 +890,8 @@ var BrokerDaemon = class {
889
890
  effectiveConfig: {
890
891
  backendBaseUrl: this.config.backendBaseUrl,
891
892
  dashboardBaseUrl: this.config.dashboardBaseUrl
892
- }
893
+ },
894
+ pid: process.pid
893
895
  }
894
896
  );
895
897
  socket.end(serializeResponse(res));
@@ -1045,6 +1047,12 @@ async function waitForBroker(options) {
1045
1047
  `muhaven-broker daemon did not become reachable within ${timeoutMs}ms: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
1046
1048
  );
1047
1049
  }
1050
+ var KNOWN_REGISTER_HOSTS = [
1051
+ "claude-code",
1052
+ "claude-desktop",
1053
+ "cursor"
1054
+ ];
1055
+ var KNOWN_REGISTER_SCOPES = ["user", "project", "local"];
1048
1056
  function parseSetupFlags(argv) {
1049
1057
  let foreground = false;
1050
1058
  let noLaunchBrowser = false;
@@ -1052,6 +1060,8 @@ function parseSetupFlags(argv) {
1052
1060
  let backendBaseUrl;
1053
1061
  let dashboardBaseUrl;
1054
1062
  let skipLogin = false;
1063
+ const register = [];
1064
+ let registerScope = "user";
1055
1065
  for (let i = 0; i < argv.length; i++) {
1056
1066
  const a = argv[i];
1057
1067
  if (a === "--foreground" || a === "-f") foreground = true;
@@ -1060,7 +1070,29 @@ function parseSetupFlags(argv) {
1060
1070
  else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
1061
1071
  else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
1062
1072
  else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
1063
- 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}`);
1064
1096
  }
1065
1097
  return {
1066
1098
  foreground,
@@ -1068,8 +1100,76 @@ function parseSetupFlags(argv) {
1068
1100
  brokerEndpoint,
1069
1101
  backendBaseUrl,
1070
1102
  dashboardBaseUrl,
1071
- skipLogin
1103
+ skipLogin,
1104
+ register,
1105
+ registerScope
1106
+ };
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"
1072
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 };
1073
1173
  }
1074
1174
  async function runSetup(argv, deps) {
1075
1175
  let flags;
@@ -1078,7 +1178,7 @@ async function runSetup(argv, deps) {
1078
1178
  } catch (err) {
1079
1179
  deps.printErr(`error: ${err.message}`);
1080
1180
  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]"
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]"
1082
1182
  );
1083
1183
  return 2;
1084
1184
  }
@@ -1241,6 +1341,39 @@ async function runSetup(argv, deps) {
1241
1341
  return code;
1242
1342
  }
1243
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
+ }
1244
1377
  deps.print("");
1245
1378
  deps.print("================================");
1246
1379
  deps.print("Setup complete.");
@@ -1260,6 +1393,82 @@ async function runSetup(argv, deps) {
1260
1393
  return 0;
1261
1394
  }
1262
1395
 
1396
+ // src/broker/stop.ts
1397
+ async function runStop(deps) {
1398
+ const gracefulShutdownMs = deps.gracefulShutdownMs ?? 5e3;
1399
+ const pollIntervalMs = deps.pollIntervalMs ?? 200;
1400
+ const broker = deps.newBrokerClient(deps.endpoint, deps.brokerTimeoutMs);
1401
+ let hello;
1402
+ try {
1403
+ hello = await broker.hello();
1404
+ } catch {
1405
+ deps.print("Broker daemon: not running, nothing to stop.");
1406
+ return 0;
1407
+ }
1408
+ try {
1409
+ await broker.clearJwt();
1410
+ deps.print("JWT cleared from keystore.");
1411
+ } catch (err) {
1412
+ deps.print(
1413
+ `Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
1414
+ );
1415
+ }
1416
+ const pid = hello.pid;
1417
+ if (pid === void 0) {
1418
+ deps.printErr(
1419
+ "Broker daemon did not advertise its PID (older than @muhaven/mcp@0.1.5)."
1420
+ );
1421
+ deps.printErr("Stop manually with:");
1422
+ deps.printErr(" POSIX: pkill -f muhaven-broker");
1423
+ deps.printErr(" Windows: Stop-Process -Name node -Force (filter to muhaven-broker)");
1424
+ return 1;
1425
+ }
1426
+ try {
1427
+ deps.killProcess(pid, "SIGTERM");
1428
+ deps.print(`Sent SIGTERM to broker daemon (PID ${pid}). Waiting up to ${gracefulShutdownMs}ms for clean exit...`);
1429
+ } catch (err) {
1430
+ deps.printErr(
1431
+ `Failed to send SIGTERM to PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
1432
+ );
1433
+ return 1;
1434
+ }
1435
+ const maxAttempts = Math.ceil(gracefulShutdownMs / pollIntervalMs);
1436
+ for (let i = 0; i < maxAttempts; i++) {
1437
+ await deps.sleep(pollIntervalMs);
1438
+ try {
1439
+ await broker.hello();
1440
+ } catch {
1441
+ deps.print("Broker daemon stopped cleanly.");
1442
+ return 0;
1443
+ }
1444
+ }
1445
+ deps.print(`Daemon did not exit after ${gracefulShutdownMs}ms \u2014 sending SIGKILL.`);
1446
+ try {
1447
+ deps.killProcess(pid, "SIGKILL");
1448
+ deps.print(`Broker daemon force-killed (PID ${pid}).`);
1449
+ return 0;
1450
+ } catch (err) {
1451
+ deps.printErr(
1452
+ `Failed to SIGKILL PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
1453
+ );
1454
+ deps.printErr(
1455
+ " Daemon process may be orphaned. Inspect with `ps aux | grep muhaven-broker` and kill manually."
1456
+ );
1457
+ return 1;
1458
+ }
1459
+ }
1460
+ function defaultKillProcess(pid, signal) {
1461
+ try {
1462
+ process.kill(pid, signal);
1463
+ return true;
1464
+ } catch (err) {
1465
+ if (err.code === "ESRCH") {
1466
+ return false;
1467
+ }
1468
+ throw err;
1469
+ }
1470
+ }
1471
+
1263
1472
  // src/broker/cli.ts
1264
1473
  function print(line) {
1265
1474
  process.stdout.write(line + "\n");
@@ -1526,16 +1735,22 @@ function printUsage() {
1526
1735
  print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
1527
1736
  print(" [--skip-login] starts the daemon but lets you run login later");
1528
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)");
1742
+ print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
1743
+ print(" after 5s). Also clears the keystore JWT as a best effort.");
1529
1744
  print(" login Acquire a JWT via the device-code flow + store in keystore");
1530
1745
  print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
1531
- print(" logout Clear the JWT from the keystore");
1746
+ print(" logout Clear the JWT from the keystore (does NOT stop the daemon)");
1532
1747
  print(" doctor Print environment + keystore + reachability report");
1533
1748
  print(" -h, --help Show this help");
1534
1749
  print(" -v, --version Print the @muhaven/mcp package version");
1535
1750
  }
1536
1751
  function getBrokerPackageVersion() {
1537
1752
  {
1538
- return "0.1.4";
1753
+ return "0.1.6";
1539
1754
  }
1540
1755
  }
1541
1756
  function printVersion() {
@@ -1544,6 +1759,33 @@ function printVersion() {
1544
1759
  function resolveBrokerBinPath() {
1545
1760
  return path.resolve(__dirname, "..", "bin", "muhaven-broker.cjs");
1546
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
+ }
1547
1789
  async function runSetup2(argv) {
1548
1790
  const deps = {
1549
1791
  print,
@@ -1557,10 +1799,24 @@ async function runSetup2(argv) {
1557
1799
  resolveBinPath: resolveBrokerBinPath,
1558
1800
  env: process.env,
1559
1801
  platformId: process.platform,
1560
- osRelease: os.release()
1802
+ osRelease: os.release(),
1803
+ shellOut: defaultShellOut
1561
1804
  };
1562
1805
  return runSetup(argv, deps);
1563
1806
  }
1807
+ async function runStop2() {
1808
+ const config = loadMcpConfig();
1809
+ const deps = {
1810
+ print,
1811
+ printErr,
1812
+ newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
1813
+ killProcess: defaultKillProcess,
1814
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1815
+ endpoint: config.brokerEndpoint,
1816
+ brokerTimeoutMs: config.brokerTimeoutMs
1817
+ };
1818
+ return runStop(deps);
1819
+ }
1564
1820
  async function runCli(argv) {
1565
1821
  const [sub, ...rest] = argv;
1566
1822
  switch (sub) {
@@ -1569,6 +1825,8 @@ async function runCli(argv) {
1569
1825
  return 0;
1570
1826
  case "setup":
1571
1827
  return runSetup2(rest);
1828
+ case "stop":
1829
+ return runStop2();
1572
1830
  case "login":
1573
1831
  return runLogin(rest);
1574
1832
  case "logout":
@@ -1597,3 +1855,4 @@ exports.runDoctor = runDoctor;
1597
1855
  exports.runLogin = runLogin;
1598
1856
  exports.runLogout = runLogout;
1599
1857
  exports.runSetup = runSetup2;
1858
+ exports.runStop = runStop2;
package/dist/broker.d.cts CHANGED
@@ -39,6 +39,10 @@ declare function getBrokerPackageVersion(): string;
39
39
  * surface unnecessarily).
40
40
  */
41
41
  declare function runSetup(argv: readonly string[]): Promise<number>;
42
+ /**
43
+ * Wire `runStop` against the real BrokerClient + Node's process.kill.
44
+ */
45
+ declare function runStop(): Promise<number>;
42
46
  declare function runCli(argv: readonly string[]): Promise<number>;
43
47
 
44
- export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup };
48
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStop };
package/dist/broker.d.ts CHANGED
@@ -39,6 +39,10 @@ declare function getBrokerPackageVersion(): string;
39
39
  * surface unnecessarily).
40
40
  */
41
41
  declare function runSetup(argv: readonly string[]): Promise<number>;
42
+ /**
43
+ * Wire `runStop` against the real BrokerClient + Node's process.kill.
44
+ */
45
+ declare function runStop(): Promise<number>;
42
46
  declare function runCli(argv: readonly string[]): Promise<number>;
43
47
 
44
- export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup };
48
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStop };
package/dist/broker.js CHANGED
@@ -660,7 +660,8 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
660
660
  sessionKeyAddress: signer.address,
661
661
  hasJwt,
662
662
  hasSessionKey,
663
- ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
663
+ ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {},
664
+ ...options.pid !== void 0 ? { pid: options.pid } : {}
664
665
  };
665
666
  }
666
667
  case "sign_hash": {
@@ -891,7 +892,8 @@ var BrokerDaemon = class {
891
892
  effectiveConfig: {
892
893
  backendBaseUrl: this.config.backendBaseUrl,
893
894
  dashboardBaseUrl: this.config.dashboardBaseUrl
894
- }
895
+ },
896
+ pid: process.pid
895
897
  }
896
898
  );
897
899
  socket.end(serializeResponse(res));
@@ -1047,6 +1049,12 @@ async function waitForBroker(options) {
1047
1049
  `muhaven-broker daemon did not become reachable within ${timeoutMs}ms: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
1048
1050
  );
1049
1051
  }
1052
+ var KNOWN_REGISTER_HOSTS = [
1053
+ "claude-code",
1054
+ "claude-desktop",
1055
+ "cursor"
1056
+ ];
1057
+ var KNOWN_REGISTER_SCOPES = ["user", "project", "local"];
1050
1058
  function parseSetupFlags(argv) {
1051
1059
  let foreground = false;
1052
1060
  let noLaunchBrowser = false;
@@ -1054,6 +1062,8 @@ function parseSetupFlags(argv) {
1054
1062
  let backendBaseUrl;
1055
1063
  let dashboardBaseUrl;
1056
1064
  let skipLogin = false;
1065
+ const register = [];
1066
+ let registerScope = "user";
1057
1067
  for (let i = 0; i < argv.length; i++) {
1058
1068
  const a = argv[i];
1059
1069
  if (a === "--foreground" || a === "-f") foreground = true;
@@ -1062,7 +1072,29 @@ function parseSetupFlags(argv) {
1062
1072
  else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
1063
1073
  else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
1064
1074
  else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
1065
- 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}`);
1066
1098
  }
1067
1099
  return {
1068
1100
  foreground,
@@ -1070,8 +1102,76 @@ function parseSetupFlags(argv) {
1070
1102
  brokerEndpoint,
1071
1103
  backendBaseUrl,
1072
1104
  dashboardBaseUrl,
1073
- skipLogin
1105
+ skipLogin,
1106
+ register,
1107
+ registerScope
1108
+ };
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"
1074
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 };
1075
1175
  }
1076
1176
  async function runSetup(argv, deps) {
1077
1177
  let flags;
@@ -1080,7 +1180,7 @@ async function runSetup(argv, deps) {
1080
1180
  } catch (err) {
1081
1181
  deps.printErr(`error: ${err.message}`);
1082
1182
  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]"
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]"
1084
1184
  );
1085
1185
  return 2;
1086
1186
  }
@@ -1243,6 +1343,39 @@ async function runSetup(argv, deps) {
1243
1343
  return code;
1244
1344
  }
1245
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
+ }
1246
1379
  deps.print("");
1247
1380
  deps.print("================================");
1248
1381
  deps.print("Setup complete.");
@@ -1262,6 +1395,82 @@ async function runSetup(argv, deps) {
1262
1395
  return 0;
1263
1396
  }
1264
1397
 
1398
+ // src/broker/stop.ts
1399
+ async function runStop(deps) {
1400
+ const gracefulShutdownMs = deps.gracefulShutdownMs ?? 5e3;
1401
+ const pollIntervalMs = deps.pollIntervalMs ?? 200;
1402
+ const broker = deps.newBrokerClient(deps.endpoint, deps.brokerTimeoutMs);
1403
+ let hello;
1404
+ try {
1405
+ hello = await broker.hello();
1406
+ } catch {
1407
+ deps.print("Broker daemon: not running, nothing to stop.");
1408
+ return 0;
1409
+ }
1410
+ try {
1411
+ await broker.clearJwt();
1412
+ deps.print("JWT cleared from keystore.");
1413
+ } catch (err) {
1414
+ deps.print(
1415
+ `Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
1416
+ );
1417
+ }
1418
+ const pid = hello.pid;
1419
+ if (pid === void 0) {
1420
+ deps.printErr(
1421
+ "Broker daemon did not advertise its PID (older than @muhaven/mcp@0.1.5)."
1422
+ );
1423
+ deps.printErr("Stop manually with:");
1424
+ deps.printErr(" POSIX: pkill -f muhaven-broker");
1425
+ deps.printErr(" Windows: Stop-Process -Name node -Force (filter to muhaven-broker)");
1426
+ return 1;
1427
+ }
1428
+ try {
1429
+ deps.killProcess(pid, "SIGTERM");
1430
+ deps.print(`Sent SIGTERM to broker daemon (PID ${pid}). Waiting up to ${gracefulShutdownMs}ms for clean exit...`);
1431
+ } catch (err) {
1432
+ deps.printErr(
1433
+ `Failed to send SIGTERM to PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
1434
+ );
1435
+ return 1;
1436
+ }
1437
+ const maxAttempts = Math.ceil(gracefulShutdownMs / pollIntervalMs);
1438
+ for (let i = 0; i < maxAttempts; i++) {
1439
+ await deps.sleep(pollIntervalMs);
1440
+ try {
1441
+ await broker.hello();
1442
+ } catch {
1443
+ deps.print("Broker daemon stopped cleanly.");
1444
+ return 0;
1445
+ }
1446
+ }
1447
+ deps.print(`Daemon did not exit after ${gracefulShutdownMs}ms \u2014 sending SIGKILL.`);
1448
+ try {
1449
+ deps.killProcess(pid, "SIGKILL");
1450
+ deps.print(`Broker daemon force-killed (PID ${pid}).`);
1451
+ return 0;
1452
+ } catch (err) {
1453
+ deps.printErr(
1454
+ `Failed to SIGKILL PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
1455
+ );
1456
+ deps.printErr(
1457
+ " Daemon process may be orphaned. Inspect with `ps aux | grep muhaven-broker` and kill manually."
1458
+ );
1459
+ return 1;
1460
+ }
1461
+ }
1462
+ function defaultKillProcess(pid, signal) {
1463
+ try {
1464
+ process.kill(pid, signal);
1465
+ return true;
1466
+ } catch (err) {
1467
+ if (err.code === "ESRCH") {
1468
+ return false;
1469
+ }
1470
+ throw err;
1471
+ }
1472
+ }
1473
+
1265
1474
  // src/broker/cli.ts
1266
1475
  function print(line) {
1267
1476
  process.stdout.write(line + "\n");
@@ -1528,16 +1737,22 @@ function printUsage() {
1528
1737
  print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
1529
1738
  print(" [--skip-login] starts the daemon but lets you run login later");
1530
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)");
1744
+ print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
1745
+ print(" after 5s). Also clears the keystore JWT as a best effort.");
1531
1746
  print(" login Acquire a JWT via the device-code flow + store in keystore");
1532
1747
  print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
1533
- print(" logout Clear the JWT from the keystore");
1748
+ print(" logout Clear the JWT from the keystore (does NOT stop the daemon)");
1534
1749
  print(" doctor Print environment + keystore + reachability report");
1535
1750
  print(" -h, --help Show this help");
1536
1751
  print(" -v, --version Print the @muhaven/mcp package version");
1537
1752
  }
1538
1753
  function getBrokerPackageVersion() {
1539
1754
  {
1540
- return "0.1.4";
1755
+ return "0.1.6";
1541
1756
  }
1542
1757
  }
1543
1758
  function printVersion() {
@@ -1546,6 +1761,33 @@ function printVersion() {
1546
1761
  function resolveBrokerBinPath() {
1547
1762
  return resolve(__dirname$1, "..", "bin", "muhaven-broker.cjs");
1548
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
+ }
1549
1791
  async function runSetup2(argv) {
1550
1792
  const deps = {
1551
1793
  print,
@@ -1559,10 +1801,24 @@ async function runSetup2(argv) {
1559
1801
  resolveBinPath: resolveBrokerBinPath,
1560
1802
  env: process.env,
1561
1803
  platformId: process.platform,
1562
- osRelease: release()
1804
+ osRelease: release(),
1805
+ shellOut: defaultShellOut
1563
1806
  };
1564
1807
  return runSetup(argv, deps);
1565
1808
  }
1809
+ async function runStop2() {
1810
+ const config = loadMcpConfig();
1811
+ const deps = {
1812
+ print,
1813
+ printErr,
1814
+ newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
1815
+ killProcess: defaultKillProcess,
1816
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
1817
+ endpoint: config.brokerEndpoint,
1818
+ brokerTimeoutMs: config.brokerTimeoutMs
1819
+ };
1820
+ return runStop(deps);
1821
+ }
1566
1822
  async function runCli(argv) {
1567
1823
  const [sub, ...rest] = argv;
1568
1824
  switch (sub) {
@@ -1571,6 +1827,8 @@ async function runCli(argv) {
1571
1827
  return 0;
1572
1828
  case "setup":
1573
1829
  return runSetup2(rest);
1830
+ case "stop":
1831
+ return runStop2();
1574
1832
  case "login":
1575
1833
  return runLogin(rest);
1576
1834
  case "logout":
@@ -1592,4 +1850,4 @@ async function runCli(argv) {
1592
1850
  }
1593
1851
  }
1594
1852
 
1595
- export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup2 as runSetup };
1853
+ export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup2 as runSetup, runStop2 as runStop };
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.4";
1213
+ return "0.1.6";
1214
1214
  }
1215
1215
  }
1216
1216
  function toJsonInputSchema(schema) {
@@ -1794,7 +1794,8 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
1794
1794
  sessionKeyAddress: signer.address,
1795
1795
  hasJwt,
1796
1796
  hasSessionKey,
1797
- ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
1797
+ ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {},
1798
+ ...options.pid !== void 0 ? { pid: options.pid } : {}
1798
1799
  };
1799
1800
  }
1800
1801
  case "sign_hash": {
@@ -2025,7 +2026,8 @@ var BrokerDaemon = class {
2025
2026
  effectiveConfig: {
2026
2027
  backendBaseUrl: this.config.backendBaseUrl,
2027
2028
  dashboardBaseUrl: this.config.dashboardBaseUrl
2028
- }
2029
+ },
2030
+ pid: process.pid
2029
2031
  }
2030
2032
  );
2031
2033
  socket.end(serializeResponse(res));
package/dist/index.d.cts CHANGED
@@ -18,6 +18,12 @@ import { z } from 'zod';
18
18
  * keeper of the device-flow JWT (per ADR-3 D1 "polling, not loopback
19
19
  * callback") in addition to the session-key private half.
20
20
  *
21
+ * @muhaven/mcp@0.1.5 added `hello.pid` (still protocol 0.3.0 — additive
22
+ * optional field) so `muhaven-broker stop` can reach into the daemon
23
+ * process by PID without forcing operators to grep `ps` output. Older
24
+ * 0.1.4 daemons omit the field; `runStop` falls back to a structured
25
+ * error message in that case.
26
+ *
21
27
  * Threat-model invariants:
22
28
  * - The broker NEVER reaches out to the network. It only:
23
29
  * (a) signs hashes that the MCP server received from the backend,
@@ -94,6 +100,13 @@ interface BrokerHelloResponse {
94
100
  readonly backendBaseUrl: string;
95
101
  readonly dashboardBaseUrl: string;
96
102
  };
103
+ /**
104
+ * OS process id of the daemon. Surfaced so `muhaven-broker stop` can
105
+ * `process.kill(pid, 'SIGTERM')` without grepping `ps` output. Added in
106
+ * @muhaven/mcp@0.1.5 (no protocol-version bump — additive optional
107
+ * field). Older daemons omit; consumers MUST handle `undefined`.
108
+ */
109
+ readonly pid?: number;
97
110
  }
98
111
  interface BrokerSignHashResponse {
99
112
  readonly type: 'sign_hash';
@@ -804,6 +817,13 @@ interface HandleBrokerRequestOptions {
804
817
  readonly backendBaseUrl: string;
805
818
  readonly dashboardBaseUrl: string;
806
819
  };
820
+ /**
821
+ * The daemon's own OS PID. Injectable so unit tests can pin a value;
822
+ * production callers pass `process.pid` from the daemon process at
823
+ * handler-construction time. Surfaced via `hello.pid` so
824
+ * `muhaven-broker stop` can SIGTERM by PID. Added in @muhaven/mcp@0.1.5.
825
+ */
826
+ pid?: number;
807
827
  }
808
828
  declare function handleBrokerRequest(req: BrokerRequest, signer: ISigner, keystore: IKeystore, nowSec?: () => number, options?: HandleBrokerRequestOptions): Promise<BrokerResponse>;
809
829
  declare class BrokerDaemon {
package/dist/index.d.ts CHANGED
@@ -18,6 +18,12 @@ import { z } from 'zod';
18
18
  * keeper of the device-flow JWT (per ADR-3 D1 "polling, not loopback
19
19
  * callback") in addition to the session-key private half.
20
20
  *
21
+ * @muhaven/mcp@0.1.5 added `hello.pid` (still protocol 0.3.0 — additive
22
+ * optional field) so `muhaven-broker stop` can reach into the daemon
23
+ * process by PID without forcing operators to grep `ps` output. Older
24
+ * 0.1.4 daemons omit the field; `runStop` falls back to a structured
25
+ * error message in that case.
26
+ *
21
27
  * Threat-model invariants:
22
28
  * - The broker NEVER reaches out to the network. It only:
23
29
  * (a) signs hashes that the MCP server received from the backend,
@@ -94,6 +100,13 @@ interface BrokerHelloResponse {
94
100
  readonly backendBaseUrl: string;
95
101
  readonly dashboardBaseUrl: string;
96
102
  };
103
+ /**
104
+ * OS process id of the daemon. Surfaced so `muhaven-broker stop` can
105
+ * `process.kill(pid, 'SIGTERM')` without grepping `ps` output. Added in
106
+ * @muhaven/mcp@0.1.5 (no protocol-version bump — additive optional
107
+ * field). Older daemons omit; consumers MUST handle `undefined`.
108
+ */
109
+ readonly pid?: number;
97
110
  }
98
111
  interface BrokerSignHashResponse {
99
112
  readonly type: 'sign_hash';
@@ -804,6 +817,13 @@ interface HandleBrokerRequestOptions {
804
817
  readonly backendBaseUrl: string;
805
818
  readonly dashboardBaseUrl: string;
806
819
  };
820
+ /**
821
+ * The daemon's own OS PID. Injectable so unit tests can pin a value;
822
+ * production callers pass `process.pid` from the daemon process at
823
+ * handler-construction time. Surfaced via `hello.pid` so
824
+ * `muhaven-broker stop` can SIGTERM by PID. Added in @muhaven/mcp@0.1.5.
825
+ */
826
+ pid?: number;
807
827
  }
808
828
  declare function handleBrokerRequest(req: BrokerRequest, signer: ISigner, keystore: IKeystore, nowSec?: () => number, options?: HandleBrokerRequestOptions): Promise<BrokerResponse>;
809
829
  declare class BrokerDaemon {
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.4";
1209
+ return "0.1.6";
1210
1210
  }
1211
1211
  }
1212
1212
  function toJsonInputSchema(schema) {
@@ -1790,7 +1790,8 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
1790
1790
  sessionKeyAddress: signer.address,
1791
1791
  hasJwt,
1792
1792
  hasSessionKey,
1793
- ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
1793
+ ...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {},
1794
+ ...options.pid !== void 0 ? { pid: options.pid } : {}
1794
1795
  };
1795
1796
  }
1796
1797
  case "sign_hash": {
@@ -2021,7 +2022,8 @@ var BrokerDaemon = class {
2021
2022
  effectiveConfig: {
2022
2023
  backendBaseUrl: this.config.backendBaseUrl,
2023
2024
  dashboardBaseUrl: this.config.dashboardBaseUrl
2024
- }
2025
+ },
2026
+ pid: process.pid
2025
2027
  }
2026
2028
  );
2027
2029
  socket.end(serializeResponse(res));
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.4",
6
+ "version": "0.1.6",
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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muhaven/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {