@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/README.md +21 -18
- package/dist/server.d.ts +1 -0
- package/dist/server.js +644 -17
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +46 -1
- package/node_modules/@hirey/hi-agent-contracts/dist/index.d.ts +36 -9
- package/node_modules/@hirey/hi-agent-contracts/dist/index.d.ts.map +1 -1
- package/node_modules/@hirey/hi-agent-contracts/dist/index.js +102 -11
- package/node_modules/@hirey/hi-agent-contracts/package.json +2 -1
- package/node_modules/@hirey/hi-agent-sdk/README.md +1 -0
- package/node_modules/@hirey/hi-agent-sdk/dist/client.d.ts +3 -2
- package/node_modules/@hirey/hi-agent-sdk/dist/client.d.ts.map +1 -1
- package/node_modules/@hirey/hi-agent-sdk/package.json +2 -2
- package/package.json +4 -4
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:
|
|
16
|
-
stateDir: normalizeText(process.env.HI_MCP_STATE_DIR) ||
|
|
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
|
|
455
|
-
|
|
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.
|
|
1325
|
+
version: '0.1.3',
|
|
699
1326
|
}, {
|
|
700
1327
|
capabilities: {
|
|
701
1328
|
tools: {},
|