@hirey/hi-mcp-server 0.1.2 → 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/dist/server.js CHANGED
@@ -1,19 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
1
4
  import { createServer } from 'node:http';
5
+ import path from 'node:path';
2
6
  import process from 'node:process';
3
7
  import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
4
8
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
10
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
11
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
- import { normalizeAgentEndpointList, normalizeAgentInstallationDeliveryDeclaration, normalizeAgentSubscriptionList, normalizeText, } from '@hirey/hi-agent-contracts';
12
+ import { AGENT_GATEWAY_EVENT_TOPICS, normalizeAgentEndpointList, normalizeAgentInstallationDeliveryDeclaration, normalizeAgentSubscriptionList, normalizeText, } from '@hirey/hi-agent-contracts';
9
13
  import { createHiAgentClients, exchangeHiAgentClientCredentialsToken, HiAgentGatewayClient, HiAgentPlatformClient, } from '@hirey/hi-agent-sdk';
10
- import { readState, updateState, } from './state.js';
14
+ import { readState, resolveCanonicalOpenClawStateDir, resolveDefaultStateDir, resolveLegacyStateFiles, resolveStateFile, updateState, normalizeStateProfile, } from './state.js';
11
15
  const CAPABILITY_CACHE_TTL_MS = 30_000;
16
+ const resolvedProfile = normalizeStateProfile(process.env.HI_MCP_PROFILE);
12
17
  const config = {
13
18
  host: normalizeText(process.env.HI_MCP_HOST) || '127.0.0.1',
14
19
  port: Number(process.env.HI_MCP_PORT || 8788),
15
- profile: normalizeText(process.env.HI_MCP_PROFILE) || 'default',
16
- stateDir: normalizeText(process.env.HI_MCP_STATE_DIR) || `${process.cwd()}/.hi-agent-state`,
20
+ profile: resolvedProfile,
21
+ stateDir: normalizeText(process.env.HI_MCP_STATE_DIR) || resolveDefaultStateDir(resolvedProfile),
17
22
  platformBaseUrl: normalizeText(process.env.HI_PLATFORM_BASE_URL),
18
23
  transport: normalizeText(process.env.HI_MCP_TRANSPORT).toLowerCase() === 'stdio' ? 'stdio' : 'http',
19
24
  };
@@ -58,6 +63,13 @@ function fail(message, detail) {
58
63
  function sleep(ms) {
59
64
  return new Promise((resolve) => setTimeout(resolve, ms));
60
65
  }
66
+ function normalizeCommandArgv(input) {
67
+ if (!Array.isArray(input))
68
+ return [];
69
+ return input
70
+ .map((value) => normalizeText(value))
71
+ .filter(Boolean);
72
+ }
61
73
  function controlTools() {
62
74
  // control tools 只映射 gateway/onboarding/runtime 管理面,不伪装成第二套业务 API。
63
75
  return [
@@ -71,15 +83,68 @@ function controlTools() {
71
83
  },
72
84
  },
73
85
  },
86
+ {
87
+ name: 'hi_agent_install',
88
+ description: '普通用户安装第二阶段:在 hi-mcp-server 已经挂进宿主后,自动完成 register/activate、delivery 声明、默认订阅、receiver 配置与可选启动。',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ display_name: { type: 'string', description: '首次安装且当前 profile 还没有 identity 时使用的人类可读名称。' },
93
+ host_kind: { type: 'string', description: "可选:'openclaw'|'generic'。默认 generic。" },
94
+ agent_kind: { type: 'string', description: '首次 register 时可选的 agent_kind。默认 external。' },
95
+ replace_existing_state: { type: 'boolean', description: '首次 register 且本地已有 state 时,是否允许覆盖本地持久化身份。' },
96
+ migrate_legacy_state: { type: 'boolean', description: '默认 true。若 canonical state 为空,则尝试一次已知 legacy state 迁移。' },
97
+ enable_local_receiver: { type: 'boolean', description: '是否配置 local_receiver 作为正式事件主路径。OpenClaw 默认 true,其它宿主默认 false。' },
98
+ receiver_transport: { type: 'string', description: "enable_local_receiver=true 时可选:'claim'|'stream'。默认 claim。" },
99
+ receiver_start: { type: 'boolean', description: 'enable_local_receiver=true 时,是否尝试后台启动 hi-agent-receiver。默认 true。' },
100
+ receiver_command: { type: 'string', description: '可选:receiver CLI 命令名。默认 hi-agent-receiver。' },
101
+ receiver_command_argv: {
102
+ type: 'array',
103
+ description: '可选:receiver CLI 启动 argv 前缀。hi-mcp-server 会在这组 argv 后自动追加 `run --config <path>`;适合 node/npx/绝对路径场景。',
104
+ items: { type: 'string' },
105
+ },
106
+ host_adapter_kind: { type: 'string', description: "enable_local_receiver=true 时可选:'openclaw_hooks'|'openresponses'。OpenClaw 默认 openclaw_hooks。" },
107
+ host_adapter_url: { type: 'string', description: 'enable_local_receiver=true 时可选:宿主本地接收入口。OpenClaw 默认 http://127.0.0.1:18789/hooks/agent。' },
108
+ host_adapter_bearer_token: { type: 'string', description: 'host_adapter_kind=openclaw_hooks 时必填:本地 hooks bearer token。' },
109
+ openresponses_model: { type: 'string', description: 'host_adapter_kind=openresponses 时必填:receiver 发给本地入口使用的 model。' },
110
+ subscribe_default_topics: { type: 'boolean', description: '是否自动补齐 Hi 官方默认事件订阅。默认 true。' },
111
+ run_doctor: { type: 'boolean', description: '默认 true。安装完成后自动跑一次 hi_agent_doctor。' },
112
+ },
113
+ },
114
+ },
115
+ {
116
+ name: 'hi_agent_doctor',
117
+ description: '一次性检查当前安装是否已连接、已激活、事件主路径是否清晰、receiver 配置是否齐全,并在 local_receiver 场景下做真实 delivery 探测。',
118
+ inputSchema: {
119
+ type: 'object',
120
+ properties: {
121
+ include_remote: { type: 'boolean', description: '默认 true。true 时读取远端 me/installation/endpoints/subscriptions。' },
122
+ probe_delivery: { type: 'boolean', description: '默认 true。当前事件主路径是 local_receiver 时,触发一次 synthetic test-delivery。' },
123
+ },
124
+ },
125
+ },
126
+ {
127
+ name: 'hi_agent_reset',
128
+ description: '清理当前 profile 的本地 Hi 安装状态;可选删除 receiver config,并尝试停止由当前 profile 记录过的 receiver 进程。',
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ clear_state: { type: 'boolean', description: '默认 true。是否删除当前 profile 的 hi-mcp persisted state。' },
133
+ remove_receiver_config: { type: 'boolean', description: '默认 true。是否删除当前 profile 记录过的 receiver config。' },
134
+ stop_receiver: { type: 'boolean', description: '默认 true。若当前 profile 记录过 receiver pid,是否尝试发 SIGTERM。' },
135
+ },
136
+ },
137
+ },
74
138
  {
75
139
  name: 'hi_agent_register',
76
140
  description: '按 Hi 官方 register -> token -> activate 主线创建一个新的 external agent installation,并把长期凭证持久化到本地 state。',
77
141
  inputSchema: {
78
142
  type: 'object',
79
143
  properties: {
80
- agent_id: { type: 'string' },
144
+ agent_id: { type: 'string', description: '可选;省略时由 gateway 正式生成 canonical ag_ id,显式传入时必须符合 ag_ 前缀规则。' },
81
145
  display_name: { type: 'string' },
82
146
  agent_kind: { type: 'string' },
147
+ status: { type: 'string' },
83
148
  capabilities: { type: 'object' },
84
149
  metadata: { type: 'object' },
85
150
  delivery_capabilities: { type: 'object' },
@@ -257,6 +322,14 @@ function isPlainObject(input) {
257
322
  function normalizeRecord(input) {
258
323
  return isPlainObject(input) ? input : {};
259
324
  }
325
+ function normalizeBooleanFlag(value, fallback = false) {
326
+ if (typeof value === 'boolean')
327
+ return value;
328
+ const normalized = normalizeText(value).toLowerCase();
329
+ if (!normalized)
330
+ return fallback;
331
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
332
+ }
260
333
  async function loadWellKnown() {
261
334
  const platform = new HiAgentPlatformClient({ baseUrl: config.platformBaseUrl });
262
335
  return await platform.wellKnown();
@@ -319,6 +392,227 @@ async function createAuthorizedClients() {
319
392
  ...clients,
320
393
  };
321
394
  }
395
+ function resolveCurrentStateFile() {
396
+ return resolveStateFile({
397
+ stateDir: config.stateDir,
398
+ profile: config.profile,
399
+ });
400
+ }
401
+ function normalizeHostKind(raw) {
402
+ return normalizeText(raw).toLowerCase() === 'openclaw' ? 'openclaw' : 'generic';
403
+ }
404
+ function normalizeReceiverTransport(raw) {
405
+ return normalizeText(raw).toLowerCase() === 'stream' ? 'stream' : 'claim';
406
+ }
407
+ function stateInstallSnapshot(runtime) {
408
+ return runtime.install || {
409
+ host_kind: null,
410
+ receiver_config_path: null,
411
+ receiver_pid: null,
412
+ receiver_last_started_at: null,
413
+ receiver_last_error: null,
414
+ };
415
+ }
416
+ function buildInstallRuntimeState(current, patch) {
417
+ return {
418
+ ...current,
419
+ install: {
420
+ ...stateInstallSnapshot(current),
421
+ ...patch,
422
+ },
423
+ updated_at: new Date().toISOString(),
424
+ };
425
+ }
426
+ async function maybeMigrateLegacyState() {
427
+ const targetStateFile = resolveCurrentStateFile();
428
+ try {
429
+ await fs.access(targetStateFile);
430
+ return { migrated: false, from: null, to: targetStateFile };
431
+ }
432
+ catch { }
433
+ for (const candidate of resolveLegacyStateFiles(config.profile)) {
434
+ if (!candidate || candidate === targetStateFile)
435
+ continue;
436
+ try {
437
+ const raw = await fs.readFile(candidate, 'utf8');
438
+ const parsed = JSON.parse(raw);
439
+ if (!parsed.identity)
440
+ continue;
441
+ await fs.mkdir(path.dirname(targetStateFile), { recursive: true });
442
+ await fs.writeFile(targetStateFile, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
443
+ return {
444
+ migrated: true,
445
+ from: candidate,
446
+ to: targetStateFile,
447
+ };
448
+ }
449
+ catch (error) {
450
+ if (error?.code === 'ENOENT')
451
+ continue;
452
+ throw error;
453
+ }
454
+ }
455
+ return { migrated: false, from: null, to: targetStateFile };
456
+ }
457
+ function buildDefaultSubscriptions() {
458
+ return AGENT_GATEWAY_EVENT_TOPICS.map((topic) => ({
459
+ topic,
460
+ status: 'active',
461
+ filter: null,
462
+ }));
463
+ }
464
+ function buildInstallationDeliveryDeclaration(args) {
465
+ const capabilities = [];
466
+ if (args.enableLocalReceiver) {
467
+ capabilities.push({
468
+ kind: 'local_receiver',
469
+ status: 'active',
470
+ config: { transport: args.receiverTransport },
471
+ });
472
+ }
473
+ capabilities.push({
474
+ kind: 'claim_ack',
475
+ status: 'active',
476
+ config: { transport: 'claim' },
477
+ });
478
+ if (args.enableLocalReceiver && args.receiverTransport === 'stream') {
479
+ capabilities.push({
480
+ kind: 'pull_stream',
481
+ status: 'active',
482
+ config: { transport: 'sse' },
483
+ });
484
+ }
485
+ return {
486
+ capabilities,
487
+ preferred: args.enableLocalReceiver ? 'local_receiver' : 'claim_ack',
488
+ };
489
+ }
490
+ function hasActiveInstallationCapability(declaration, kind) {
491
+ return !!declaration?.capabilities?.some((entry) => entry.kind === kind && entry.status !== 'disabled');
492
+ }
493
+ function resolveReceiverConfigPath(state) {
494
+ const existing = normalizeText(stateInstallSnapshot(state.runtime).receiver_config_path);
495
+ if (existing)
496
+ return existing;
497
+ return path.join(config.stateDir, 'hi-agent-receiver.json');
498
+ }
499
+ function buildReceiverConfig(args) {
500
+ if (args.hostAdapterKind === 'openclaw_hooks' && !normalizeText(args.hostAdapterBearerToken)) {
501
+ throw new Error('missing_host_adapter_bearer_token');
502
+ }
503
+ if (args.hostAdapterKind === 'openresponses' && !normalizeText(args.openresponsesModel)) {
504
+ throw new Error('missing_openresponses_model');
505
+ }
506
+ return {
507
+ profile: config.profile,
508
+ platform_base_url: config.platformBaseUrl,
509
+ identity_source: {
510
+ kind: 'hi_mcp_profile',
511
+ profile: config.profile,
512
+ state_dir: config.stateDir,
513
+ },
514
+ runtime: {
515
+ last_consumed_stream_seq: 0,
516
+ last_claim_lease_id: null,
517
+ updated_at: null,
518
+ },
519
+ event_source: {
520
+ transport: args.receiverTransport,
521
+ claim_limit: 20,
522
+ claim_poll_interval_ms: 1500,
523
+ stream_reconnect_delay_ms: 2000,
524
+ ack_retry_after_ms: 30000,
525
+ },
526
+ host_adapter: args.hostAdapterKind === 'openresponses'
527
+ ? {
528
+ kind: 'openresponses',
529
+ url: args.hostAdapterUrl,
530
+ auth: null,
531
+ config: {
532
+ model: normalizeText(args.openresponsesModel),
533
+ },
534
+ }
535
+ : {
536
+ kind: 'openclaw_hooks',
537
+ url: args.hostAdapterUrl,
538
+ auth: {
539
+ type: 'bearer',
540
+ token: normalizeText(args.hostAdapterBearerToken),
541
+ },
542
+ // OpenClaw 本地消息的展示前缀需要稳定,避免安装后又冒出第二套文案。
543
+ config: {
544
+ name: args.hostKind === 'openclaw' ? 'Hirey Hi Agent Platform' : 'Hirey Hi',
545
+ agent_id: 'main',
546
+ message_prefix: '[Hi Event]',
547
+ },
548
+ },
549
+ _generated_by: 'hi_agent_install',
550
+ _generated_at: new Date().toISOString(),
551
+ _receiver_config_path: args.receiverConfigPath,
552
+ };
553
+ }
554
+ async function writeReceiverConfigFile(receiverConfigPath, receiverConfig) {
555
+ await fs.mkdir(path.dirname(receiverConfigPath), { recursive: true });
556
+ await fs.writeFile(receiverConfigPath, `${JSON.stringify(receiverConfig, null, 2)}\n`, 'utf8');
557
+ }
558
+ function isProcessAlive(pid) {
559
+ if (!Number.isInteger(pid) || Number(pid) <= 0)
560
+ return false;
561
+ try {
562
+ process.kill(Number(pid), 0);
563
+ return true;
564
+ }
565
+ catch {
566
+ return false;
567
+ }
568
+ }
569
+ async function startDetachedReceiver(args) {
570
+ const [command, ...prefixArgs] = args.receiverCommandArgv;
571
+ if (!command)
572
+ throw new Error('missing_receiver_command');
573
+ return await new Promise((resolve, reject) => {
574
+ const child = spawn(command, [...prefixArgs, 'run', '--config', args.receiverConfigPath], {
575
+ detached: true,
576
+ stdio: 'ignore',
577
+ env: {
578
+ ...process.env,
579
+ HI_AGENT_RECEIVER_CONFIG: args.receiverConfigPath,
580
+ },
581
+ });
582
+ child.once('error', (error) => reject(error));
583
+ child.once('spawn', () => {
584
+ child.unref();
585
+ resolve(Number(child.pid || 0));
586
+ });
587
+ });
588
+ }
589
+ function buildDoctorSummary(args) {
590
+ const installState = stateInstallSnapshot(args.state.runtime);
591
+ const remoteInstallation = args.remote?.installation;
592
+ const deliveryDeclaration = remoteInstallation?.installation?.delivery_capabilities
593
+ || args.state.identity?.delivery_capabilities
594
+ || null;
595
+ const localReceiverEnabled = hasActiveInstallationCapability(deliveryDeclaration, 'local_receiver');
596
+ const deliveryProbeOk = args.deliveryProbe ? normalizeBooleanFlag(args.deliveryProbe.ok) : !localReceiverEnabled;
597
+ return {
598
+ ok: args.blockers.length === 0,
599
+ profile: config.profile,
600
+ platform_base_url: config.platformBaseUrl,
601
+ state_dir: config.stateDir,
602
+ state_file: resolveCurrentStateFile(),
603
+ canonical_openclaw_state_dir: resolveCanonicalOpenClawStateDir(config.profile),
604
+ connected: !!args.state.identity,
605
+ activated: !!(remoteInstallation?.installation?.activated_at || args.state.identity?.activated_at),
606
+ event_path: deliveryDeclaration?.preferred || (localReceiverEnabled ? 'local_receiver' : 'claim_ack'),
607
+ receiver_config_path: installState.receiver_config_path,
608
+ receiver_running: isProcessAlive(installState.receiver_pid),
609
+ push_ready: deliveryProbeOk,
610
+ blockers: args.blockers,
611
+ warnings: args.warnings,
612
+ delivery_capabilities: deliveryDeclaration,
613
+ delivery_probe: args.deliveryProbe,
614
+ };
615
+ }
322
616
  async function handleRegister(args) {
323
617
  const current = await loadPersistedState();
324
618
  if (current.identity && args.replace_existing_state !== true) {
@@ -330,11 +624,7 @@ async function handleRegister(args) {
330
624
  }
331
625
  const { wellKnown, gateway } = await createBootstrapClients();
332
626
  const agentId = normalizeText(args.agent_id);
333
- if (!agentId) {
334
- return fail('missing_agent_id');
335
- }
336
627
  const request = {
337
- agent_id: agentId,
338
628
  display_name: normalizeText(args.display_name),
339
629
  agent_kind: normalizeText(args.agent_kind) || undefined,
340
630
  capabilities: args.capabilities,
@@ -342,6 +632,10 @@ async function handleRegister(args) {
342
632
  delivery_capabilities: normalizeAgentInstallationDeliveryDeclaration(args.delivery_capabilities),
343
633
  status: normalizeText(args.status) || undefined,
344
634
  };
635
+ if (agentId)
636
+ request.agent_id = agentId;
637
+ // gateway 现在会在省略 agent_id 时正式生成 canonical ag_ id;
638
+ // 这里保持请求与网关 contract 一致,不再人为制造一个本地占位 id。
345
639
  const registered = await gateway.register(request);
346
640
  await persistState(() => ({
347
641
  profile: config.profile,
@@ -365,11 +659,7 @@ async function handleRegister(args) {
365
659
  activated_at: registered.installation.activated_at,
366
660
  delivery_capabilities: registered.installation.delivery_capabilities,
367
661
  },
368
- runtime: {
369
- last_consumed_stream_seq: 0,
370
- last_claim_lease_id: null,
371
- updated_at: new Date().toISOString(),
372
- },
662
+ runtime: buildInstallRuntimeState(current.runtime, {}),
373
663
  }));
374
664
  return ok({
375
665
  ok: true,
@@ -447,22 +737,353 @@ async function handleStatus(args) {
447
737
  return ok({
448
738
  ok: true,
449
739
  profile: config.profile,
740
+ state_dir: config.stateDir,
741
+ state_file: resolveCurrentStateFile(),
742
+ summary: {
743
+ connected: !!state.identity,
744
+ activated: !!state.identity?.activated_at,
745
+ receiver_config_path: stateInstallSnapshot(state.runtime).receiver_config_path,
746
+ receiver_running: isProcessAlive(stateInstallSnapshot(state.runtime).receiver_pid),
747
+ },
450
748
  state,
451
749
  });
452
750
  }
453
751
  const { gateway } = await createAuthorizedClients();
454
- const me = await gateway.me();
455
- const installation = await gateway.getInstallation();
752
+ const [me, installation, endpoints, subscriptions] = await Promise.all([
753
+ gateway.me(),
754
+ gateway.getInstallation(),
755
+ gateway.listEndpoints(),
756
+ gateway.listSubscriptions(),
757
+ ]);
456
758
  return ok({
457
759
  ok: true,
458
760
  profile: config.profile,
761
+ state_dir: config.stateDir,
762
+ state_file: resolveCurrentStateFile(),
763
+ summary: {
764
+ connected: !!state.identity,
765
+ activated: !!installation.installation?.activated_at,
766
+ receiver_config_path: stateInstallSnapshot(state.runtime).receiver_config_path,
767
+ receiver_running: isProcessAlive(stateInstallSnapshot(state.runtime).receiver_pid),
768
+ event_path: installation.installation?.delivery_capabilities?.preferred || null,
769
+ },
459
770
  state,
460
771
  remote: {
461
772
  me,
462
773
  installation,
774
+ endpoints,
775
+ subscriptions,
463
776
  },
464
777
  });
465
778
  }
779
+ async function handleDoctor(args) {
780
+ const includeRemote = args.include_remote !== false;
781
+ const probeDelivery = args.probe_delivery !== false;
782
+ const state = await loadPersistedState();
783
+ const blockers = [];
784
+ const warnings = [];
785
+ const installState = stateInstallSnapshot(state.runtime);
786
+ let remote = null;
787
+ let deliveryProbe = null;
788
+ if (!state.identity) {
789
+ blockers.push('missing_agent_identity');
790
+ }
791
+ if (installState.host_kind === 'openclaw' && config.stateDir !== resolveCanonicalOpenClawStateDir(config.profile)) {
792
+ warnings.push('openclaw_state_dir_not_canonical');
793
+ }
794
+ if (includeRemote && state.identity) {
795
+ try {
796
+ const { gateway } = await createAuthorizedClients();
797
+ const [me, installation, endpoints, subscriptions] = await Promise.all([
798
+ gateway.me(),
799
+ gateway.getInstallation(),
800
+ gateway.listEndpoints(),
801
+ gateway.listSubscriptions(),
802
+ ]);
803
+ remote = {
804
+ me: me,
805
+ installation: installation,
806
+ endpoints: endpoints.endpoints,
807
+ subscriptions: subscriptions.subscriptions,
808
+ };
809
+ const declaration = installation.installation.delivery_capabilities || null;
810
+ const activeTopics = new Set((subscriptions.subscriptions || [])
811
+ .filter((entry) => normalizeText(entry?.status) === 'active')
812
+ .map((entry) => normalizeText(entry?.topic))
813
+ .filter(Boolean));
814
+ const missingTopics = AGENT_GATEWAY_EVENT_TOPICS.filter((topic) => !activeTopics.has(topic));
815
+ if (missingTopics.length > 0) {
816
+ blockers.push(`missing_default_subscriptions:${missingTopics.join(',')}`);
817
+ }
818
+ if (hasActiveInstallationCapability(declaration, 'local_receiver')) {
819
+ const receiverConfigPath = resolveReceiverConfigPath(state);
820
+ try {
821
+ await fs.access(receiverConfigPath);
822
+ }
823
+ catch {
824
+ blockers.push('missing_receiver_config');
825
+ }
826
+ const activePushEndpoints = (endpoints.endpoints || [])
827
+ .filter((entry) => normalizeText(entry?.status) === 'active')
828
+ .filter((entry) => normalizeText(entry?.profile) !== 'hi.sse.v1');
829
+ if (activePushEndpoints.length > 0) {
830
+ warnings.push('multiple_event_paths_configured');
831
+ }
832
+ if (probeDelivery) {
833
+ try {
834
+ const probe = await gateway.testDelivery({
835
+ text: `hi_agent_doctor:${new Date().toISOString()}`,
836
+ });
837
+ deliveryProbe = probe;
838
+ if (!(probe.results || []).some((entry) => entry.ok)) {
839
+ blockers.push('local_receiver_delivery_probe_failed');
840
+ }
841
+ }
842
+ catch (error) {
843
+ blockers.push(`local_receiver_delivery_probe_failed:${String(error?.message || error || 'unknown_error')}`);
844
+ }
845
+ }
846
+ else {
847
+ warnings.push('local_receiver_delivery_not_probed');
848
+ }
849
+ }
850
+ }
851
+ catch (error) {
852
+ blockers.push(`remote_doctor_check_failed:${String(error?.message || error || 'unknown_error')}`);
853
+ }
854
+ }
855
+ return ok(buildDoctorSummary({
856
+ state,
857
+ remote,
858
+ blockers,
859
+ warnings,
860
+ deliveryProbe,
861
+ }));
862
+ }
863
+ async function handleInstall(args) {
864
+ const hostKind = normalizeHostKind(args.host_kind);
865
+ const enableLocalReceiver = typeof args.enable_local_receiver === 'boolean'
866
+ ? args.enable_local_receiver
867
+ : hostKind === 'openclaw';
868
+ const receiverTransport = normalizeReceiverTransport(args.receiver_transport);
869
+ const subscribeDefaultTopics = args.subscribe_default_topics !== false;
870
+ const receiverShouldStart = enableLocalReceiver ? args.receiver_start !== false : false;
871
+ const receiverCommandArgv = (() => {
872
+ const explicitArgv = normalizeCommandArgv(args.receiver_command_argv);
873
+ if (explicitArgv.length > 0)
874
+ return explicitArgv;
875
+ const command = normalizeText(args.receiver_command) || 'hi-agent-receiver';
876
+ return [command];
877
+ })();
878
+ const hostAdapterKindRaw = normalizeText(args.host_adapter_kind).toLowerCase();
879
+ const hostAdapterKind = hostAdapterKindRaw === 'openresponses'
880
+ ? 'openresponses'
881
+ : 'openclaw_hooks';
882
+ const hostAdapterUrl = normalizeText(args.host_adapter_url)
883
+ || (hostKind === 'openclaw' && hostAdapterKind === 'openclaw_hooks' ? 'http://127.0.0.1:18789/hooks/agent' : '');
884
+ const migration = args.migrate_legacy_state !== false
885
+ ? await maybeMigrateLegacyState()
886
+ : { migrated: false, from: null, to: resolveCurrentStateFile() };
887
+ let state = await loadPersistedState();
888
+ let registerPayload = null;
889
+ if (!state.identity) {
890
+ const displayName = normalizeText(args.display_name);
891
+ if (!displayName) {
892
+ return fail('missing_display_name', {
893
+ profile: config.profile,
894
+ state_file: resolveCurrentStateFile(),
895
+ });
896
+ }
897
+ const registerResult = await handleRegister({
898
+ display_name: displayName,
899
+ agent_kind: normalizeText(args.agent_kind) || undefined,
900
+ replace_existing_state: args.replace_existing_state === true,
901
+ });
902
+ if (registerResult.isError)
903
+ return registerResult;
904
+ registerPayload = isPlainObject(registerResult.structuredContent)
905
+ ? registerResult.structuredContent
906
+ : null;
907
+ state = await loadPersistedState();
908
+ }
909
+ let activatePayload = null;
910
+ if (state.identity && !state.identity.activated_at) {
911
+ const activateResult = await handleActivate({});
912
+ if (activateResult.isError)
913
+ return activateResult;
914
+ activatePayload = isPlainObject(activateResult.structuredContent)
915
+ ? activateResult.structuredContent
916
+ : null;
917
+ state = await loadPersistedState();
918
+ }
919
+ const { gateway } = await createAuthorizedClients();
920
+ const desiredDeliveryCapabilities = buildInstallationDeliveryDeclaration({
921
+ enableLocalReceiver,
922
+ receiverTransport,
923
+ });
924
+ const updatedInstallation = await gateway.updateInstallation({
925
+ delivery_capabilities: desiredDeliveryCapabilities,
926
+ });
927
+ await persistState((current) => ({
928
+ ...current,
929
+ identity: current.identity
930
+ ? {
931
+ ...current.identity,
932
+ delivery_capabilities: updatedInstallation.installation.delivery_capabilities,
933
+ activated_at: updatedInstallation.installation.activated_at,
934
+ }
935
+ : current.identity,
936
+ runtime: buildInstallRuntimeState(current.runtime, {
937
+ host_kind: hostKind,
938
+ }),
939
+ }));
940
+ let subscriptionsPayload = null;
941
+ if (subscribeDefaultTopics) {
942
+ const subscriptions = buildDefaultSubscriptions();
943
+ const upserted = await gateway.upsertSubscriptions({ subscriptions });
944
+ subscriptionsPayload = upserted;
945
+ }
946
+ let receiverPayload = null;
947
+ if (enableLocalReceiver) {
948
+ if (!hostAdapterUrl) {
949
+ return fail('missing_host_adapter_url');
950
+ }
951
+ const receiverConfigPath = resolveReceiverConfigPath(await loadPersistedState());
952
+ const receiverConfig = buildReceiverConfig({
953
+ receiverConfigPath,
954
+ hostKind,
955
+ receiverTransport,
956
+ hostAdapterKind,
957
+ hostAdapterUrl,
958
+ hostAdapterBearerToken: normalizeText(args.host_adapter_bearer_token) || undefined,
959
+ openresponsesModel: normalizeText(args.openresponses_model) || undefined,
960
+ });
961
+ await writeReceiverConfigFile(receiverConfigPath, receiverConfig);
962
+ state = await persistState((current) => ({
963
+ ...current,
964
+ runtime: buildInstallRuntimeState(current.runtime, {
965
+ host_kind: hostKind,
966
+ receiver_config_path: receiverConfigPath,
967
+ receiver_last_error: null,
968
+ }),
969
+ }));
970
+ let receiverPid = stateInstallSnapshot(state.runtime).receiver_pid;
971
+ if (receiverShouldStart && !isProcessAlive(receiverPid)) {
972
+ try {
973
+ receiverPid = await startDetachedReceiver({
974
+ receiverConfigPath,
975
+ receiverCommandArgv,
976
+ });
977
+ state = await persistState((current) => ({
978
+ ...current,
979
+ runtime: buildInstallRuntimeState(current.runtime, {
980
+ host_kind: hostKind,
981
+ receiver_config_path: receiverConfigPath,
982
+ receiver_pid: receiverPid,
983
+ receiver_last_started_at: new Date().toISOString(),
984
+ receiver_last_error: null,
985
+ }),
986
+ }));
987
+ }
988
+ catch (error) {
989
+ state = await persistState((current) => ({
990
+ ...current,
991
+ runtime: buildInstallRuntimeState(current.runtime, {
992
+ host_kind: hostKind,
993
+ receiver_config_path: receiverConfigPath,
994
+ receiver_pid: null,
995
+ receiver_last_error: String(error?.message || error || 'receiver_start_failed'),
996
+ }),
997
+ }));
998
+ return fail('receiver_start_failed', {
999
+ receiver_config_path: receiverConfigPath,
1000
+ error: String(error?.message || error || 'receiver_start_failed'),
1001
+ });
1002
+ }
1003
+ }
1004
+ receiverPayload = {
1005
+ config_path: receiverConfigPath,
1006
+ started: receiverShouldStart,
1007
+ receiver_pid: receiverPid || null,
1008
+ receiver_command_argv: receiverCommandArgv,
1009
+ transport: receiverTransport,
1010
+ host_adapter_kind: hostAdapterKind,
1011
+ host_adapter_url: hostAdapterUrl,
1012
+ };
1013
+ }
1014
+ if (receiverShouldStart) {
1015
+ await sleep(1500);
1016
+ }
1017
+ const doctor = args.run_doctor === false
1018
+ ? null
1019
+ : await handleDoctor({
1020
+ include_remote: true,
1021
+ probe_delivery: enableLocalReceiver,
1022
+ });
1023
+ return ok({
1024
+ ok: true,
1025
+ profile: config.profile,
1026
+ state_dir: config.stateDir,
1027
+ state_file: resolveCurrentStateFile(),
1028
+ migrated_legacy_state: migration,
1029
+ register: registerPayload,
1030
+ activate: activatePayload,
1031
+ installation: updatedInstallation,
1032
+ subscriptions: subscriptionsPayload,
1033
+ receiver: receiverPayload,
1034
+ doctor: doctor?.structuredContent || null,
1035
+ });
1036
+ }
1037
+ async function handleReset(args) {
1038
+ const state = await loadPersistedState();
1039
+ const installState = stateInstallSnapshot(state.runtime);
1040
+ const stopReceiver = args.stop_receiver !== false;
1041
+ const removeReceiverConfig = args.remove_receiver_config !== false;
1042
+ const clearState = args.clear_state !== false;
1043
+ let stopResult = null;
1044
+ if (stopReceiver && isProcessAlive(installState.receiver_pid)) {
1045
+ try {
1046
+ process.kill(Number(installState.receiver_pid), 'SIGTERM');
1047
+ stopResult = {
1048
+ attempted: true,
1049
+ pid: installState.receiver_pid,
1050
+ signalled: true,
1051
+ };
1052
+ }
1053
+ catch (error) {
1054
+ stopResult = {
1055
+ attempted: true,
1056
+ pid: installState.receiver_pid,
1057
+ signalled: false,
1058
+ error: String(error?.message || error || 'receiver_stop_failed'),
1059
+ };
1060
+ }
1061
+ }
1062
+ const receiverConfigPath = installState.receiver_config_path || resolveReceiverConfigPath(state);
1063
+ if (removeReceiverConfig && receiverConfigPath) {
1064
+ await fs.rm(receiverConfigPath, { force: true });
1065
+ }
1066
+ if (clearState) {
1067
+ await fs.rm(resolveCurrentStateFile(), { force: true });
1068
+ }
1069
+ else {
1070
+ await persistState((current) => ({
1071
+ ...current,
1072
+ runtime: buildInstallRuntimeState(current.runtime, {
1073
+ receiver_config_path: removeReceiverConfig ? null : receiverConfigPath,
1074
+ receiver_pid: null,
1075
+ receiver_last_error: null,
1076
+ }),
1077
+ }));
1078
+ }
1079
+ return ok({
1080
+ ok: true,
1081
+ profile: config.profile,
1082
+ removed_state_file: clearState ? resolveCurrentStateFile() : null,
1083
+ removed_receiver_config_path: removeReceiverConfig ? receiverConfigPath : null,
1084
+ receiver_stop: stopResult,
1085
+ });
1086
+ }
466
1087
  async function handleInstallationGet() {
467
1088
  const { gateway } = await createAuthorizedClients();
468
1089
  const installation = await gateway.getInstallation();
@@ -645,6 +1266,12 @@ async function handleControlTool(name, args) {
645
1266
  switch (name) {
646
1267
  case 'hi_agent_status':
647
1268
  return await handleStatus(args);
1269
+ case 'hi_agent_install':
1270
+ return await handleInstall(args);
1271
+ case 'hi_agent_doctor':
1272
+ return await handleDoctor(args);
1273
+ case 'hi_agent_reset':
1274
+ return await handleReset(args);
648
1275
  case 'hi_agent_register':
649
1276
  return await handleRegister(args);
650
1277
  case 'hi_agent_connect':
@@ -695,7 +1322,7 @@ async function listTools() {
695
1322
  function createMcpServer() {
696
1323
  const server = new Server({
697
1324
  name: 'hi-mcp-server',
698
- version: '0.1.0',
1325
+ version: '0.1.3',
699
1326
  }, {
700
1327
  capabilities: {
701
1328
  tools: {},