@hirey/hi-mcp-server 0.1.1

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 ADDED
@@ -0,0 +1,285 @@
1
+ # hi-mcp-server
2
+
3
+ `hi-mcp-server` 是 `Hi` 面向 `MCP-first` 宿主的统一适配层。
4
+
5
+ 它不替代 `hi-platform` 或 `hi-agent-gateway` 的 public entry,而是把现有 public surface 原样接成 MCP:
6
+
7
+ - `well-known / skills / capabilities / capability call` 继续来自 `hi-platform`
8
+ - `register / oauth/token / activate / installation / endpoints / subscriptions / events` 继续来自 `hi-agent-gateway`
9
+ - 业务工具名继续直接复用 `Hi` 当前公开的 `tool_name`
10
+
11
+ ## 当前职责
12
+
13
+ - 把 `Hi` public capability catalog 动态暴露成 MCP tools
14
+ - 保留 gateway 控制面为 `hi_agent_*` 控制工具,而不是伪装成新的业务工具
15
+ - 本地持久化 `agent_id / installation_id / client_id / client_secret`
16
+ - 运行时再用 `client_credentials` 换短期 `access_token`
17
+ - 为没有宿主原生 live push 的 MCP client 提供 durable 事件消费面:
18
+ - `hi_agent_events_claim`
19
+ - `hi_agent_event_fetch`
20
+ - `hi_agent_events_ack`
21
+ - `hi_agent_events_wait`
22
+
23
+ ## 对外暴露的两类工具
24
+
25
+ ### 1. 控制面工具
26
+
27
+ 这些工具都以 `hi_agent_*` 命名,保持和 gateway 控制面同一语义边界:
28
+
29
+ - `hi_agent_status`
30
+ - `hi_agent_register`
31
+ - `hi_agent_connect`
32
+ - `hi_agent_activate`
33
+ - `hi_agent_installation_get`
34
+ - `hi_agent_installation_update`
35
+ - `hi_agent_endpoints_list`
36
+ - `hi_agent_endpoints_upsert`
37
+ - `hi_agent_subscriptions_list`
38
+ - `hi_agent_subscriptions_upsert`
39
+ - `hi_agent_test_delivery`
40
+ - `hi_agent_events_claim`
41
+ - `hi_agent_event_fetch`
42
+ - `hi_agent_events_ack`
43
+ - `hi_agent_events_wait`
44
+
45
+ ### 2. 业务工具
46
+
47
+ 业务工具不在这里重新命名。
48
+
49
+ `hi-mcp-server` 会实时读取 `Hi` 的 public capability catalog,并把当前平台公开的工具原样映射出来,例如:
50
+
51
+ - `agent_listings`
52
+ - `matching_sessions`
53
+ - `pairings`
54
+ - `meeting_chain`
55
+ - `meeting_confirmation`
56
+
57
+ 这样做的目的是让 Claude Code、OpenClaw 和其他 `MCP-first` 宿主看到的工具名、参数 schema、调用语义都跟 `Hi` 平台目录保持一致,不产生第二套 API。
58
+
59
+ ## 普通用户安装
60
+
61
+ 普通用户的正式安装路径不应该依赖 `AWS`、`CodeArtifact`、私有 registry,或者一套专门给内部同事写的 Docker/Helm 操作。
62
+
63
+ 面向 OpenClaw / 本地 MCP host 的官方安装方式应是:
64
+
65
+ ```bash
66
+ npm install -g @hirey/hi-mcp-server@0.1.1
67
+ ```
68
+
69
+ 安装后,普通用户推荐把它作为 **本地 stdio MCP server** 挂到宿主里,而不是先自己部署一个公网 `/mcp` 服务。
70
+
71
+ 对 OpenClaw,推荐的 MCP 配置形态是:
72
+
73
+ ```json
74
+ {
75
+ "command": "hi-mcp-server",
76
+ "env": {
77
+ "HI_PLATFORM_BASE_URL": "https://your-hi-platform.example.com",
78
+ "HI_MCP_TRANSPORT": "stdio",
79
+ "HI_MCP_PROFILE": "openclaw-main",
80
+ "HI_MCP_STATE_DIR": "/Users/you/.openclaw/hi"
81
+ }
82
+ }
83
+ ```
84
+
85
+ 普通用户首次接入后的典型顺序:
86
+
87
+ 1. 用宿主把 `hi-mcp-server` 作为 `stdio` MCP server 挂上
88
+ 2. 在宿主对话里执行 `hi_agent_register -> hi_agent_connect -> hi_agent_activate`
89
+ 3. 让 `hi-mcp-server` 把 installation 长期身份持久化到固定 `HI_MCP_STATE_DIR`
90
+ 4. 如果宿主需要主动事件回流,再安装 `hi-agent-receiver`
91
+
92
+ 这条路径的目标是:**普通 OpenClaw 用户只需要 `npm + OpenClaw TUI`,不需要 AWS。**
93
+
94
+ ## 配置
95
+
96
+ ### 环境变量
97
+
98
+ - `HI_PLATFORM_BASE_URL`
99
+ - 必填。指向目标 `Hi` 平台公网入口。
100
+ - `HI_MCP_TRANSPORT`
101
+ - 可选。`http` 或 `stdio`。
102
+ - 默认 `http`。
103
+ - `HI_MCP_HOST`
104
+ - `http` 模式监听地址,默认 `127.0.0.1`。
105
+ - `HI_MCP_PORT`
106
+ - `http` 模式端口,默认 `8788`。
107
+ - `HI_MCP_PROFILE`
108
+ - 本地 state profile,默认 `default`。
109
+ - `HI_MCP_STATE_DIR`
110
+ - 本地 state 目录,默认 `./.hi-agent-state`。
111
+ - OpenClaw 普通用户建议显式固定到稳定目录,例如 `/Users/you/.openclaw/hi`,方便后续 `hi-agent-receiver` 直接复用同一份身份 state。
112
+
113
+ ### 本地 state
114
+
115
+ state 会按 profile 落到 `HI_MCP_STATE_DIR/<profile>.json`。
116
+
117
+ 文件内部明确分成两块:
118
+
119
+ - `identity`
120
+ - 长期凭证与安装身份:`agent_id / installation_id / client_id / client_secret / token_url`
121
+ - `runtime`
122
+ - 事件消费状态:`last_consumed_stream_seq / last_claim_lease_id`
123
+
124
+ 短期 `access_token` 不会被持久化。
125
+
126
+ ## 启动方式
127
+
128
+ ### Remote MCP / HTTP
129
+
130
+ ```bash
131
+ HI_PLATFORM_BASE_URL="https://your-hi-platform.example.com" \
132
+ HI_MCP_TRANSPORT=http \
133
+ HI_MCP_HOST=0.0.0.0 \
134
+ HI_MCP_PORT=8788 \
135
+ hi-mcp-server
136
+ ```
137
+
138
+ MCP endpoint:
139
+
140
+ - `POST /mcp`
141
+ - `GET /healthz`
142
+
143
+ ### Stdio
144
+
145
+ ```bash
146
+ HI_PLATFORM_BASE_URL="https://your-hi-platform.example.com" \
147
+ HI_MCP_TRANSPORT=stdio \
148
+ hi-mcp-server
149
+ ```
150
+
151
+ 这条路径适合本地宿主直接拉起 server 进程。
152
+
153
+ ## 宿主接收矩阵
154
+
155
+ ### MCP-first 宿主
156
+
157
+ - 主路径:`hi-mcp-server`
158
+ - 消息/事件:`hi_agent_events_wait` 或 `claim/fetch/ack`
159
+ - 是否要求 live push:不要求
160
+ - 典型宿主:
161
+ - Claude Code
162
+ - generic MCP host
163
+
164
+ ### OpenClaw
165
+
166
+ - 工具接入主路径:仍然是 `hi-mcp-server`
167
+ - 普通用户主路径:本地 `stdio` child process
168
+ - 推荐先通过公开 npm 安装 `@hirey/hi-mcp-server`,再把它注册到 `openclaw mcp set`
169
+ - 有公网 ingress:
170
+ - 事件主路径:gateway endpoint `openclaw.hooks.agent.v1`
171
+ - 补充路径:`openresponses.v1`
172
+ - 无公网 ingress:
173
+ - 事件主路径:`hi-agent-receiver`
174
+ - 本机适配器:
175
+ - `openclaw_hooks`
176
+ - `openresponses`
177
+
178
+ ### HTTP / webhook-first 宿主
179
+
180
+ - 当前阶段:只收口 contract 与矩阵,不在 `hi-mcp-server` 里额外包一套 HTTP-first 业务工具
181
+ - 后续正式主路径:`generic.event-webhook.v1`
182
+
183
+ ### 带 channel / live push 的宿主
184
+
185
+ - channel 只作为可选增强
186
+ - 不能作为 `Hi` 的公共主协议
187
+ - 当前 `hi-mcp-server` 默认仍以 durable pull 为基线
188
+
189
+ ## 分发与部署
190
+
191
+ ### 固定公网入口
192
+
193
+ `hi-mcp-server` 依赖 `Hi` 的固定 well-known:
194
+
195
+ - `https://<platform>/.well-known/hi-agent-platform.json`
196
+
197
+ 这个入口继续由 `hi-platform` 提供,不迁到 `hi-mcp-server`。
198
+
199
+ ### `hi-mcp-server` 自身
200
+
201
+ `hi-mcp-server` 现在的 installation 身份与 event cursor 是本地持久化的,所以它的正式部署形态是:
202
+
203
+ - **dedicated adapter**
204
+ - **单 installation / 单 state**
205
+ - **固定版本镜像**
206
+ - **固定 HTTPS 地址指向 `/mcp`**
207
+
208
+ 不应该把一个共享的 `hi-mcp-server` release 直接暴露给多个互不相关的宿主 installation;否则它们会争用同一份 persisted state。
209
+
210
+ 这不意味着普通用户不能按 npm 包安装。
211
+
212
+ - 对普通 OpenClaw / 本地 MCP host:推荐 **公开 npm 包 + 本地 stdio**
213
+ - 对基础设施运营方:推荐 **dedicated adapter service**
214
+
215
+ 当前仓库已经提供:
216
+
217
+ - `Dockerfile`
218
+ - `deploy/chart/hi-mcp-server`
219
+
220
+ 如果你是在做源码级自建镜像,而不是普通用户本地安装,直接走公开 npm 依赖即可,不需要再为这个 adapter 准备 `AWS` 或 `CodeArtifact`:
221
+
222
+ ```bash
223
+ docker build \
224
+ -t hi-mcp-server:0.1.1 \
225
+ .
226
+ ```
227
+
228
+ 推荐地址形态例如:
229
+
230
+ - `https://claude-agent-01.example.com/mcp`
231
+ - `https://openclaw-bridge-01.example.com/mcp`
232
+
233
+ 推荐部署约束:
234
+
235
+ - 固定 DNS / HTTPS 地址
236
+ - 反向代理到 `POST /mcp`
237
+ - 单副本
238
+ - 持久卷保存 `HI_MCP_STATE_DIR`
239
+ - 容器镜像 tag 必须固定,不要使用 `latest`
240
+
241
+ 一个最小 Helm values 只需要覆盖:
242
+
243
+ - `image.repository`
244
+ - `image.tag`
245
+ - `env` 中的 `HI_PLATFORM_BASE_URL`
246
+ - `ingress.hosts`
247
+ - `persistence.storageClassName`
248
+
249
+ 如果只是做本地或单机验证,也可以直接:
250
+
251
+ ```bash
252
+ HI_PLATFORM_BASE_URL="https://your-hi-platform.example.com" \
253
+ HI_MCP_TRANSPORT=http \
254
+ HI_MCP_HOST=0.0.0.0 \
255
+ HI_MCP_PORT=8788 \
256
+ HI_MCP_STATE_DIR=/var/lib/hi-mcp-server/state \
257
+ hi-mcp-server
258
+ ```
259
+
260
+ ### 升级策略
261
+
262
+ - 先升级 `hi-agent-contracts`
263
+ - 再升级 `hi-agent-delivery / hi-agent-sdk / hi-agent-gateway`
264
+ - 最后滚动升级 `hi-mcp-server`
265
+
266
+ 这样可以避免 capability schema、delivery profile 和控制面 contract 漂移。
267
+
268
+ ### 为什么不放进 shared `hi-infra`
269
+
270
+ shared `hi-infra` 负责 `Hi` 自己的公共平台面;`hi-mcp-server` 则持有 installation 级本地 state。
271
+
272
+ 因此它更适合:
273
+
274
+ - 按宿主/agent 单独部署
275
+ - 或由外部用户自己部署
276
+
277
+ 而不是和 `hi-platform` / `hi-agent-gateway` 一起作为共享多租户 release 常驻在同一个公共入口下。
278
+
279
+ ## 验证建议
280
+
281
+ phase 1 至少验证三条线:
282
+
283
+ - Claude Code / generic MCP host 能通过 `hi_agent_register -> hi_agent_activate` 后调用 `agent_listings / matching_sessions / pairings / meeting_chain`
284
+ - OpenClaw 公网 ingress 能接 `openclaw.hooks.agent.v1`
285
+ - OpenClaw 无公网 ingress 能通过 `hi-agent-receiver` 收到并 ack `Hi` durable events
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
package/dist/server.js ADDED
@@ -0,0 +1,838 @@
1
+ import { createServer } from 'node:http';
2
+ import process from 'node:process';
3
+ import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ import { normalizeAgentEndpointList, normalizeAgentInstallationDeliveryDeclaration, normalizeAgentSubscriptionList, normalizeText, } from '@hirey/hi-agent-contracts';
9
+ import { createHiAgentClients, exchangeHiAgentClientCredentialsToken, HiAgentGatewayClient, HiAgentPlatformClient, } from '@hirey/hi-agent-sdk';
10
+ import { readState, updateState, } from './state.js';
11
+ const CAPABILITY_CACHE_TTL_MS = 30_000;
12
+ const config = {
13
+ host: normalizeText(process.env.HI_MCP_HOST) || '127.0.0.1',
14
+ 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`,
17
+ platformBaseUrl: normalizeText(process.env.HI_PLATFORM_BASE_URL),
18
+ transport: normalizeText(process.env.HI_MCP_TRANSPORT).toLowerCase() === 'stdio' ? 'stdio' : 'http',
19
+ };
20
+ let capabilityCache = null;
21
+ function assertConfig() {
22
+ if (!config.platformBaseUrl) {
23
+ throw new Error('missing_hi_platform_base_url');
24
+ }
25
+ if (!Number.isFinite(config.port) || config.port <= 0) {
26
+ throw new Error('invalid_hi_mcp_port');
27
+ }
28
+ }
29
+ function jsonText(value) {
30
+ return JSON.stringify(value, null, 2);
31
+ }
32
+ function ok(structuredContent, text) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: 'text',
37
+ text: text || jsonText(structuredContent),
38
+ },
39
+ ],
40
+ structuredContent,
41
+ };
42
+ }
43
+ function fail(message, detail) {
44
+ const payload = detail == null
45
+ ? { ok: false, error: message }
46
+ : { ok: false, error: message, detail };
47
+ return {
48
+ isError: true,
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: jsonText(payload),
53
+ },
54
+ ],
55
+ structuredContent: payload,
56
+ };
57
+ }
58
+ function sleep(ms) {
59
+ return new Promise((resolve) => setTimeout(resolve, ms));
60
+ }
61
+ function controlTools() {
62
+ // control tools 只映射 gateway/onboarding/runtime 管理面,不伪装成第二套业务 API。
63
+ return [
64
+ {
65
+ name: 'hi_agent_status',
66
+ description: '读取当前本地持久化的 Hi agent 身份、installation 与事件 cursor;可选做一次远端 me/installation 刷新。',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ include_remote: { type: 'boolean', description: 'true 时额外请求 gateway me/installation 做一次实时校验。' },
71
+ },
72
+ },
73
+ },
74
+ {
75
+ name: 'hi_agent_register',
76
+ description: '按 Hi 官方 register -> token -> activate 主线创建一个新的 external agent installation,并把长期凭证持久化到本地 state。',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ agent_id: { type: 'string' },
81
+ display_name: { type: 'string' },
82
+ agent_kind: { type: 'string' },
83
+ capabilities: { type: 'object' },
84
+ metadata: { type: 'object' },
85
+ delivery_capabilities: { type: 'object' },
86
+ replace_existing_state: { type: 'boolean', description: 'true 时允许覆盖当前 profile 已持久化的 installation state。' },
87
+ },
88
+ required: ['display_name'],
89
+ },
90
+ },
91
+ {
92
+ name: 'hi_agent_connect',
93
+ description: '把当前 installation 绑定到一个已存在的 Hi agent;要求本地 profile 已经持久化了 installation 的 client_credentials。',
94
+ inputSchema: {
95
+ type: 'object',
96
+ properties: {
97
+ agent_id: { type: 'string' },
98
+ metadata: { type: 'object' },
99
+ delivery_capabilities: { type: 'object' },
100
+ },
101
+ required: ['agent_id'],
102
+ },
103
+ },
104
+ {
105
+ name: 'hi_agent_activate',
106
+ description: '对当前 installation 执行 activate,让它进入正式可用状态。',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ agent_id: { type: 'string' },
111
+ },
112
+ },
113
+ },
114
+ {
115
+ name: 'hi_agent_installation_get',
116
+ description: '读取当前 installation 的正式 persisted state,包括 delivery_capabilities。',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {},
120
+ },
121
+ },
122
+ {
123
+ name: 'hi_agent_installation_update',
124
+ description: '更新当前 installation 的 metadata / delivery_capabilities;不会额外包装成新的业务协议。',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ metadata: { type: 'object' },
129
+ delivery_capabilities: { type: 'object' },
130
+ },
131
+ },
132
+ },
133
+ {
134
+ name: 'hi_agent_endpoints_list',
135
+ description: '列出当前 agent 的 delivery endpoints。',
136
+ inputSchema: {
137
+ type: 'object',
138
+ properties: {},
139
+ },
140
+ },
141
+ {
142
+ name: 'hi_agent_endpoints_upsert',
143
+ description: '写入当前 agent 的 delivery endpoints,用于公网 webhook / openclaw hooks / openresponses 等正式交付面。',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ endpoints: {
148
+ type: 'array',
149
+ items: { type: 'object' },
150
+ },
151
+ },
152
+ required: ['endpoints'],
153
+ },
154
+ },
155
+ {
156
+ name: 'hi_agent_subscriptions_list',
157
+ description: '列出当前 agent 的 event subscriptions。',
158
+ inputSchema: {
159
+ type: 'object',
160
+ properties: {},
161
+ },
162
+ },
163
+ {
164
+ name: 'hi_agent_subscriptions_upsert',
165
+ description: '写入当前 agent 的 event subscriptions。',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ subscriptions: {
170
+ type: 'array',
171
+ items: { type: 'object' },
172
+ },
173
+ },
174
+ required: ['subscriptions'],
175
+ },
176
+ },
177
+ {
178
+ name: 'hi_agent_test_delivery',
179
+ description: '对当前 agent 的 endpoints 触发一次 test-delivery。',
180
+ inputSchema: {
181
+ type: 'object',
182
+ properties: {
183
+ topic: { type: 'string' },
184
+ text: { type: 'string' },
185
+ payload: { type: 'object' },
186
+ profile: { type: 'string' },
187
+ },
188
+ },
189
+ },
190
+ {
191
+ name: 'hi_agent_events_claim',
192
+ description: '按 persisted cursor 或显式 after_seq claim 一批 durable events。',
193
+ inputSchema: {
194
+ type: 'object',
195
+ properties: {
196
+ after_seq: { type: 'number' },
197
+ limit: { type: 'number' },
198
+ claim_lease_id: { type: 'string' },
199
+ use_persisted_cursor: { type: 'boolean', description: '默认 true;未显式传 after_seq 时从本地 runtime cursor 继续。' },
200
+ },
201
+ },
202
+ },
203
+ {
204
+ name: 'hi_agent_event_fetch',
205
+ description: '按 event_id 拉取单条 event 的完整 payload。',
206
+ inputSchema: {
207
+ type: 'object',
208
+ properties: {
209
+ event_id: { type: 'string' },
210
+ },
211
+ required: ['event_id'],
212
+ },
213
+ },
214
+ {
215
+ name: 'hi_agent_events_ack',
216
+ description: '提交一批 event ack,并在 consumed 时推进本地 persisted cursor。',
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: {
220
+ acks: {
221
+ type: 'array',
222
+ items: {
223
+ type: 'object',
224
+ properties: {
225
+ event_id: { type: 'string' },
226
+ status: { type: 'string' },
227
+ last_error: { type: 'string' },
228
+ result: { type: 'object' },
229
+ retry_after_ms: { type: 'number' },
230
+ },
231
+ required: ['event_id', 'status'],
232
+ },
233
+ },
234
+ },
235
+ required: ['acks'],
236
+ },
237
+ },
238
+ {
239
+ name: 'hi_agent_events_wait',
240
+ description: '用 claim/poll 等待下一批 durable events,适合没有 live push 的 MCP-first 宿主。',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ after_seq: { type: 'number' },
245
+ limit: { type: 'number' },
246
+ timeout_ms: { type: 'number' },
247
+ poll_interval_ms: { type: 'number' },
248
+ use_persisted_cursor: { type: 'boolean' },
249
+ },
250
+ },
251
+ },
252
+ ];
253
+ }
254
+ function isPlainObject(input) {
255
+ return !!input && typeof input === 'object' && !Array.isArray(input);
256
+ }
257
+ function normalizeRecord(input) {
258
+ return isPlainObject(input) ? input : {};
259
+ }
260
+ async function loadWellKnown() {
261
+ const platform = new HiAgentPlatformClient({ baseUrl: config.platformBaseUrl });
262
+ return await platform.wellKnown();
263
+ }
264
+ async function loadCapabilities() {
265
+ if (capabilityCache && Date.now() - capabilityCache.fetchedAt < CAPABILITY_CACHE_TTL_MS) {
266
+ return capabilityCache.items;
267
+ }
268
+ const platform = new HiAgentPlatformClient({ baseUrl: config.platformBaseUrl });
269
+ const out = await platform.listCapabilities();
270
+ const items = Array.isArray(out.capabilities) ? out.capabilities : [];
271
+ capabilityCache = {
272
+ fetchedAt: Date.now(),
273
+ items,
274
+ };
275
+ return items;
276
+ }
277
+ async function createBootstrapClients() {
278
+ const wellKnown = await loadWellKnown();
279
+ const platform = new HiAgentPlatformClient({ baseUrl: config.platformBaseUrl });
280
+ const gateway = new HiAgentGatewayClient({
281
+ baseUrl: wellKnown.platform.registry_base_url,
282
+ token: '',
283
+ });
284
+ return {
285
+ wellKnown,
286
+ platform,
287
+ gateway,
288
+ };
289
+ }
290
+ async function loadPersistedState() {
291
+ return await readState({
292
+ stateDir: config.stateDir,
293
+ profile: config.profile,
294
+ });
295
+ }
296
+ async function persistState(updater) {
297
+ return await updateState({
298
+ stateDir: config.stateDir,
299
+ profile: config.profile,
300
+ updater,
301
+ });
302
+ }
303
+ async function createAuthorizedClients() {
304
+ const state = await loadPersistedState();
305
+ if (!state.identity)
306
+ throw new Error('missing_agent_identity');
307
+ const token = await exchangeHiAgentClientCredentialsToken({
308
+ tokenUrl: state.identity.token_url,
309
+ clientId: state.identity.client_id,
310
+ clientSecret: state.identity.client_secret,
311
+ });
312
+ const clients = await createHiAgentClients({
313
+ platformBaseUrl: state.platform?.platform_base_url || config.platformBaseUrl,
314
+ token: token.access_token,
315
+ });
316
+ return {
317
+ state,
318
+ accessToken: token.access_token,
319
+ ...clients,
320
+ };
321
+ }
322
+ async function handleRegister(args) {
323
+ const current = await loadPersistedState();
324
+ if (current.identity && args.replace_existing_state !== true) {
325
+ return fail('agent_state_already_exists', {
326
+ profile: config.profile,
327
+ agent_id: current.identity.agent_id,
328
+ installation_id: current.identity.installation_id,
329
+ });
330
+ }
331
+ const { wellKnown, gateway } = await createBootstrapClients();
332
+ const agentId = normalizeText(args.agent_id);
333
+ if (!agentId) {
334
+ return fail('missing_agent_id');
335
+ }
336
+ const request = {
337
+ agent_id: agentId,
338
+ display_name: normalizeText(args.display_name),
339
+ agent_kind: normalizeText(args.agent_kind) || undefined,
340
+ capabilities: args.capabilities,
341
+ metadata: isPlainObject(args.metadata) ? args.metadata : null,
342
+ delivery_capabilities: normalizeAgentInstallationDeliveryDeclaration(args.delivery_capabilities),
343
+ status: normalizeText(args.status) || undefined,
344
+ };
345
+ const registered = await gateway.register(request);
346
+ await persistState(() => ({
347
+ profile: config.profile,
348
+ platform: {
349
+ platform_base_url: config.platformBaseUrl,
350
+ registry_base_url: wellKnown.platform.registry_base_url,
351
+ fetched_at: new Date().toISOString(),
352
+ },
353
+ identity: {
354
+ agent_id: registered.agent.agent_id,
355
+ installation_id: registered.installation.installation_id,
356
+ display_name: registered.agent.display_name,
357
+ agent_kind: registered.agent.agent_kind,
358
+ client_id: registered.auth.client_id,
359
+ client_secret: registered.auth.client_secret,
360
+ installation_subject: registered.auth.installation_subject,
361
+ issuer: registered.auth.issuer,
362
+ audience: registered.auth.audience,
363
+ token_url: registered.auth.token_url,
364
+ jwks_url: registered.auth.jwks_url,
365
+ activated_at: registered.installation.activated_at,
366
+ delivery_capabilities: registered.installation.delivery_capabilities,
367
+ },
368
+ runtime: {
369
+ last_consumed_stream_seq: 0,
370
+ last_claim_lease_id: null,
371
+ updated_at: new Date().toISOString(),
372
+ },
373
+ }));
374
+ return ok({
375
+ ok: true,
376
+ agent: registered.agent,
377
+ installation: registered.installation,
378
+ next: registered.next,
379
+ contract: registered.contract,
380
+ persisted_profile: config.profile,
381
+ });
382
+ }
383
+ async function handleConnect(args) {
384
+ const { gateway } = await createAuthorizedClients();
385
+ const connected = await gateway.connect({
386
+ agent_id: normalizeText(args.agent_id),
387
+ metadata: isPlainObject(args.metadata) ? args.metadata : null,
388
+ delivery_capabilities: normalizeAgentInstallationDeliveryDeclaration(args.delivery_capabilities),
389
+ });
390
+ await persistState((current) => ({
391
+ ...current,
392
+ identity: current.identity
393
+ ? {
394
+ ...current.identity,
395
+ agent_id: connected.agent.agent_id,
396
+ display_name: connected.agent.display_name,
397
+ agent_kind: connected.agent.agent_kind,
398
+ installation_id: connected.installation.installation_id,
399
+ activated_at: connected.installation.activated_at,
400
+ delivery_capabilities: connected.installation.delivery_capabilities,
401
+ }
402
+ : current.identity,
403
+ runtime: {
404
+ ...current.runtime,
405
+ updated_at: new Date().toISOString(),
406
+ },
407
+ }));
408
+ return ok({
409
+ ok: true,
410
+ agent: connected.agent,
411
+ installation: connected.installation,
412
+ contract: connected.contract,
413
+ });
414
+ }
415
+ async function handleActivate(args) {
416
+ const { gateway } = await createAuthorizedClients();
417
+ const activated = await gateway.activate({
418
+ agent_id: normalizeText(args.agent_id) || undefined,
419
+ });
420
+ await persistState((current) => ({
421
+ ...current,
422
+ identity: current.identity
423
+ ? {
424
+ ...current.identity,
425
+ agent_id: activated.agent.agent_id,
426
+ display_name: activated.agent.display_name,
427
+ agent_kind: activated.agent.agent_kind,
428
+ activated_at: activated.installation.activated_at,
429
+ delivery_capabilities: activated.installation.delivery_capabilities,
430
+ }
431
+ : current.identity,
432
+ runtime: {
433
+ ...current.runtime,
434
+ updated_at: new Date().toISOString(),
435
+ },
436
+ }));
437
+ return ok({
438
+ ok: true,
439
+ agent: activated.agent,
440
+ installation: activated.installation,
441
+ contract: activated.contract,
442
+ });
443
+ }
444
+ async function handleStatus(args) {
445
+ const state = await loadPersistedState();
446
+ if (args.include_remote !== true || !state.identity) {
447
+ return ok({
448
+ ok: true,
449
+ profile: config.profile,
450
+ state,
451
+ });
452
+ }
453
+ const { gateway } = await createAuthorizedClients();
454
+ const me = await gateway.me();
455
+ const installation = await gateway.getInstallation();
456
+ return ok({
457
+ ok: true,
458
+ profile: config.profile,
459
+ state,
460
+ remote: {
461
+ me,
462
+ installation,
463
+ },
464
+ });
465
+ }
466
+ async function handleInstallationGet() {
467
+ const { gateway } = await createAuthorizedClients();
468
+ const installation = await gateway.getInstallation();
469
+ return ok(installation);
470
+ }
471
+ async function handleInstallationUpdate(args) {
472
+ const { gateway } = await createAuthorizedClients();
473
+ const updated = await gateway.updateInstallation({
474
+ ...(Object.prototype.hasOwnProperty.call(args, 'metadata')
475
+ ? { metadata: isPlainObject(args.metadata) ? args.metadata : null }
476
+ : {}),
477
+ ...(Object.prototype.hasOwnProperty.call(args, 'delivery_capabilities')
478
+ ? { delivery_capabilities: normalizeAgentInstallationDeliveryDeclaration(args.delivery_capabilities) }
479
+ : {}),
480
+ });
481
+ await persistState((current) => ({
482
+ ...current,
483
+ identity: current.identity
484
+ ? {
485
+ ...current.identity,
486
+ delivery_capabilities: updated.installation.delivery_capabilities,
487
+ }
488
+ : current.identity,
489
+ runtime: {
490
+ ...current.runtime,
491
+ updated_at: new Date().toISOString(),
492
+ },
493
+ }));
494
+ return ok({
495
+ ok: true,
496
+ ...updated,
497
+ });
498
+ }
499
+ async function handleEndpointsList() {
500
+ const { gateway } = await createAuthorizedClients();
501
+ const out = await gateway.listEndpoints();
502
+ return ok(out);
503
+ }
504
+ async function handleEndpointsUpsert(args) {
505
+ const { gateway } = await createAuthorizedClients();
506
+ const endpoints = normalizeAgentEndpointList(args.endpoints);
507
+ const out = await gateway.upsertEndpoints({ endpoints });
508
+ return ok({
509
+ ok: true,
510
+ ...out,
511
+ });
512
+ }
513
+ async function handleSubscriptionsList() {
514
+ const { gateway } = await createAuthorizedClients();
515
+ const out = await gateway.listSubscriptions();
516
+ return ok(out);
517
+ }
518
+ async function handleSubscriptionsUpsert(args) {
519
+ const { gateway } = await createAuthorizedClients();
520
+ const subscriptions = normalizeAgentSubscriptionList(args.subscriptions);
521
+ const out = await gateway.upsertSubscriptions({ subscriptions });
522
+ return ok({
523
+ ok: true,
524
+ ...out,
525
+ });
526
+ }
527
+ async function handleTestDelivery(args) {
528
+ const { gateway } = await createAuthorizedClients();
529
+ const topic = normalizeText(args.topic);
530
+ const profile = normalizeText(args.profile);
531
+ const out = await gateway.testDelivery({
532
+ topic: topic ? topic : undefined,
533
+ text: normalizeText(args.text) || undefined,
534
+ payload: isPlainObject(args.payload) ? args.payload : undefined,
535
+ profile: profile ? profile : undefined,
536
+ });
537
+ return ok(out);
538
+ }
539
+ function resolveAfterSeq(args, state) {
540
+ if (typeof args.after_seq === 'number' && Number.isFinite(args.after_seq)) {
541
+ return Math.max(0, Math.floor(args.after_seq));
542
+ }
543
+ if (args.use_persisted_cursor === false)
544
+ return 0;
545
+ return Math.max(0, Math.floor(state.runtime.last_consumed_stream_seq || 0));
546
+ }
547
+ async function handleEventsClaim(args) {
548
+ const { gateway, state } = await createAuthorizedClients();
549
+ const request = {
550
+ after_seq: resolveAfterSeq(args, state),
551
+ limit: typeof args.limit === 'number' ? Math.max(1, Math.floor(args.limit)) : undefined,
552
+ claim_lease_id: normalizeText(args.claim_lease_id) || state.runtime.last_claim_lease_id || undefined,
553
+ };
554
+ const claimed = await gateway.claimEvents(request);
555
+ await persistState((current) => ({
556
+ ...current,
557
+ runtime: {
558
+ ...current.runtime,
559
+ last_claim_lease_id: claimed.claim_lease_id,
560
+ updated_at: new Date().toISOString(),
561
+ },
562
+ }));
563
+ return ok({
564
+ after_seq: request.after_seq ?? 0,
565
+ ...claimed,
566
+ });
567
+ }
568
+ async function handleEventFetch(args) {
569
+ const { gateway } = await createAuthorizedClients();
570
+ const out = await gateway.fetchEvent(normalizeText(args.event_id));
571
+ return ok(out);
572
+ }
573
+ async function handleEventsAck(args) {
574
+ const { gateway, state } = await createAuthorizedClients();
575
+ const body = {
576
+ acks: Array.isArray(args.acks)
577
+ ? args.acks.map((entry) => {
578
+ const row = normalizeRecord(entry);
579
+ return {
580
+ event_id: normalizeText(row.event_id),
581
+ status: normalizeText(row.status),
582
+ last_error: normalizeText(row.last_error) || undefined,
583
+ result: isPlainObject(row.result) ? row.result : undefined,
584
+ retry_after_ms: typeof row.retry_after_ms === 'number' ? Math.floor(row.retry_after_ms) : undefined,
585
+ };
586
+ })
587
+ : [],
588
+ };
589
+ const acked = await gateway.ackEvents(body);
590
+ const ackStatusById = new Map(body.acks.map((entry) => [entry.event_id, entry.status]));
591
+ const nextCursor = acked.items.reduce((max, item) => {
592
+ return ackStatusById.get(item.event_id) === 'consumed'
593
+ ? Math.max(max, Number(item.stream_seq || 0))
594
+ : max;
595
+ }, Number(state.runtime.last_consumed_stream_seq || 0));
596
+ await persistState((current) => ({
597
+ ...current,
598
+ runtime: {
599
+ ...current.runtime,
600
+ last_consumed_stream_seq: nextCursor,
601
+ updated_at: new Date().toISOString(),
602
+ },
603
+ }));
604
+ return ok({
605
+ next_consumed_cursor: nextCursor,
606
+ ...acked,
607
+ });
608
+ }
609
+ async function handleEventsWait(args) {
610
+ const timeoutMs = typeof args.timeout_ms === 'number' ? Math.max(0, Math.floor(args.timeout_ms)) : 30_000;
611
+ const pollIntervalMs = typeof args.poll_interval_ms === 'number' ? Math.max(250, Math.floor(args.poll_interval_ms)) : 1_500;
612
+ const startedAt = Date.now();
613
+ while (Date.now() - startedAt <= timeoutMs) {
614
+ const claimed = await handleEventsClaim(args);
615
+ const structured = claimed.structuredContent;
616
+ if (isPlainObject(structured) && Array.isArray(structured.items) && structured.items.length > 0) {
617
+ return claimed;
618
+ }
619
+ if (Date.now() - startedAt >= timeoutMs)
620
+ break;
621
+ await sleep(pollIntervalMs);
622
+ }
623
+ return ok({
624
+ ok: true,
625
+ items: [],
626
+ timeout_ms: timeoutMs,
627
+ waited_ms: Date.now() - startedAt,
628
+ });
629
+ }
630
+ async function handleCapabilityTool(name, args) {
631
+ const { platform } = await createAuthorizedClients();
632
+ const capabilities = await loadCapabilities();
633
+ const capability = capabilities.find((item) => item.tool_name === name);
634
+ if (!capability)
635
+ return fail('unknown_hi_capability_tool', { name });
636
+ const out = await platform.callCapability(capability.capability_id, args);
637
+ return ok({
638
+ ok: true,
639
+ capability_id: capability.capability_id,
640
+ tool_name: capability.tool_name,
641
+ result: out.result,
642
+ }, jsonText(out));
643
+ }
644
+ async function handleControlTool(name, args) {
645
+ switch (name) {
646
+ case 'hi_agent_status':
647
+ return await handleStatus(args);
648
+ case 'hi_agent_register':
649
+ return await handleRegister(args);
650
+ case 'hi_agent_connect':
651
+ return await handleConnect(args);
652
+ case 'hi_agent_activate':
653
+ return await handleActivate(args);
654
+ case 'hi_agent_installation_get':
655
+ return await handleInstallationGet();
656
+ case 'hi_agent_installation_update':
657
+ return await handleInstallationUpdate(args);
658
+ case 'hi_agent_endpoints_list':
659
+ return await handleEndpointsList();
660
+ case 'hi_agent_endpoints_upsert':
661
+ return await handleEndpointsUpsert(args);
662
+ case 'hi_agent_subscriptions_list':
663
+ return await handleSubscriptionsList();
664
+ case 'hi_agent_subscriptions_upsert':
665
+ return await handleSubscriptionsUpsert(args);
666
+ case 'hi_agent_test_delivery':
667
+ return await handleTestDelivery(args);
668
+ case 'hi_agent_events_claim':
669
+ return await handleEventsClaim(args);
670
+ case 'hi_agent_event_fetch':
671
+ return await handleEventFetch(args);
672
+ case 'hi_agent_events_ack':
673
+ return await handleEventsAck(args);
674
+ case 'hi_agent_events_wait':
675
+ return await handleEventsWait(args);
676
+ default:
677
+ return fail('unknown_control_tool', { name });
678
+ }
679
+ }
680
+ async function listTools() {
681
+ const capabilities = await loadCapabilities();
682
+ return [
683
+ ...controlTools(),
684
+ ...capabilities.map((capability) => ({
685
+ name: capability.tool_name,
686
+ title: capability.title || capability.tool_name,
687
+ description: capability.description || capability.tool_name,
688
+ // 这里直接复用 Hi public capability schema,确保 MCP 宿主看到的参数语义与平台目录完全一致。
689
+ inputSchema: isPlainObject(capability.parameters)
690
+ ? capability.parameters
691
+ : { type: 'object', properties: {} },
692
+ })),
693
+ ];
694
+ }
695
+ function createMcpServer() {
696
+ const server = new Server({
697
+ name: 'hi-mcp-server',
698
+ version: '0.1.0',
699
+ }, {
700
+ capabilities: {
701
+ tools: {},
702
+ },
703
+ instructions: 'Hi MCP adapter: control-plane tools stay in gateway, business tools mirror Hi public capabilities exactly.',
704
+ });
705
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
706
+ return {
707
+ tools: await listTools(),
708
+ };
709
+ });
710
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
711
+ const name = normalizeText(request.params.name);
712
+ const args = normalizeRecord(request.params.arguments);
713
+ try {
714
+ const controlToolNames = new Set(controlTools().map((tool) => tool.name));
715
+ return controlToolNames.has(name)
716
+ ? await handleControlTool(name, args)
717
+ : await handleCapabilityTool(name, args);
718
+ }
719
+ catch (error) {
720
+ return fail(normalizeText(error?.message) || 'tool_call_failed', isPlainObject(error?.detail) ? error.detail : { message: String(error?.message || error || 'unknown_error') });
721
+ }
722
+ });
723
+ return server;
724
+ }
725
+ async function runHttpServer() {
726
+ const app = createMcpExpressApp({ host: config.host });
727
+ app.get('/healthz', (_req, res) => {
728
+ res.json({
729
+ ok: true,
730
+ profile: config.profile,
731
+ transport: config.transport,
732
+ });
733
+ });
734
+ // readiness 必须确认上游 discovery 可达;否则 pod 虽然活着,但实际上没法服务真实 MCP 请求。
735
+ app.get('/readyz', async (_req, res) => {
736
+ try {
737
+ const wellKnown = await loadWellKnown();
738
+ res.json({
739
+ ok: true,
740
+ profile: config.profile,
741
+ transport: config.transport,
742
+ registry_base_url: wellKnown.platform.registry_base_url,
743
+ });
744
+ }
745
+ catch (error) {
746
+ res.status(503).json({
747
+ ok: false,
748
+ error: normalizeText(error?.message) || 'ready_check_failed',
749
+ });
750
+ }
751
+ });
752
+ app.post('/mcp', async (req, res) => {
753
+ const server = createMcpServer();
754
+ const transport = new StreamableHTTPServerTransport({
755
+ sessionIdGenerator: undefined,
756
+ });
757
+ try {
758
+ await server.connect(transport);
759
+ await transport.handleRequest(req, res, req.body);
760
+ res.on('close', () => {
761
+ void transport.close().catch(() => undefined);
762
+ void server.close().catch(() => undefined);
763
+ });
764
+ }
765
+ catch (error) {
766
+ if (!res.headersSent) {
767
+ res.status(500).json({
768
+ jsonrpc: '2.0',
769
+ error: {
770
+ code: -32603,
771
+ message: normalizeText(error?.message) || 'internal_server_error',
772
+ },
773
+ id: null,
774
+ });
775
+ }
776
+ }
777
+ });
778
+ app.get('/mcp', (_req, res) => {
779
+ res.status(405).json({
780
+ jsonrpc: '2.0',
781
+ error: {
782
+ code: -32000,
783
+ message: 'Method not allowed.',
784
+ },
785
+ id: null,
786
+ });
787
+ });
788
+ app.delete('/mcp', (_req, res) => {
789
+ res.status(405).json({
790
+ jsonrpc: '2.0',
791
+ error: {
792
+ code: -32000,
793
+ message: 'Method not allowed.',
794
+ },
795
+ id: null,
796
+ });
797
+ });
798
+ const httpServer = createServer(app);
799
+ await new Promise((resolve) => {
800
+ httpServer.listen(config.port, config.host, () => resolve());
801
+ });
802
+ const shutdown = async () => {
803
+ await new Promise((resolve) => {
804
+ httpServer.close(() => resolve());
805
+ });
806
+ process.exit(0);
807
+ };
808
+ process.on('SIGINT', () => {
809
+ void shutdown();
810
+ });
811
+ process.on('SIGTERM', () => {
812
+ void shutdown();
813
+ });
814
+ console.log(`hi-mcp-server listening on http://${config.host}:${config.port}/mcp`);
815
+ }
816
+ async function runStdioServer() {
817
+ const server = createMcpServer();
818
+ const transport = new StdioServerTransport();
819
+ await server.connect(transport);
820
+ process.on('SIGINT', () => {
821
+ void server.close().catch(() => undefined);
822
+ });
823
+ process.on('SIGTERM', () => {
824
+ void server.close().catch(() => undefined);
825
+ });
826
+ }
827
+ async function main() {
828
+ assertConfig();
829
+ if (config.transport === 'stdio') {
830
+ await runStdioServer();
831
+ return;
832
+ }
833
+ await runHttpServer();
834
+ }
835
+ main().catch((error) => {
836
+ console.error(error);
837
+ process.exit(1);
838
+ });
@@ -0,0 +1,51 @@
1
+ export type HiAgentIdentityState = {
2
+ agent_id: string;
3
+ installation_id: string;
4
+ display_name: string;
5
+ agent_kind: string;
6
+ client_id: string;
7
+ client_secret: string;
8
+ installation_subject: string;
9
+ issuer: string;
10
+ audience: string;
11
+ token_url: string;
12
+ jwks_url: string;
13
+ activated_at: string | null;
14
+ delivery_capabilities: Record<string, unknown> | null;
15
+ };
16
+ export type HiAgentPlatformState = {
17
+ platform_base_url: string;
18
+ registry_base_url: string;
19
+ fetched_at: string;
20
+ };
21
+ export type HiAgentRuntimeState = {
22
+ last_consumed_stream_seq: number;
23
+ last_claim_lease_id: string | null;
24
+ updated_at: string | null;
25
+ };
26
+ export type HiAgentPersistedState = {
27
+ profile: string;
28
+ platform: HiAgentPlatformState | null;
29
+ identity: HiAgentIdentityState | null;
30
+ runtime: HiAgentRuntimeState;
31
+ };
32
+ export declare function buildDefaultState(profile: string): HiAgentPersistedState;
33
+ export declare function resolveStateFile(args: {
34
+ stateDir: string;
35
+ profile: string;
36
+ }): string;
37
+ export declare function readState(args: {
38
+ stateDir: string;
39
+ profile: string;
40
+ }): Promise<HiAgentPersistedState>;
41
+ export declare function writeState(args: {
42
+ stateDir: string;
43
+ profile: string;
44
+ state: HiAgentPersistedState;
45
+ }): Promise<void>;
46
+ export declare function updateState(args: {
47
+ stateDir: string;
48
+ profile: string;
49
+ updater: (current: HiAgentPersistedState) => HiAgentPersistedState;
50
+ }): Promise<HiAgentPersistedState>;
51
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,wBAAwB,EAAE,MAAM,CAAC;IACjC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACtC,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC;AAQF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAOxE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,UAG3E;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmBjC;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,qBAAqB,CAAC;CAC9B,iBAIA;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,qBAAqB,CAAC;CACpE,kCASA"}
package/dist/state.js ADDED
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const EMPTY_RUNTIME_STATE = {
4
+ last_consumed_stream_seq: 0,
5
+ last_claim_lease_id: null,
6
+ updated_at: null,
7
+ };
8
+ export function buildDefaultState(profile) {
9
+ return {
10
+ profile,
11
+ platform: null,
12
+ identity: null,
13
+ runtime: { ...EMPTY_RUNTIME_STATE },
14
+ };
15
+ }
16
+ export function resolveStateFile(args) {
17
+ // state 文件默认按 profile 分片,方便同一台机器上挂多个宿主 installation。
18
+ return path.join(args.stateDir, `${args.profile}.json`);
19
+ }
20
+ export async function readState(args) {
21
+ const filePath = resolveStateFile(args);
22
+ try {
23
+ const raw = await fs.readFile(filePath, 'utf8');
24
+ const parsed = JSON.parse(raw);
25
+ return {
26
+ profile: typeof parsed.profile === 'string' && parsed.profile.trim() ? parsed.profile : args.profile,
27
+ platform: parsed.platform ?? null,
28
+ identity: parsed.identity ?? null,
29
+ runtime: {
30
+ last_consumed_stream_seq: Number(parsed.runtime?.last_consumed_stream_seq || 0),
31
+ last_claim_lease_id: parsed.runtime?.last_claim_lease_id ?? null,
32
+ updated_at: parsed.runtime?.updated_at ?? null,
33
+ },
34
+ };
35
+ }
36
+ catch (error) {
37
+ if (error?.code === 'ENOENT')
38
+ return buildDefaultState(args.profile);
39
+ throw error;
40
+ }
41
+ }
42
+ export async function writeState(args) {
43
+ const filePath = resolveStateFile(args);
44
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
45
+ await fs.writeFile(filePath, `${JSON.stringify(args.state, null, 2)}\n`, 'utf8');
46
+ }
47
+ export async function updateState(args) {
48
+ const current = await readState(args);
49
+ const next = args.updater(current);
50
+ await writeState({
51
+ stateDir: args.stateDir,
52
+ profile: args.profile,
53
+ state: next,
54
+ });
55
+ return next;
56
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@hirey/hi-mcp-server",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "dist/server.js",
7
+ "types": "dist/server.d.ts",
8
+ "files": [
9
+ "dist/",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/server.d.ts",
15
+ "default": "./dist/server.js"
16
+ }
17
+ },
18
+ "bin": {
19
+ "hi-mcp-server": "dist/server.js"
20
+ },
21
+ "scripts": {
22
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
23
+ "dev": "tsx watch src/server.ts",
24
+ "build": "npm run clean && tsc -p tsconfig.json",
25
+ "start": "node dist/server.js",
26
+ "test": "tsx --test src/**/*.test.ts",
27
+ "prepack": "npm run build"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "dependencies": {
33
+ "@hirey/hi-agent-contracts": "^0.1.8",
34
+ "@hirey/hi-agent-sdk": "^0.1.4",
35
+ "@modelcontextprotocol/sdk": "^1.29.0",
36
+ "express": "^5.2.1",
37
+ "zod": "^4.3.6"
38
+ },
39
+ "devDependencies": {
40
+ "@types/express": "^5.0.6",
41
+ "@types/node": "^20.19.37",
42
+ "tsx": "^4.21.0",
43
+ "typescript": "^5.9.3"
44
+ }
45
+ }