@muhaven/mcp 0.1.3 → 0.1.5
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 +177 -0
- package/bin/muhaven-mcp.cjs +45 -0
- package/dist/broker.cjs +467 -5
- package/dist/broker.d.cts +13 -1
- package/dist/broker.d.ts +13 -1
- package/dist/broker.js +474 -11
- package/dist/index.cjs +5 -3
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +5 -3
- package/manifest.json +2 -2
- package/package.json +1 -1
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;
|
|
@@ -659,7 +658,8 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
659
658
|
sessionKeyAddress: signer.address,
|
|
660
659
|
hasJwt,
|
|
661
660
|
hasSessionKey,
|
|
662
|
-
...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
|
|
661
|
+
...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {},
|
|
662
|
+
...options.pid !== void 0 ? { pid: options.pid } : {}
|
|
663
663
|
};
|
|
664
664
|
}
|
|
665
665
|
case "sign_hash": {
|
|
@@ -890,7 +890,8 @@ var BrokerDaemon = class {
|
|
|
890
890
|
effectiveConfig: {
|
|
891
891
|
backendBaseUrl: this.config.backendBaseUrl,
|
|
892
892
|
dashboardBaseUrl: this.config.dashboardBaseUrl
|
|
893
|
-
}
|
|
893
|
+
},
|
|
894
|
+
pid: process.pid
|
|
894
895
|
}
|
|
895
896
|
);
|
|
896
897
|
socket.end(serializeResponse(res));
|
|
@@ -934,6 +935,408 @@ async function runBrokerDaemonCli() {
|
|
|
934
935
|
await new Promise(() => {
|
|
935
936
|
});
|
|
936
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 ?? os.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 accounts.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 = child_process.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
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/broker/stop.ts
|
|
1266
|
+
async function runStop(deps) {
|
|
1267
|
+
const gracefulShutdownMs = deps.gracefulShutdownMs ?? 5e3;
|
|
1268
|
+
const pollIntervalMs = deps.pollIntervalMs ?? 200;
|
|
1269
|
+
const broker = deps.newBrokerClient(deps.endpoint, deps.brokerTimeoutMs);
|
|
1270
|
+
let hello;
|
|
1271
|
+
try {
|
|
1272
|
+
hello = await broker.hello();
|
|
1273
|
+
} catch {
|
|
1274
|
+
deps.print("Broker daemon: not running, nothing to stop.");
|
|
1275
|
+
return 0;
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
await broker.clearJwt();
|
|
1279
|
+
deps.print("JWT cleared from keystore.");
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
deps.print(
|
|
1282
|
+
`Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
const pid = hello.pid;
|
|
1286
|
+
if (pid === void 0) {
|
|
1287
|
+
deps.printErr(
|
|
1288
|
+
"Broker daemon did not advertise its PID (older than @muhaven/mcp@0.1.5)."
|
|
1289
|
+
);
|
|
1290
|
+
deps.printErr("Stop manually with:");
|
|
1291
|
+
deps.printErr(" POSIX: pkill -f muhaven-broker");
|
|
1292
|
+
deps.printErr(" Windows: Stop-Process -Name node -Force (filter to muhaven-broker)");
|
|
1293
|
+
return 1;
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
deps.killProcess(pid, "SIGTERM");
|
|
1297
|
+
deps.print(`Sent SIGTERM to broker daemon (PID ${pid}). Waiting up to ${gracefulShutdownMs}ms for clean exit...`);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
deps.printErr(
|
|
1300
|
+
`Failed to send SIGTERM to PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
|
|
1301
|
+
);
|
|
1302
|
+
return 1;
|
|
1303
|
+
}
|
|
1304
|
+
const maxAttempts = Math.ceil(gracefulShutdownMs / pollIntervalMs);
|
|
1305
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1306
|
+
await deps.sleep(pollIntervalMs);
|
|
1307
|
+
try {
|
|
1308
|
+
await broker.hello();
|
|
1309
|
+
} catch {
|
|
1310
|
+
deps.print("Broker daemon stopped cleanly.");
|
|
1311
|
+
return 0;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
deps.print(`Daemon did not exit after ${gracefulShutdownMs}ms \u2014 sending SIGKILL.`);
|
|
1315
|
+
try {
|
|
1316
|
+
deps.killProcess(pid, "SIGKILL");
|
|
1317
|
+
deps.print(`Broker daemon force-killed (PID ${pid}).`);
|
|
1318
|
+
return 0;
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
deps.printErr(
|
|
1321
|
+
`Failed to SIGKILL PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
|
|
1322
|
+
);
|
|
1323
|
+
deps.printErr(
|
|
1324
|
+
" Daemon process may be orphaned. Inspect with `ps aux | grep muhaven-broker` and kill manually."
|
|
1325
|
+
);
|
|
1326
|
+
return 1;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function defaultKillProcess(pid, signal) {
|
|
1330
|
+
try {
|
|
1331
|
+
process.kill(pid, signal);
|
|
1332
|
+
return true;
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
if (err.code === "ESRCH") {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
throw err;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
937
1340
|
|
|
938
1341
|
// src/broker/cli.ts
|
|
939
1342
|
function print(line) {
|
|
@@ -943,7 +1346,7 @@ function printErr(line) {
|
|
|
943
1346
|
process.stderr.write(line + "\n");
|
|
944
1347
|
}
|
|
945
1348
|
function detectMcpHost() {
|
|
946
|
-
return process.env.MCP_HOST_NAME ?? process.env.CLAUDE_CODE_HOST ??
|
|
1349
|
+
return process.env.MCP_HOST_NAME ?? process.env.CLAUDE_CODE_HOST ?? "muhaven-broker-cli";
|
|
947
1350
|
}
|
|
948
1351
|
function detectEnvironment() {
|
|
949
1352
|
const warnings = [];
|
|
@@ -1197,11 +1600,59 @@ function printUsage() {
|
|
|
1197
1600
|
print("usage: muhaven-broker [<subcommand>] [options]");
|
|
1198
1601
|
print("");
|
|
1199
1602
|
print(" (no subcommand) Run the daemon (production mode)");
|
|
1603
|
+
print(" setup One-shot install: env defaults + session key + detached daemon + login");
|
|
1604
|
+
print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
|
|
1605
|
+
print(" [--skip-login] starts the daemon but lets you run login later");
|
|
1606
|
+
print(" [--no-launch-browser] pass-through to login");
|
|
1607
|
+
print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
|
|
1608
|
+
print(" after 5s). Also clears the keystore JWT as a best effort.");
|
|
1200
1609
|
print(" login Acquire a JWT via the device-code flow + store in keystore");
|
|
1201
1610
|
print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
|
|
1202
|
-
print(" logout Clear the JWT from the keystore");
|
|
1611
|
+
print(" logout Clear the JWT from the keystore (does NOT stop the daemon)");
|
|
1203
1612
|
print(" doctor Print environment + keystore + reachability report");
|
|
1204
1613
|
print(" -h, --help Show this help");
|
|
1614
|
+
print(" -v, --version Print the @muhaven/mcp package version");
|
|
1615
|
+
}
|
|
1616
|
+
function getBrokerPackageVersion() {
|
|
1617
|
+
{
|
|
1618
|
+
return "0.1.5";
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function printVersion() {
|
|
1622
|
+
print(`muhaven-broker @muhaven/mcp@${getBrokerPackageVersion()}`);
|
|
1623
|
+
}
|
|
1624
|
+
function resolveBrokerBinPath() {
|
|
1625
|
+
return path.resolve(__dirname, "..", "bin", "muhaven-broker.cjs");
|
|
1626
|
+
}
|
|
1627
|
+
async function runSetup2(argv) {
|
|
1628
|
+
const deps = {
|
|
1629
|
+
print,
|
|
1630
|
+
printErr,
|
|
1631
|
+
mintSessionKey,
|
|
1632
|
+
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
1633
|
+
spawnDaemon,
|
|
1634
|
+
waitForBroker,
|
|
1635
|
+
runLogin,
|
|
1636
|
+
runForegroundDaemon: runBrokerDaemonCli,
|
|
1637
|
+
resolveBinPath: resolveBrokerBinPath,
|
|
1638
|
+
env: process.env,
|
|
1639
|
+
platformId: process.platform,
|
|
1640
|
+
osRelease: os.release()
|
|
1641
|
+
};
|
|
1642
|
+
return runSetup(argv, deps);
|
|
1643
|
+
}
|
|
1644
|
+
async function runStop2() {
|
|
1645
|
+
const config = loadMcpConfig();
|
|
1646
|
+
const deps = {
|
|
1647
|
+
print,
|
|
1648
|
+
printErr,
|
|
1649
|
+
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
1650
|
+
killProcess: defaultKillProcess,
|
|
1651
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
1652
|
+
endpoint: config.brokerEndpoint,
|
|
1653
|
+
brokerTimeoutMs: config.brokerTimeoutMs
|
|
1654
|
+
};
|
|
1655
|
+
return runStop(deps);
|
|
1205
1656
|
}
|
|
1206
1657
|
async function runCli(argv) {
|
|
1207
1658
|
const [sub, ...rest] = argv;
|
|
@@ -1209,6 +1660,10 @@ async function runCli(argv) {
|
|
|
1209
1660
|
case void 0:
|
|
1210
1661
|
await runBrokerDaemonCli();
|
|
1211
1662
|
return 0;
|
|
1663
|
+
case "setup":
|
|
1664
|
+
return runSetup2(rest);
|
|
1665
|
+
case "stop":
|
|
1666
|
+
return runStop2();
|
|
1212
1667
|
case "login":
|
|
1213
1668
|
return runLogin(rest);
|
|
1214
1669
|
case "logout":
|
|
@@ -1219,6 +1674,10 @@ async function runCli(argv) {
|
|
|
1219
1674
|
case "--help":
|
|
1220
1675
|
printUsage();
|
|
1221
1676
|
return 0;
|
|
1677
|
+
case "-v":
|
|
1678
|
+
case "--version":
|
|
1679
|
+
printVersion();
|
|
1680
|
+
return 0;
|
|
1222
1681
|
default:
|
|
1223
1682
|
printErr(`unknown subcommand: ${sub}`);
|
|
1224
1683
|
printUsage();
|
|
@@ -1226,8 +1685,11 @@ async function runCli(argv) {
|
|
|
1226
1685
|
}
|
|
1227
1686
|
}
|
|
1228
1687
|
|
|
1688
|
+
exports.getBrokerPackageVersion = getBrokerPackageVersion;
|
|
1229
1689
|
exports.parseLoginFlags = parseLoginFlags;
|
|
1230
1690
|
exports.runCli = runCli;
|
|
1231
1691
|
exports.runDoctor = runDoctor;
|
|
1232
1692
|
exports.runLogin = runLogin;
|
|
1233
1693
|
exports.runLogout = runLogout;
|
|
1694
|
+
exports.runSetup = runSetup2;
|
|
1695
|
+
exports.runStop = runStop2;
|
package/dist/broker.d.cts
CHANGED
|
@@ -31,6 +31,18 @@ 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>;
|
|
42
|
+
/**
|
|
43
|
+
* Wire `runStop` against the real BrokerClient + Node's process.kill.
|
|
44
|
+
*/
|
|
45
|
+
declare function runStop(): Promise<number>;
|
|
34
46
|
declare function runCli(argv: readonly string[]): Promise<number>;
|
|
35
47
|
|
|
36
|
-
export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
|
|
48
|
+
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStop };
|
package/dist/broker.d.ts
CHANGED
|
@@ -31,6 +31,18 @@ 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>;
|
|
42
|
+
/**
|
|
43
|
+
* Wire `runStop` against the real BrokerClient + Node's process.kill.
|
|
44
|
+
*/
|
|
45
|
+
declare function runStop(): Promise<number>;
|
|
34
46
|
declare function runCli(argv: readonly string[]): Promise<number>;
|
|
35
47
|
|
|
36
|
-
export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
|
|
48
|
+
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStop };
|