@hirey/hi-mcp-server 0.1.19 → 0.1.25

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.
@@ -1,3 +1,40 @@
1
+ // 跨大洋 / GFW 干扰 / NLB TLS handshake 偶发 reset 等 transient 网络层故障在国内
2
+ // agent host 上很常见(实测从大陆访问 us-east-2 prod ELB 单次 SSL handshake 成功率
3
+ // 约 30%)。下面这个 retry 配置目标是把 5 次独立 transient 故障的累计失败率降到
4
+ // 单次 30% → ~0.2%(几何级别),覆盖单次 install 流程内多次 fetch(register、
5
+ // activate、token、updateInstallation、subscriptions 等)整体跑得通。
6
+ //
7
+ // 重试条件:
8
+ // - fetch throw(globalThis.fetch 在跨域 SSL fail / DNS / ECONNRESET / ETIMEDOUT 等
9
+ // 上层都把 error 包成 'fetch failed',这类视为 transient)
10
+ // - HTTP 5xx
11
+ // - HTTP 429(服务端显式让重试)
12
+ // 不重试:
13
+ // - 其它 4xx(业务错误,重试无意义且会触发 ON CONFLICT 等副作用)
14
+ const DEFAULT_FETCH_RETRY_ATTEMPTS = 5;
15
+ const DEFAULT_FETCH_RETRY_BASE_MS = 400;
16
+ const DEFAULT_FETCH_RETRY_MAX_MS = 8000;
17
+ function isTransientFetchError(err) {
18
+ if (!err)
19
+ return false;
20
+ const message = String(err.message || err);
21
+ if (/fetch failed|network|ECONN|ETIMEDOUT|EAI_AGAIN|UND_ERR|socket hang up|TLS|SSL/i.test(message)) {
22
+ return true;
23
+ }
24
+ // Node undici fetch 的 error chain 在 cause 里
25
+ const cause = err.cause;
26
+ if (cause && cause !== err)
27
+ return isTransientFetchError(cause);
28
+ return false;
29
+ }
30
+ function computeBackoffMs(attempt) {
31
+ const expo = Math.min(DEFAULT_FETCH_RETRY_MAX_MS, DEFAULT_FETCH_RETRY_BASE_MS * 2 ** attempt);
32
+ // full jitter:[0, expo),避免多个 client 同步震荡 retry 把 backend 打垮
33
+ return Math.floor(Math.random() * expo);
34
+ }
35
+ async function sleep(ms) {
36
+ await new Promise((r) => setTimeout(r, ms));
37
+ }
1
38
  class BaseHiClient {
2
39
  baseUrl;
3
40
  token;
@@ -9,7 +46,8 @@ class BaseHiClient {
9
46
  }
10
47
  async request(path, options = {}) {
11
48
  const hasBody = options.body != null;
12
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
49
+ const url = `${this.baseUrl}${path}`;
50
+ const init = {
13
51
  method: options.method || 'GET',
14
52
  headers: {
15
53
  ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
@@ -17,7 +55,32 @@ class BaseHiClient {
17
55
  ...(options.headers || {}),
18
56
  },
19
57
  body: hasBody ? JSON.stringify(options.body) : undefined,
20
- });
58
+ };
59
+ let lastErr;
60
+ let response = null;
61
+ for (let attempt = 0; attempt < DEFAULT_FETCH_RETRY_ATTEMPTS; attempt += 1) {
62
+ try {
63
+ response = await this.fetchImpl(url, init);
64
+ // 5xx / 429 触发重试;4xx 其余 fall through 给业务错误处理。
65
+ if (response.status >= 500 || response.status === 429) {
66
+ if (attempt < DEFAULT_FETCH_RETRY_ATTEMPTS - 1) {
67
+ await sleep(computeBackoffMs(attempt));
68
+ continue;
69
+ }
70
+ }
71
+ break;
72
+ }
73
+ catch (err) {
74
+ lastErr = err;
75
+ if (!isTransientFetchError(err) || attempt >= DEFAULT_FETCH_RETRY_ATTEMPTS - 1) {
76
+ throw err;
77
+ }
78
+ await sleep(computeBackoffMs(attempt));
79
+ }
80
+ }
81
+ if (!response) {
82
+ throw lastErr || new Error('fetch failed');
83
+ }
21
84
  if (options.responseType === 'text') {
22
85
  const text = await response.text();
23
86
  if (!response.ok) {
@@ -150,17 +213,41 @@ export class HiAgentGatewayClient extends BaseHiClient {
150
213
  }
151
214
  export async function exchangeHiAgentClientCredentialsToken(options) {
152
215
  const fetchImpl = options.fetchImpl || fetch;
153
- const response = await fetchImpl(options.tokenUrl, {
216
+ // BaseHiClient.request 同样的 retry 策略:transient network / 5xx / 429 重试,
217
+ // 4xx 其它原样 throw。token exchange 第一次 SSL handshake 经常被跨大洋路径打掉。
218
+ const init = {
154
219
  method: 'POST',
155
- headers: {
156
- 'content-type': 'application/json',
157
- },
220
+ headers: { 'content-type': 'application/json' },
158
221
  body: JSON.stringify({
159
222
  grant_type: 'client_credentials',
160
223
  client_id: options.clientId,
161
224
  client_secret: options.clientSecret,
162
225
  }),
163
- });
226
+ };
227
+ let response = null;
228
+ let lastErr;
229
+ for (let attempt = 0; attempt < DEFAULT_FETCH_RETRY_ATTEMPTS; attempt += 1) {
230
+ try {
231
+ response = await fetchImpl(options.tokenUrl, init);
232
+ if (response.status >= 500 || response.status === 429) {
233
+ if (attempt < DEFAULT_FETCH_RETRY_ATTEMPTS - 1) {
234
+ await sleep(computeBackoffMs(attempt));
235
+ continue;
236
+ }
237
+ }
238
+ break;
239
+ }
240
+ catch (err) {
241
+ lastErr = err;
242
+ if (!isTransientFetchError(err) || attempt >= DEFAULT_FETCH_RETRY_ATTEMPTS - 1) {
243
+ throw err;
244
+ }
245
+ await sleep(computeBackoffMs(attempt));
246
+ }
247
+ }
248
+ if (!response) {
249
+ throw lastErr || new Error('fetch failed');
250
+ }
164
251
  const body = await response.json().catch(() => ({}));
165
252
  if (!response.ok) {
166
253
  const error = new Error(String(body?.error || response.statusText || 'request_failed'));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirey/hi-agent-sdk",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@hirey/hi-agent-contracts": "^0.1.14"
28
+ "@hirey/hi-agent-contracts": "^0.1.15"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.11.30",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hirey/hi-mcp-server",
3
- "version": "0.1.19",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@hirey/hi-agent-contracts": "^0.1.14",
38
- "@hirey/hi-agent-sdk": "^0.1.10",
37
+ "@hirey/hi-agent-contracts": "^0.1.16",
38
+ "@hirey/hi-agent-sdk": "^0.1.12",
39
39
  "@modelcontextprotocol/sdk": "^1.29.0",
40
40
  "express": "^5.2.1",
41
41
  "zod": "^4.3.6"