@hirey/hi-mcp-server 0.1.9 → 0.1.11

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/README.md CHANGED
@@ -91,9 +91,10 @@ npm install -g @hirey/hi-mcp-server
91
91
  1. 用宿主把 `hi-mcp-server` 作为 `stdio` MCP server 挂上
92
92
  2. 优先执行 `hi_agent_install`,而不是让用户手工走 `register -> connect -> activate`
93
93
  3. `hi_agent_install` 会自动完成 register / activate、delivery capability 声明、默认订阅,以及在 OpenClaw 场景下的 receiver 配置
94
- 4. `agent_id` 可以省略;省略时由 gateway 正式生成 canonical `ag_...`,不需要用户手写
95
- 5. 完成后执行 `hi_agent_doctor`
96
- 6. 如果要 clean reinstall,先执行 `hi_agent_reset`
94
+ 4. 当 receiver 的物料配置(例如 hooks token、adapter URL、transport、default reply route)发生变化时,`hi_agent_install` 会先停止当前 profile 记录的 receiver,再写入新配置并按需要重新启动;如果配置没变,则保留现有 runtime cursor,不重置消费进度
95
+ 5. `agent_id` 可以省略;省略时由 gateway 正式生成 canonical `ag_...`,不需要用户手写
96
+ 6. 完成后执行 `hi_agent_doctor`
97
+ 7. 如果要 clean reinstall,先执行 `hi_agent_reset`;`hi_agent_reset` 现在会等待当前 profile 记录的 receiver 真实退出,再删除本地 state / receiver config,避免立刻重装时旧进程把旧配置写回去
97
98
 
98
99
  如果宿主不能直接执行裸命令 `hi-agent-receiver`,而是需要 `node /path/to/dist/server.js`、`npx ...` 或其它显式 argv 前缀,`hi_agent_install` 现在也支持把 receiver 启动入口写成 `receiver_command_argv` 数组;`hi-mcp-server` 会在这组 argv 后自动追加 `run --config <path>`,不再把整段命令误当成单个可执行文件。
99
100
 
@@ -0,0 +1,15 @@
1
+ export type DefaultReplyDeliveryContext = {
2
+ channel: string | null;
3
+ to: string | null;
4
+ account_id: string | null;
5
+ thread_id: string | null;
6
+ };
7
+ export declare function resolveInstallDefaultReplyDeliveryContext(args: {
8
+ hostKind: 'openclaw' | 'generic';
9
+ hasSessionKey: boolean;
10
+ defaultReplyChannel?: unknown;
11
+ defaultReplyTo?: unknown;
12
+ defaultReplyAccountId?: unknown;
13
+ defaultReplyThreadId?: unknown;
14
+ }): DefaultReplyDeliveryContext | null;
15
+ //# sourceMappingURL=defaultReplyRoute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultReplyRoute.d.ts","sourceRoot":"","sources":["../src/defaultReplyRoute.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,2BAA2B,GAAG;IACxC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,wBAAgB,yCAAyC,CAAC,IAAI,EAAE;IAC9D,QAAQ,EAAE,UAAU,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,GAAG,2BAA2B,GAAG,IAAI,CAyBrC"}
@@ -0,0 +1,28 @@
1
+ function normalizeText(input) {
2
+ return String(input || '').trim();
3
+ }
4
+ export function resolveInstallDefaultReplyDeliveryContext(args) {
5
+ const explicit = {
6
+ channel: normalizeText(args.defaultReplyChannel) || null,
7
+ to: normalizeText(args.defaultReplyTo) || null,
8
+ account_id: normalizeText(args.defaultReplyAccountId) || null,
9
+ thread_id: normalizeText(args.defaultReplyThreadId) || null,
10
+ };
11
+ const hasExplicit = !!(explicit.channel
12
+ || explicit.to
13
+ || explicit.account_id
14
+ || explicit.thread_id);
15
+ if (hasExplicit)
16
+ return explicit;
17
+ // OpenClaw hooks continuation accepts `last|imessage`; when we already have the
18
+ // canonical session key, `last` is the canonical "same chat" lane.
19
+ if (args.hostKind === 'openclaw' && args.hasSessionKey) {
20
+ return {
21
+ channel: 'last',
22
+ to: null,
23
+ account_id: null,
24
+ thread_id: null,
25
+ };
26
+ }
27
+ return null;
28
+ }
@@ -0,0 +1,4 @@
1
+ export declare function receiverConfigMaterialSnapshot(raw: unknown): Record<string, unknown> | null;
2
+ export declare function receiverConfigMaterialEquals(left: unknown, right: unknown): boolean;
3
+ export declare function applyReceiverRuntimeSnapshot(nextConfig: Record<string, unknown>, existingConfig: unknown): Record<string, unknown>;
4
+ //# sourceMappingURL=receiver-config-material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receiver-config-material.d.ts","sourceRoot":"","sources":["../src/receiver-config-material.ts"],"names":[],"mappings":"AAcA,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAQ3F;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAEnF;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,cAAc,EAAE,OAAO,GACtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzB"}
@@ -0,0 +1,47 @@
1
+ function isPlainObject(value) {
2
+ return !!value && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+ function canonicalizeJson(value) {
5
+ if (Array.isArray(value))
6
+ return value.map((entry) => canonicalizeJson(entry));
7
+ if (!isPlainObject(value))
8
+ return value;
9
+ const out = {};
10
+ for (const key of Object.keys(value).sort()) {
11
+ out[key] = canonicalizeJson(value[key]);
12
+ }
13
+ return out;
14
+ }
15
+ export function receiverConfigMaterialSnapshot(raw) {
16
+ if (!isPlainObject(raw))
17
+ return null;
18
+ const snapshot = JSON.parse(JSON.stringify(raw));
19
+ delete snapshot.runtime;
20
+ delete snapshot._generated_at;
21
+ delete snapshot._generated_by;
22
+ delete snapshot._receiver_config_path;
23
+ return canonicalizeJson(snapshot);
24
+ }
25
+ export function receiverConfigMaterialEquals(left, right) {
26
+ return JSON.stringify(receiverConfigMaterialSnapshot(left)) === JSON.stringify(receiverConfigMaterialSnapshot(right));
27
+ }
28
+ export function applyReceiverRuntimeSnapshot(nextConfig, existingConfig) {
29
+ if (!isPlainObject(existingConfig) || !isPlainObject(existingConfig.runtime))
30
+ return nextConfig;
31
+ const runtime = existingConfig.runtime;
32
+ return {
33
+ ...nextConfig,
34
+ // 安装修复时保留已消费 cursor,避免仅因重写物料配置就把 receiver 从头回放。
35
+ runtime: {
36
+ last_consumed_stream_seq: Number.isFinite(Number(runtime.last_consumed_stream_seq))
37
+ ? Math.max(0, Number(runtime.last_consumed_stream_seq))
38
+ : 0,
39
+ last_claim_lease_id: typeof runtime.last_claim_lease_id === 'string'
40
+ ? runtime.last_claim_lease_id
41
+ : null,
42
+ updated_at: typeof runtime.updated_at === 'string'
43
+ ? runtime.updated_at
44
+ : null,
45
+ },
46
+ };
47
+ }
package/dist/server.js CHANGED
@@ -14,7 +14,11 @@ import { createHiAgentClients, exchangeHiAgentClientCredentialsToken, HiAgentGat
14
14
  import { readState, resolveCanonicalOpenClawStateDir, resolveDefaultStateDir, resolveLegacyStateFiles, resolveStateFile, updateState, normalizeStateProfile, } from './state.js';
15
15
  import { looksLikeOpenClawSessionKey, validateOpenClawSessionKey, } from './openclaw-session-key.js';
16
16
  import { buildInstallReceiverCommandArgv, } from './receiver-command.js';
17
+ import { applyReceiverRuntimeSnapshot, receiverConfigMaterialEquals, } from './receiver-config-material.js';
18
+ import { resolveInstallDefaultReplyDeliveryContext, } from './defaultReplyRoute.js';
17
19
  const CAPABILITY_CACHE_TTL_MS = 30_000;
20
+ const RECEIVER_STOP_TIMEOUT_MS = 3_000;
21
+ const RECEIVER_STOP_POLL_MS = 100;
18
22
  const resolvedProfile = normalizeStateProfile(process.env.HI_MCP_PROFILE);
19
23
  const config = {
20
24
  host: normalizeText(process.env.HI_MCP_HOST) || '127.0.0.1',
@@ -622,16 +626,15 @@ function buildDefaultReplyRoute(args, options = {}) {
622
626
  const sessionKey = normalizeReplyRouteSessionKey(args.host_session_key, {
623
627
  requireOpenClawSessionKey: options.requireOpenClawSessionKey,
624
628
  });
625
- const deliveryContext = {
626
- channel: normalizeText(args.default_reply_channel) || null,
627
- to: normalizeText(args.default_reply_to) || null,
628
- account_id: normalizeText(args.default_reply_account_id) || null,
629
- thread_id: normalizeText(args.default_reply_thread_id) || null,
630
- };
631
- const hasDeliveryContext = !!(deliveryContext.channel
632
- || deliveryContext.to
633
- || deliveryContext.account_id
634
- || deliveryContext.thread_id);
629
+ const deliveryContext = resolveInstallDefaultReplyDeliveryContext({
630
+ hostKind: options.hostKind === 'openclaw' ? 'openclaw' : 'generic',
631
+ hasSessionKey: !!sessionKey,
632
+ defaultReplyChannel: args.default_reply_channel,
633
+ defaultReplyTo: args.default_reply_to,
634
+ defaultReplyAccountId: args.default_reply_account_id,
635
+ defaultReplyThreadId: args.default_reply_thread_id,
636
+ });
637
+ const hasDeliveryContext = !!deliveryContext;
635
638
  if (!sessionKey && !hasDeliveryContext)
636
639
  return null;
637
640
  return {
@@ -724,6 +727,58 @@ function isProcessAlive(pid) {
724
727
  return false;
725
728
  }
726
729
  }
730
+ async function waitForProcessExit(pid, timeoutMs = RECEIVER_STOP_TIMEOUT_MS) {
731
+ const deadline = Date.now() + timeoutMs;
732
+ while (Date.now() < deadline) {
733
+ if (!isProcessAlive(pid))
734
+ return true;
735
+ await sleep(RECEIVER_STOP_POLL_MS);
736
+ }
737
+ return !isProcessAlive(pid);
738
+ }
739
+ async function stopTrackedReceiverProcess(pidRaw) {
740
+ const pid = Number(pidRaw || 0);
741
+ if (!Number.isInteger(pid) || pid <= 0) {
742
+ return {
743
+ attempted: false,
744
+ pid: null,
745
+ signal: null,
746
+ exited: true,
747
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
748
+ };
749
+ }
750
+ if (!isProcessAlive(pid)) {
751
+ return {
752
+ attempted: true,
753
+ pid,
754
+ signal: null,
755
+ exited: true,
756
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
757
+ };
758
+ }
759
+ try {
760
+ process.kill(pid, 'SIGTERM');
761
+ }
762
+ catch (error) {
763
+ if (error?.code === 'ESRCH') {
764
+ return {
765
+ attempted: true,
766
+ pid,
767
+ signal: 'SIGTERM',
768
+ exited: true,
769
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
770
+ };
771
+ }
772
+ throw error;
773
+ }
774
+ return {
775
+ attempted: true,
776
+ pid,
777
+ signal: 'SIGTERM',
778
+ exited: await waitForProcessExit(pid),
779
+ timeout_ms: RECEIVER_STOP_TIMEOUT_MS,
780
+ };
781
+ }
727
782
  async function startDetachedReceiver(args) {
728
783
  const [command, ...prefixArgs] = args.receiverCommandArgv;
729
784
  if (!command)
@@ -1204,6 +1259,7 @@ async function handleInstall(args) {
1204
1259
  const defaultReplyRoute = buildDefaultReplyRoute(args, {
1205
1260
  installationId: state.identity?.installation_id || null,
1206
1261
  requireOpenClawSessionKey: hostKind === 'openclaw',
1262
+ hostKind,
1207
1263
  });
1208
1264
  const routeMissingPolicy = normalizeText(args.route_missing_policy)
1209
1265
  || (defaultReplyRoute ? 'use_explicit_default_route' : '');
@@ -1241,7 +1297,8 @@ async function handleInstall(args) {
1241
1297
  return fail('missing_host_adapter_url');
1242
1298
  }
1243
1299
  const receiverConfigPath = resolveReceiverConfigPath(await loadPersistedState());
1244
- const receiverConfig = buildReceiverConfig({
1300
+ let existingReceiverConfig = await readReceiverConfigSnapshot(receiverConfigPath);
1301
+ const desiredReceiverConfig = buildReceiverConfig({
1245
1302
  receiverConfigPath,
1246
1303
  hostKind,
1247
1304
  receiverTransport,
@@ -1251,16 +1308,37 @@ async function handleInstall(args) {
1251
1308
  openresponsesModel: normalizeText(args.openresponses_model) || undefined,
1252
1309
  defaultReplyRoute,
1253
1310
  });
1254
- await writeReceiverConfigFile(receiverConfigPath, receiverConfig);
1311
+ const receiverConfigChanged = !receiverConfigMaterialEquals(existingReceiverConfig, desiredReceiverConfig);
1312
+ let receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
1313
+ let receiverStop = null;
1314
+ if (receiverConfigChanged && isProcessAlive(receiverPid)) {
1315
+ // 先停 tracked receiver,避免旧进程继续拿旧 token 投递,并把旧物料配置再写回磁盘。
1316
+ receiverStop = await stopTrackedReceiverProcess(receiverPid);
1317
+ if (!normalizeBooleanFlag(receiverStop?.exited)) {
1318
+ return fail('receiver_stop_timeout', {
1319
+ receiver_config_path: receiverConfigPath,
1320
+ receiver_pid: receiverPid,
1321
+ receiver_stop: receiverStop,
1322
+ });
1323
+ }
1324
+ receiverPid = null;
1325
+ // 停掉 tracked receiver 后再读一次磁盘,尽量保留它退出前最后一次写下的 runtime cursor。
1326
+ existingReceiverConfig = await readReceiverConfigSnapshot(receiverConfigPath);
1327
+ }
1328
+ if (receiverConfigChanged) {
1329
+ const receiverConfig = applyReceiverRuntimeSnapshot(desiredReceiverConfig, existingReceiverConfig);
1330
+ await writeReceiverConfigFile(receiverConfigPath, receiverConfig);
1331
+ }
1255
1332
  state = await persistState((current) => ({
1256
1333
  ...current,
1257
1334
  runtime: buildInstallRuntimeState(current.runtime, {
1258
1335
  host_kind: hostKind,
1259
1336
  receiver_config_path: receiverConfigPath,
1337
+ receiver_pid: isProcessAlive(receiverPid) ? receiverPid : null,
1260
1338
  receiver_last_error: null,
1261
1339
  }),
1262
1340
  }));
1263
- let receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
1341
+ receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
1264
1342
  if (receiverShouldStart && !isProcessAlive(receiverPid)) {
1265
1343
  try {
1266
1344
  receiverPid = await startDetachedReceiver({
@@ -1296,12 +1374,14 @@ async function handleInstall(args) {
1296
1374
  }
1297
1375
  receiverPayload = {
1298
1376
  config_path: receiverConfigPath,
1377
+ config_changed: receiverConfigChanged,
1299
1378
  started: receiverShouldStart,
1300
1379
  receiver_pid: receiverPid || null,
1301
1380
  receiver_command_argv: receiverCommandArgv,
1302
1381
  transport: receiverTransport,
1303
1382
  host_adapter_kind: hostAdapterKind,
1304
1383
  host_adapter_url: hostAdapterUrl,
1384
+ ...(receiverStop ? { receiver_stop: receiverStop } : {}),
1305
1385
  };
1306
1386
  }
1307
1387
  if (receiverShouldStart) {
@@ -1334,22 +1414,21 @@ async function handleReset(args) {
1334
1414
  const removeReceiverConfig = args.remove_receiver_config !== false;
1335
1415
  const clearState = args.clear_state !== false;
1336
1416
  let stopResult = null;
1337
- if (stopReceiver && isProcessAlive(installState.receiver_pid)) {
1417
+ if (stopReceiver && Number.isInteger(installState.receiver_pid) && Number(installState.receiver_pid) > 0) {
1338
1418
  try {
1339
- process.kill(Number(installState.receiver_pid), 'SIGTERM');
1340
- stopResult = {
1341
- attempted: true,
1342
- pid: installState.receiver_pid,
1343
- signalled: true,
1344
- };
1419
+ stopResult = await stopTrackedReceiverProcess(installState.receiver_pid);
1345
1420
  }
1346
1421
  catch (error) {
1347
- stopResult = {
1348
- attempted: true,
1422
+ return fail('receiver_stop_failed', {
1349
1423
  pid: installState.receiver_pid,
1350
- signalled: false,
1351
1424
  error: String(error?.message || error || 'receiver_stop_failed'),
1352
- };
1425
+ });
1426
+ }
1427
+ if (!normalizeBooleanFlag(stopResult?.exited)) {
1428
+ return fail('receiver_stop_timeout', {
1429
+ pid: installState.receiver_pid,
1430
+ receiver_stop: stopResult,
1431
+ });
1353
1432
  }
1354
1433
  }
1355
1434
  const receiverConfigPath = installState.receiver_config_path || resolveReceiverConfigPath(state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirey/hi-mcp-server",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/server.js",