@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.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";
|
|
@@ -657,7 +660,8 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
657
660
|
sessionKeyAddress: signer.address,
|
|
658
661
|
hasJwt,
|
|
659
662
|
hasSessionKey,
|
|
660
|
-
...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
|
|
663
|
+
...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {},
|
|
664
|
+
...options.pid !== void 0 ? { pid: options.pid } : {}
|
|
661
665
|
};
|
|
662
666
|
}
|
|
663
667
|
case "sign_hash": {
|
|
@@ -888,7 +892,8 @@ var BrokerDaemon = class {
|
|
|
888
892
|
effectiveConfig: {
|
|
889
893
|
backendBaseUrl: this.config.backendBaseUrl,
|
|
890
894
|
dashboardBaseUrl: this.config.dashboardBaseUrl
|
|
891
|
-
}
|
|
895
|
+
},
|
|
896
|
+
pid: process.pid
|
|
892
897
|
}
|
|
893
898
|
);
|
|
894
899
|
socket.end(serializeResponse(res));
|
|
@@ -932,6 +937,408 @@ async function runBrokerDaemonCli() {
|
|
|
932
937
|
await new Promise(() => {
|
|
933
938
|
});
|
|
934
939
|
}
|
|
940
|
+
var DANGEROUS_NODE_ENV_VARS = [
|
|
941
|
+
"NODE_OPTIONS",
|
|
942
|
+
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
943
|
+
"NODE_EXTRA_CA_CERTS",
|
|
944
|
+
"NODE_PATH"
|
|
945
|
+
];
|
|
946
|
+
function applyEnvDefaults(input) {
|
|
947
|
+
const { env } = input;
|
|
948
|
+
const platformId = input.platformId ?? process.platform;
|
|
949
|
+
const osRelease = input.osRelease ?? release();
|
|
950
|
+
const toSet = {};
|
|
951
|
+
const preserved = [];
|
|
952
|
+
const defaultIfUnset = (name, value) => {
|
|
953
|
+
if (env[name] && env[name].length > 0) {
|
|
954
|
+
preserved.push(name);
|
|
955
|
+
} else {
|
|
956
|
+
toSet[name] = value;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
defaultIfUnset("MUHAVEN_BACKEND_URL", "https://api.muhaven.app");
|
|
960
|
+
defaultIfUnset("MUHAVEN_DASHBOARD_URL", "https://muhaven.app");
|
|
961
|
+
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;
|
|
962
|
+
if (wantFileKeyring) {
|
|
963
|
+
defaultIfUnset("MUHAVEN_KEYRING", "file");
|
|
964
|
+
} else if (env.MUHAVEN_KEYRING) {
|
|
965
|
+
preserved.push("MUHAVEN_KEYRING");
|
|
966
|
+
}
|
|
967
|
+
return { toSet, preserved };
|
|
968
|
+
}
|
|
969
|
+
function mintSessionKey() {
|
|
970
|
+
return generatePrivateKey();
|
|
971
|
+
}
|
|
972
|
+
function decideSetupAction(input) {
|
|
973
|
+
if (input.hello === null) return "spawn_and_login";
|
|
974
|
+
if (!input.hello.hasJwt) return "login_only";
|
|
975
|
+
return "already_ready";
|
|
976
|
+
}
|
|
977
|
+
function spawnDaemon(options) {
|
|
978
|
+
const sanitized = {};
|
|
979
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
980
|
+
if (!DANGEROUS_NODE_ENV_VARS.includes(k)) {
|
|
981
|
+
sanitized[k] = v;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const merged = { ...sanitized, ...options.env };
|
|
985
|
+
const child = spawn(process.execPath, [options.binPath], {
|
|
986
|
+
detached: true,
|
|
987
|
+
stdio: "ignore",
|
|
988
|
+
windowsHide: true,
|
|
989
|
+
env: merged
|
|
990
|
+
});
|
|
991
|
+
child.unref();
|
|
992
|
+
if (child.pid === void 0) {
|
|
993
|
+
throw new Error("failed to spawn muhaven-broker daemon \u2014 child pid is undefined");
|
|
994
|
+
}
|
|
995
|
+
return child.pid;
|
|
996
|
+
}
|
|
997
|
+
function validateHttpUrlFlag(name, value) {
|
|
998
|
+
let parsed;
|
|
999
|
+
try {
|
|
1000
|
+
parsed = new URL(value);
|
|
1001
|
+
} catch {
|
|
1002
|
+
return `${name} is not a valid URL: ${value}`;
|
|
1003
|
+
}
|
|
1004
|
+
if (parsed.protocol === "https:") return null;
|
|
1005
|
+
if (parsed.protocol === "http:") {
|
|
1006
|
+
const host = parsed.hostname;
|
|
1007
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") return null;
|
|
1008
|
+
return `${name} must use https:// (got http:// to ${host} \u2014 refusing to ship JWT cleartext)`;
|
|
1009
|
+
}
|
|
1010
|
+
return `${name} must use https:// (got ${parsed.protocol})`;
|
|
1011
|
+
}
|
|
1012
|
+
function validateBrokerEndpointFlag(value, platformId) {
|
|
1013
|
+
if (!value || value.length === 0) {
|
|
1014
|
+
return "--broker-endpoint cannot be empty";
|
|
1015
|
+
}
|
|
1016
|
+
if (platformId === "win32") {
|
|
1017
|
+
if (value.startsWith("\\\\.\\pipe\\") || value.startsWith("//./pipe/")) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
return "--broker-endpoint on Windows must be a named pipe path (\\\\.\\pipe\\...)";
|
|
1021
|
+
}
|
|
1022
|
+
if (!value.startsWith("/")) {
|
|
1023
|
+
return "--broker-endpoint on POSIX must be an absolute path (e.g. /run/muhaven/broker.sock)";
|
|
1024
|
+
}
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1028
|
+
async function waitForBroker(options) {
|
|
1029
|
+
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
1030
|
+
const intervalMs = options.intervalMs ?? 200;
|
|
1031
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
1032
|
+
const now = options.now ?? Date.now;
|
|
1033
|
+
const deadline = now() + timeoutMs;
|
|
1034
|
+
let lastErr = null;
|
|
1035
|
+
while (now() < deadline) {
|
|
1036
|
+
try {
|
|
1037
|
+
const hello = await options.broker.hello();
|
|
1038
|
+
return { hasJwt: hello.hasJwt };
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
lastErr = err;
|
|
1041
|
+
if (now() + intervalMs < deadline) {
|
|
1042
|
+
await sleep(intervalMs);
|
|
1043
|
+
} else {
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`muhaven-broker daemon did not become reachable within ${timeoutMs}ms: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
function parseSetupFlags(argv) {
|
|
1053
|
+
let foreground = false;
|
|
1054
|
+
let noLaunchBrowser = false;
|
|
1055
|
+
let brokerEndpoint;
|
|
1056
|
+
let backendBaseUrl;
|
|
1057
|
+
let dashboardBaseUrl;
|
|
1058
|
+
let skipLogin = false;
|
|
1059
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1060
|
+
const a = argv[i];
|
|
1061
|
+
if (a === "--foreground" || a === "-f") foreground = true;
|
|
1062
|
+
else if (a === "--no-launch-browser") noLaunchBrowser = true;
|
|
1063
|
+
else if (a === "--skip-login") skipLogin = true;
|
|
1064
|
+
else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
|
|
1065
|
+
else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
|
|
1066
|
+
else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
|
|
1067
|
+
else throw new Error(`unknown flag: ${a}`);
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
foreground,
|
|
1071
|
+
noLaunchBrowser,
|
|
1072
|
+
brokerEndpoint,
|
|
1073
|
+
backendBaseUrl,
|
|
1074
|
+
dashboardBaseUrl,
|
|
1075
|
+
skipLogin
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async function runSetup(argv, deps) {
|
|
1079
|
+
let flags;
|
|
1080
|
+
try {
|
|
1081
|
+
flags = parseSetupFlags(argv);
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
deps.printErr(`error: ${err.message}`);
|
|
1084
|
+
deps.printErr(
|
|
1085
|
+
"usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]"
|
|
1086
|
+
);
|
|
1087
|
+
return 2;
|
|
1088
|
+
}
|
|
1089
|
+
if (flags.backendBaseUrl) {
|
|
1090
|
+
const err = validateHttpUrlFlag("--backend-base-url", flags.backendBaseUrl);
|
|
1091
|
+
if (err) {
|
|
1092
|
+
deps.printErr(`error: ${err}`);
|
|
1093
|
+
return 2;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (flags.dashboardBaseUrl) {
|
|
1097
|
+
const err = validateHttpUrlFlag("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
1098
|
+
if (err) {
|
|
1099
|
+
deps.printErr(`error: ${err}`);
|
|
1100
|
+
return 2;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (flags.brokerEndpoint) {
|
|
1104
|
+
const err = validateBrokerEndpointFlag(flags.brokerEndpoint, deps.platformId);
|
|
1105
|
+
if (err) {
|
|
1106
|
+
deps.printErr(`error: ${err}`);
|
|
1107
|
+
return 2;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const overrides = applyEnvDefaults({
|
|
1111
|
+
env: deps.env,
|
|
1112
|
+
platformId: deps.platformId,
|
|
1113
|
+
osRelease: deps.osRelease
|
|
1114
|
+
});
|
|
1115
|
+
const effectiveEnv = {};
|
|
1116
|
+
for (const [k, v] of Object.entries(deps.env)) {
|
|
1117
|
+
if (typeof v === "string") effectiveEnv[k] = v;
|
|
1118
|
+
}
|
|
1119
|
+
for (const [k, v] of Object.entries(overrides.toSet)) {
|
|
1120
|
+
effectiveEnv[k] = v;
|
|
1121
|
+
}
|
|
1122
|
+
if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
|
|
1123
|
+
if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
|
|
1124
|
+
if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
|
|
1125
|
+
for (const name of overrides.preserved) {
|
|
1126
|
+
deps.print(`Env preserved: ${name} (set in your shell)`);
|
|
1127
|
+
}
|
|
1128
|
+
for (const [k, v] of Object.entries(overrides.toSet)) {
|
|
1129
|
+
deps.print(`Env defaulted: ${k}=${v}`);
|
|
1130
|
+
}
|
|
1131
|
+
let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
|
|
1132
|
+
let mintedKey = false;
|
|
1133
|
+
if (!sessionKey || sessionKey === "") {
|
|
1134
|
+
sessionKey = deps.mintSessionKey();
|
|
1135
|
+
mintedKey = true;
|
|
1136
|
+
deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
|
|
1137
|
+
} else {
|
|
1138
|
+
deps.print("Session key: using MUHAVEN_BROKER_SESSION_KEY from env.");
|
|
1139
|
+
}
|
|
1140
|
+
effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
|
|
1141
|
+
if (flags.foreground) {
|
|
1142
|
+
deps.print("Foreground mode \u2014 running daemon attached to this shell. Ctrl-C to stop.");
|
|
1143
|
+
const restorationKeys = [
|
|
1144
|
+
...Object.keys(overrides.toSet),
|
|
1145
|
+
"MUHAVEN_BROKER_SESSION_KEY",
|
|
1146
|
+
...flags.brokerEndpoint ? ["MUHAVEN_BROKER_ENDPOINT"] : [],
|
|
1147
|
+
...flags.backendBaseUrl ? ["MUHAVEN_BACKEND_URL"] : [],
|
|
1148
|
+
...flags.dashboardBaseUrl ? ["MUHAVEN_DASHBOARD_URL"] : []
|
|
1149
|
+
];
|
|
1150
|
+
const originalValues = {};
|
|
1151
|
+
for (const k of restorationKeys) {
|
|
1152
|
+
originalValues[k] = process.env[k];
|
|
1153
|
+
process.env[k] = effectiveEnv[k];
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
await deps.runForegroundDaemon();
|
|
1157
|
+
} finally {
|
|
1158
|
+
for (const k of restorationKeys) {
|
|
1159
|
+
if (originalValues[k] === void 0) delete process.env[k];
|
|
1160
|
+
else process.env[k] = originalValues[k];
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return 0;
|
|
1164
|
+
}
|
|
1165
|
+
const config = loadMcpConfig(effectiveEnv);
|
|
1166
|
+
const broker = deps.newBrokerClient(config.brokerEndpoint, config.brokerTimeoutMs);
|
|
1167
|
+
let helloProbe = null;
|
|
1168
|
+
try {
|
|
1169
|
+
helloProbe = await broker.hello();
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
const action = decideSetupAction({ hello: helloProbe });
|
|
1173
|
+
let daemonPid = null;
|
|
1174
|
+
if (action === "spawn_and_login") {
|
|
1175
|
+
deps.print("Broker daemon: not running, starting one (detached) ...");
|
|
1176
|
+
daemonPid = deps.spawnDaemon({
|
|
1177
|
+
binPath: deps.resolveBinPath(),
|
|
1178
|
+
env: {
|
|
1179
|
+
// Explicit env for the spawned daemon. Includes every var that the
|
|
1180
|
+
// daemon's loadBrokerConfig will read, sourced from our resolved
|
|
1181
|
+
// effectiveEnv (NOT from process.env). spawnDaemon will sanitize
|
|
1182
|
+
// process.env-inherited values further (strips NODE_OPTIONS etc.).
|
|
1183
|
+
...overrides.toSet,
|
|
1184
|
+
MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
|
|
1185
|
+
MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
|
|
1186
|
+
MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
|
|
1187
|
+
MUHAVEN_BROKER_SESSION_KEY: sessionKey
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
try {
|
|
1191
|
+
const readyHello = await deps.waitForBroker({ broker });
|
|
1192
|
+
helloProbe = readyHello;
|
|
1193
|
+
deps.print(`Broker daemon: ready (PID ${daemonPid}, endpoint ${config.brokerEndpoint}).`);
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
deps.printErr(err.message);
|
|
1196
|
+
deps.printErr(
|
|
1197
|
+
" hint: re-run `muhaven-broker setup` after checking that no other broker is bound to the same endpoint."
|
|
1198
|
+
);
|
|
1199
|
+
return 1;
|
|
1200
|
+
}
|
|
1201
|
+
} else {
|
|
1202
|
+
deps.print(`Broker daemon: already reachable at ${config.brokerEndpoint}.`);
|
|
1203
|
+
}
|
|
1204
|
+
const needsLogin = !flags.skipLogin && !(helloProbe && helloProbe.hasJwt);
|
|
1205
|
+
if (flags.skipLogin) {
|
|
1206
|
+
deps.print("Login: skipped per --skip-login.");
|
|
1207
|
+
} else if (helloProbe && helloProbe.hasJwt) {
|
|
1208
|
+
deps.print("Login: skipped \u2014 JWT already in keystore.");
|
|
1209
|
+
}
|
|
1210
|
+
if (needsLogin) {
|
|
1211
|
+
const loginArgv = [];
|
|
1212
|
+
if (flags.noLaunchBrowser) loginArgv.push("--no-launch-browser");
|
|
1213
|
+
if (flags.brokerEndpoint) {
|
|
1214
|
+
loginArgv.push("--broker-endpoint", flags.brokerEndpoint);
|
|
1215
|
+
}
|
|
1216
|
+
if (flags.backendBaseUrl) {
|
|
1217
|
+
loginArgv.push("--backend-base-url", flags.backendBaseUrl);
|
|
1218
|
+
}
|
|
1219
|
+
if (flags.dashboardBaseUrl) {
|
|
1220
|
+
loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
1221
|
+
}
|
|
1222
|
+
const restorationKeys = ["MUHAVEN_BACKEND_URL", "MUHAVEN_DASHBOARD_URL", "MUHAVEN_BROKER_ENDPOINT"];
|
|
1223
|
+
const originalValues = {};
|
|
1224
|
+
for (const k of restorationKeys) {
|
|
1225
|
+
originalValues[k] = process.env[k];
|
|
1226
|
+
if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
|
|
1227
|
+
}
|
|
1228
|
+
let code;
|
|
1229
|
+
try {
|
|
1230
|
+
code = await deps.runLogin(loginArgv);
|
|
1231
|
+
} finally {
|
|
1232
|
+
for (const k of restorationKeys) {
|
|
1233
|
+
if (originalValues[k] === void 0) delete process.env[k];
|
|
1234
|
+
else process.env[k] = originalValues[k];
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (code !== 0) {
|
|
1238
|
+
deps.printErr(
|
|
1239
|
+
"Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
|
|
1240
|
+
);
|
|
1241
|
+
if (daemonPid !== null) {
|
|
1242
|
+
const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
|
|
1243
|
+
deps.printErr(` (daemon PID ${daemonPid}; stop with: ${killCmd})`);
|
|
1244
|
+
}
|
|
1245
|
+
return code;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
deps.print("");
|
|
1249
|
+
deps.print("================================");
|
|
1250
|
+
deps.print("Setup complete.");
|
|
1251
|
+
if (daemonPid !== null) {
|
|
1252
|
+
deps.print(` Daemon PID : ${daemonPid}`);
|
|
1253
|
+
const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
|
|
1254
|
+
deps.print(` Stop daemon: ${killCmd}`);
|
|
1255
|
+
} else {
|
|
1256
|
+
deps.print(" Daemon : already running");
|
|
1257
|
+
}
|
|
1258
|
+
deps.print(` Endpoint : ${config.brokerEndpoint}`);
|
|
1259
|
+
deps.print(" Sign out : muhaven-broker logout (clears JWT, leaves daemon running)");
|
|
1260
|
+
if (mintedKey) {
|
|
1261
|
+
deps.print(" Session key: ephemeral \u2014 minted by setup, lives only in the daemon process.");
|
|
1262
|
+
}
|
|
1263
|
+
deps.print("================================");
|
|
1264
|
+
return 0;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// src/broker/stop.ts
|
|
1268
|
+
async function runStop(deps) {
|
|
1269
|
+
const gracefulShutdownMs = deps.gracefulShutdownMs ?? 5e3;
|
|
1270
|
+
const pollIntervalMs = deps.pollIntervalMs ?? 200;
|
|
1271
|
+
const broker = deps.newBrokerClient(deps.endpoint, deps.brokerTimeoutMs);
|
|
1272
|
+
let hello;
|
|
1273
|
+
try {
|
|
1274
|
+
hello = await broker.hello();
|
|
1275
|
+
} catch {
|
|
1276
|
+
deps.print("Broker daemon: not running, nothing to stop.");
|
|
1277
|
+
return 0;
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
await broker.clearJwt();
|
|
1281
|
+
deps.print("JWT cleared from keystore.");
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
deps.print(
|
|
1284
|
+
`Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
const pid = hello.pid;
|
|
1288
|
+
if (pid === void 0) {
|
|
1289
|
+
deps.printErr(
|
|
1290
|
+
"Broker daemon did not advertise its PID (older than @muhaven/mcp@0.1.5)."
|
|
1291
|
+
);
|
|
1292
|
+
deps.printErr("Stop manually with:");
|
|
1293
|
+
deps.printErr(" POSIX: pkill -f muhaven-broker");
|
|
1294
|
+
deps.printErr(" Windows: Stop-Process -Name node -Force (filter to muhaven-broker)");
|
|
1295
|
+
return 1;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
deps.killProcess(pid, "SIGTERM");
|
|
1299
|
+
deps.print(`Sent SIGTERM to broker daemon (PID ${pid}). Waiting up to ${gracefulShutdownMs}ms for clean exit...`);
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
deps.printErr(
|
|
1302
|
+
`Failed to send SIGTERM to PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
|
|
1303
|
+
);
|
|
1304
|
+
return 1;
|
|
1305
|
+
}
|
|
1306
|
+
const maxAttempts = Math.ceil(gracefulShutdownMs / pollIntervalMs);
|
|
1307
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1308
|
+
await deps.sleep(pollIntervalMs);
|
|
1309
|
+
try {
|
|
1310
|
+
await broker.hello();
|
|
1311
|
+
} catch {
|
|
1312
|
+
deps.print("Broker daemon stopped cleanly.");
|
|
1313
|
+
return 0;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
deps.print(`Daemon did not exit after ${gracefulShutdownMs}ms \u2014 sending SIGKILL.`);
|
|
1317
|
+
try {
|
|
1318
|
+
deps.killProcess(pid, "SIGKILL");
|
|
1319
|
+
deps.print(`Broker daemon force-killed (PID ${pid}).`);
|
|
1320
|
+
return 0;
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
deps.printErr(
|
|
1323
|
+
`Failed to SIGKILL PID ${pid}: ${err instanceof Error ? err.message : String(err)}`
|
|
1324
|
+
);
|
|
1325
|
+
deps.printErr(
|
|
1326
|
+
" Daemon process may be orphaned. Inspect with `ps aux | grep muhaven-broker` and kill manually."
|
|
1327
|
+
);
|
|
1328
|
+
return 1;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function defaultKillProcess(pid, signal) {
|
|
1332
|
+
try {
|
|
1333
|
+
process.kill(pid, signal);
|
|
1334
|
+
return true;
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
if (err.code === "ESRCH") {
|
|
1337
|
+
return false;
|
|
1338
|
+
}
|
|
1339
|
+
throw err;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
935
1342
|
|
|
936
1343
|
// src/broker/cli.ts
|
|
937
1344
|
function print(line) {
|
|
@@ -941,7 +1348,7 @@ function printErr(line) {
|
|
|
941
1348
|
process.stderr.write(line + "\n");
|
|
942
1349
|
}
|
|
943
1350
|
function detectMcpHost() {
|
|
944
|
-
return process.env.MCP_HOST_NAME ?? process.env.CLAUDE_CODE_HOST ??
|
|
1351
|
+
return process.env.MCP_HOST_NAME ?? process.env.CLAUDE_CODE_HOST ?? "muhaven-broker-cli";
|
|
945
1352
|
}
|
|
946
1353
|
function detectEnvironment() {
|
|
947
1354
|
const warnings = [];
|
|
@@ -1195,11 +1602,59 @@ function printUsage() {
|
|
|
1195
1602
|
print("usage: muhaven-broker [<subcommand>] [options]");
|
|
1196
1603
|
print("");
|
|
1197
1604
|
print(" (no subcommand) Run the daemon (production mode)");
|
|
1605
|
+
print(" setup One-shot install: env defaults + session key + detached daemon + login");
|
|
1606
|
+
print(" [--foreground|-f] keeps the daemon attached (skip background spawn)");
|
|
1607
|
+
print(" [--skip-login] starts the daemon but lets you run login later");
|
|
1608
|
+
print(" [--no-launch-browser] pass-through to login");
|
|
1609
|
+
print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
|
|
1610
|
+
print(" after 5s). Also clears the keystore JWT as a best effort.");
|
|
1198
1611
|
print(" login Acquire a JWT via the device-code flow + store in keystore");
|
|
1199
1612
|
print(" [--from-daemon] resolves backend/dashboard URLs from the running daemon");
|
|
1200
|
-
print(" logout Clear the JWT from the keystore");
|
|
1613
|
+
print(" logout Clear the JWT from the keystore (does NOT stop the daemon)");
|
|
1201
1614
|
print(" doctor Print environment + keystore + reachability report");
|
|
1202
1615
|
print(" -h, --help Show this help");
|
|
1616
|
+
print(" -v, --version Print the @muhaven/mcp package version");
|
|
1617
|
+
}
|
|
1618
|
+
function getBrokerPackageVersion() {
|
|
1619
|
+
{
|
|
1620
|
+
return "0.1.5";
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function printVersion() {
|
|
1624
|
+
print(`muhaven-broker @muhaven/mcp@${getBrokerPackageVersion()}`);
|
|
1625
|
+
}
|
|
1626
|
+
function resolveBrokerBinPath() {
|
|
1627
|
+
return resolve(__dirname$1, "..", "bin", "muhaven-broker.cjs");
|
|
1628
|
+
}
|
|
1629
|
+
async function runSetup2(argv) {
|
|
1630
|
+
const deps = {
|
|
1631
|
+
print,
|
|
1632
|
+
printErr,
|
|
1633
|
+
mintSessionKey,
|
|
1634
|
+
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
1635
|
+
spawnDaemon,
|
|
1636
|
+
waitForBroker,
|
|
1637
|
+
runLogin,
|
|
1638
|
+
runForegroundDaemon: runBrokerDaemonCli,
|
|
1639
|
+
resolveBinPath: resolveBrokerBinPath,
|
|
1640
|
+
env: process.env,
|
|
1641
|
+
platformId: process.platform,
|
|
1642
|
+
osRelease: release()
|
|
1643
|
+
};
|
|
1644
|
+
return runSetup(argv, deps);
|
|
1645
|
+
}
|
|
1646
|
+
async function runStop2() {
|
|
1647
|
+
const config = loadMcpConfig();
|
|
1648
|
+
const deps = {
|
|
1649
|
+
print,
|
|
1650
|
+
printErr,
|
|
1651
|
+
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
1652
|
+
killProcess: defaultKillProcess,
|
|
1653
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
1654
|
+
endpoint: config.brokerEndpoint,
|
|
1655
|
+
brokerTimeoutMs: config.brokerTimeoutMs
|
|
1656
|
+
};
|
|
1657
|
+
return runStop(deps);
|
|
1203
1658
|
}
|
|
1204
1659
|
async function runCli(argv) {
|
|
1205
1660
|
const [sub, ...rest] = argv;
|
|
@@ -1207,6 +1662,10 @@ async function runCli(argv) {
|
|
|
1207
1662
|
case void 0:
|
|
1208
1663
|
await runBrokerDaemonCli();
|
|
1209
1664
|
return 0;
|
|
1665
|
+
case "setup":
|
|
1666
|
+
return runSetup2(rest);
|
|
1667
|
+
case "stop":
|
|
1668
|
+
return runStop2();
|
|
1210
1669
|
case "login":
|
|
1211
1670
|
return runLogin(rest);
|
|
1212
1671
|
case "logout":
|
|
@@ -1217,6 +1676,10 @@ async function runCli(argv) {
|
|
|
1217
1676
|
case "--help":
|
|
1218
1677
|
printUsage();
|
|
1219
1678
|
return 0;
|
|
1679
|
+
case "-v":
|
|
1680
|
+
case "--version":
|
|
1681
|
+
printVersion();
|
|
1682
|
+
return 0;
|
|
1220
1683
|
default:
|
|
1221
1684
|
printErr(`unknown subcommand: ${sub}`);
|
|
1222
1685
|
printUsage();
|
|
@@ -1224,4 +1687,4 @@ async function runCli(argv) {
|
|
|
1224
1687
|
}
|
|
1225
1688
|
}
|
|
1226
1689
|
|
|
1227
|
-
export { parseLoginFlags, runCli, runDoctor, runLogin, runLogout };
|
|
1690
|
+
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.
|
|
1213
|
+
return "0.1.5";
|
|
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.
|
|
1209
|
+
return "0.1.5";
|
|
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));
|