@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 +130 -0
- package/bin/muhaven-mcp.cjs +45 -0
- package/dist/broker.cjs +368 -2
- package/dist/broker.d.cts +9 -1
- package/dist/broker.d.ts +9 -1
- package/dist/broker.js +376 -8
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/manifest.json +2 -2
- package/package.json +1 -1
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⁶
|
package/bin/muhaven-mcp.cjs
CHANGED
|
@@ -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 ??
|
|
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
|
-
|
|
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(
|
|
422
|
-
this.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 ??
|
|
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
package/dist/index.js
CHANGED
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.
|
|
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)
|
|
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
|
+
"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": {
|