huaweicloud-devkit 1.1.2-next.1 → 1.1.2-next.2
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 +24 -6
- package/README.zh-CN.md +24 -6
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/src/mcp-protocol.mjs +88 -0
- package/plugins/huaweicloud-core/src/mcp-server-remote.mjs +107 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +125 -183
- package/plugins/huaweicloud-core/src/setup-cli.mjs +1 -1
package/README.md
CHANGED
|
@@ -11,12 +11,6 @@ Help AI coding agents use Huawei Cloud safely and accurately — a single integr
|
|
|
11
11
|
|
|
12
12
|
Supports OpenCode, Codex, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, Hermes, OpenClaw, and AtomCode.
|
|
13
13
|
|
|
14
|
-
## Cursor Directory Plugin
|
|
15
|
-
|
|
16
|
-
This repository also acts as an [Open Plugins](https://open-plugins.com) package for the [Cursor Directory](https://cursor.directory) marketplace. The root-level `plugin.json`, `mcp.json`, `skills/`, and `rules/` files are used **only** for Cursor Directory repository-scan discovery and are **not** shipped in the npm tarball (the `package.json` `files` whitelist excludes them). The npm package's manifests live under `plugins/huaweicloud-core/`.
|
|
17
|
-
|
|
18
|
-
The `mcp.json` MCP server launches via `npx -y -p huaweicloud-devkit huaweicloud-devkit-mcp`, which tracks the npm `latest` tag. During pre-release this may differ from the manifest `version`; it can be pinned after a stable release.
|
|
19
|
-
|
|
20
14
|
## Prerequisites
|
|
21
15
|
|
|
22
16
|
- Node.js >= 22
|
|
@@ -226,6 +220,30 @@ No installation required — `npx` handles everything.
|
|
|
226
220
|
|
|
227
221
|
> Set `HW_ACCESS_KEY`/`HW_SECRET_KEY` in the MCP config `env` field for project-level credentials.
|
|
228
222
|
|
|
223
|
+
#### Connecting over Remote (HTTP)
|
|
224
|
+
|
|
225
|
+
If your agent supports `type: "remote"` (Streamable HTTP) instead of stdio, start the devkit remote MCP server locally first:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npx --yes huaweicloud-devkit-mcp --transport remote
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
It listens on `127.0.0.1:9528` by default (no conflict with the IACMCPServer port 9527). Then connect with a remote config (opencode example):
|
|
232
|
+
|
|
233
|
+
```jsonc
|
|
234
|
+
{
|
|
235
|
+
"mcp": {
|
|
236
|
+
"huaweicloud-devkit": {
|
|
237
|
+
"type": "remote",
|
|
238
|
+
"url": "http://localhost:9528",
|
|
239
|
+
"enabled": true,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
> Use `--port <port>` if 9528 is taken and update `url` accordingly; add `--host 0.0.0.0` for LAN access. The remote server has no built-in auth — do not expose it anonymously to the public internet.
|
|
246
|
+
|
|
229
247
|
### Install KooCLI
|
|
230
248
|
|
|
231
249
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -11,12 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
支持 OpenCode、Codex、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、Hermes、OpenClaw、AtomCode。
|
|
13
13
|
|
|
14
|
-
## Cursor Directory 插件
|
|
15
|
-
|
|
16
|
-
本仓库同时作为 [Open Plugins](https://open-plugins.com) 包挂载到 [Cursor Directory](https://cursor.directory) 市场。根目录的 `plugin.json`、`mcp.json`、`skills/`、`rules/` 仅用于 Cursor Directory 的仓库扫描发现,**不会**打入 npm 包(`package.json` 的 `files` 白名单未包含它们)。npm 包的 manifests 位于 `plugins/huaweicloud-core/`。
|
|
17
|
-
|
|
18
|
-
`mcp.json` 中的 MCP 服务通过 `npx -y -p huaweicloud-devkit huaweicloud-devkit-mcp` 启动,跟踪的是 npm 的 `latest` tag。预发布阶段可能与 manifest 中的 `version` 不一致,稳定发版后可以固定版本。
|
|
19
|
-
|
|
20
14
|
## 前置条件
|
|
21
15
|
|
|
22
16
|
- Node.js >= 22
|
|
@@ -226,6 +220,30 @@ npx --yes huaweicloud-devkit uninstall --target atomcode
|
|
|
226
220
|
|
|
227
221
|
> 项目级 AK/SK 可通过 MCP 配置的 `env` 字段设置 `HW_ACCESS_KEY`/`HW_SECRET_KEY`。
|
|
228
222
|
|
|
223
|
+
#### 通过 Remote(HTTP)连接
|
|
224
|
+
|
|
225
|
+
若 Agent 支持 `type: "remote"`(Streamable HTTP)而非 stdio,可在本地先启动 devkit 的远程 MCP 服务器:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npx --yes huaweicloud-devkit-mcp --transport remote
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
默认监听 `127.0.0.1:9528`(与预置的 IACMCPServer 端口 9527 不冲突)。随后以远程方式连接(以 opencode 为例):
|
|
232
|
+
|
|
233
|
+
```jsonc
|
|
234
|
+
{
|
|
235
|
+
"mcp": {
|
|
236
|
+
"huaweicloud-devkit": {
|
|
237
|
+
"type": "remote",
|
|
238
|
+
"url": "http://localhost:9528",
|
|
239
|
+
"enabled": true,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
> 端口被占用时用 `--port <端口>` 换端口,`url` 同步修改;需要局域网访问时加 `--host 0.0.0.0`。remote 服务器不内置鉴权,请勿匿名暴露到公网。
|
|
246
|
+
|
|
229
247
|
### 安装 KooCLI
|
|
230
248
|
|
|
231
249
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.2-next.
|
|
3
|
+
"version": "1.1.2-next.2",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.2-next.
|
|
20
|
+
"version": "1.1.2-next.2",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.2-next.
|
|
23
|
+
"version": "1.1.2-next.2",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.2-next.
|
|
20
|
+
"version": "1.1.2-next.2",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.2-next.
|
|
3
|
+
"version": "1.1.2-next.2",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.2-next.
|
|
3
|
+
"version": "1.1.2-next.2",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.2-next.
|
|
5
|
+
"version": "1.1.2-next.2",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
|
|
6
|
+
import { peekCachedUpdateInfo, applyUpdateHint } from './update-check.mjs';
|
|
7
|
+
import { initTelemetry } from './telemetry/telemetry.mjs';
|
|
8
|
+
import { detectAgent } from './telemetry/agent-detect.mjs';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pluginRoot = resolve(__dirname, '..');
|
|
12
|
+
const packageRoot = resolve(pluginRoot, '..', '..');
|
|
13
|
+
let pkgVersion = '0.0.0';
|
|
14
|
+
for (const base of [pluginRoot, packageRoot]) {
|
|
15
|
+
try {
|
|
16
|
+
const version = JSON.parse(readFileSync(join(base, 'package.json'), 'utf8')).version;
|
|
17
|
+
if (version) {
|
|
18
|
+
pkgVersion = version;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 会话内首个非 check/upgrade 工具调用附加 _updateInfo,只消费一次。
|
|
25
|
+
let hintConsumed = false;
|
|
26
|
+
function decorateResult(name, result) {
|
|
27
|
+
if (hintConsumed) return result;
|
|
28
|
+
try {
|
|
29
|
+
const hint = peekCachedUpdateInfo();
|
|
30
|
+
if (!hint) return result;
|
|
31
|
+
const decorated = applyUpdateHint(result, name, hint);
|
|
32
|
+
if (decorated !== result) hintConsumed = true;
|
|
33
|
+
return decorated;
|
|
34
|
+
} catch {
|
|
35
|
+
return result; // 兜底装饰失败绝不影响工具调用
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function dispatch(method, params) {
|
|
40
|
+
if (method === 'initialize') {
|
|
41
|
+
const ci = params.clientInfo || {};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const { hdkitGenerateUserHash } = await import('./sandbox/hdkitservice-api.mjs');
|
|
45
|
+
await Promise.race([
|
|
46
|
+
hdkitGenerateUserHash(),
|
|
47
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
|
48
|
+
]);
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
const agent = detectAgent(ci);
|
|
52
|
+
initTelemetry({ harness: agent.harness, version: agent.version });
|
|
53
|
+
return {
|
|
54
|
+
protocolVersion: params.protocolVersion || '2024-11-05',
|
|
55
|
+
capabilities: {
|
|
56
|
+
tools: {},
|
|
57
|
+
},
|
|
58
|
+
serverInfo: {
|
|
59
|
+
name: 'huaweicloud-devkit',
|
|
60
|
+
version: pkgVersion,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (method === 'tools/list') {
|
|
66
|
+
return { tools: TOOL_DEFINITIONS };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (method === 'tools/call') {
|
|
70
|
+
const result = await callTool(params.name, params.arguments || {});
|
|
71
|
+
const decorated = decorateResult(params.name, result);
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: JSON.stringify(decorated, null, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
isError: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (method === 'resources/list') {
|
|
84
|
+
return { resources: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(`Unsupported method: ${method}`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { format } from 'node:util';
|
|
5
|
+
|
|
6
|
+
import { dispatch } from './mcp-protocol.mjs';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_PORT = 9528;
|
|
9
|
+
export const DEFAULT_HOST = '127.0.0.1';
|
|
10
|
+
|
|
11
|
+
export async function startRemoteServer({ port = DEFAULT_PORT, host = DEFAULT_HOST } = {}) {
|
|
12
|
+
const server = createServer(async (req, res) => {
|
|
13
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
14
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
15
|
+
res.setHeader(
|
|
16
|
+
'Access-Control-Allow-Headers',
|
|
17
|
+
'Content-Type, MCP-Protocol-Version, Mcp-Session-Id, Accept, Authorization',
|
|
18
|
+
);
|
|
19
|
+
res.setHeader('Access-Control-Expose-Headers', 'MCP-Protocol-Version, Mcp-Session-Id');
|
|
20
|
+
|
|
21
|
+
if (req.method === 'OPTIONS') {
|
|
22
|
+
res.writeHead(204);
|
|
23
|
+
res.end();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (req.method !== 'POST') {
|
|
28
|
+
res.writeHead(405, { Allow: 'POST, OPTIONS' });
|
|
29
|
+
res.end();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let message;
|
|
34
|
+
try {
|
|
35
|
+
const chunks = [];
|
|
36
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
37
|
+
message = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
38
|
+
} catch {
|
|
39
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
40
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!Object.hasOwn(message, 'id')) {
|
|
45
|
+
// 通知类消息(含 notifications/initialized)无需响应体,HTTP 层直接 202。
|
|
46
|
+
res.writeHead(202);
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let response;
|
|
52
|
+
try {
|
|
53
|
+
const result = await dispatch(message.method, message.params || {});
|
|
54
|
+
response = { jsonrpc: '2.0', id: message.id, result };
|
|
55
|
+
if (message.method === 'initialize') {
|
|
56
|
+
res.setHeader('MCP-Protocol-Version', result.protocolVersion || '2024-11-05');
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
response = {
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
id: message.id,
|
|
62
|
+
error: { code: -32603, message: error.message },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
writeMCPResponse(res, response, req.headers.accept || '');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
70
|
+
server.once('error', rejectPromise);
|
|
71
|
+
server.listen(port, host, () => resolvePromise());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const address = server.address();
|
|
75
|
+
process.stdout.write(format('huaweicloud-devkit MCP server (remote) listening on %s:%s\n', host, address.port));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
server,
|
|
79
|
+
port: address.port,
|
|
80
|
+
close: () => new Promise((resolvePromise) => server.close(resolvePromise)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeMCPResponse(res, response, accept) {
|
|
85
|
+
const json = JSON.stringify(response);
|
|
86
|
+
if (accept.includes('application/json')) {
|
|
87
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
88
|
+
res.end(json);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// 客户端只接受 SSE 时的兜底:单帧 event 后关闭流。
|
|
92
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
|
|
93
|
+
res.write(`event: message\ndata: ${json}\n\n`);
|
|
94
|
+
res.end();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
98
|
+
const portIdx = process.argv.indexOf('--port');
|
|
99
|
+
const port = portIdx > -1 ? Number(process.argv[portIdx + 1]) : DEFAULT_PORT;
|
|
100
|
+
const hostIdx = process.argv.indexOf('--host');
|
|
101
|
+
const host = hostIdx > -1 && process.argv[hostIdx + 1] ? process.argv[hostIdx + 1] : DEFAULT_HOST;
|
|
102
|
+
startRemoteServer({ port, host }).catch((error) => {
|
|
103
|
+
process.stderr.write(`Failed to start MCP remote server: ${error.message}\n`);
|
|
104
|
+
// eslint-disable-next-line n/no-process-exit -- fatal startup error in standalone CLI mode
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { stdin, stdout } from 'node:process';
|
|
3
|
-
import { rmSync, existsSync
|
|
4
|
-
import { resolve, dirname
|
|
3
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
5
|
import { platform } from 'node:os';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { dispatch } from './mcp-protocol.mjs';
|
|
9
|
+
import { DEFAULT_PORT, DEFAULT_HOST } from './mcp-server-remote.mjs';
|
|
10
|
+
import { getCachedUpdateInfo, readInstalledVersion } from './update-check.mjs';
|
|
11
11
|
import { detectAgent } from './telemetry/agent-detect.mjs';
|
|
12
12
|
|
|
13
|
+
const transportIdx = process.argv.indexOf('--transport');
|
|
14
|
+
const transport = transportIdx > -1 && process.argv[transportIdx + 1] ? process.argv[transportIdx + 1] : 'stdio';
|
|
15
|
+
const portIdx = process.argv.indexOf('--port');
|
|
16
|
+
const remotePort = portIdx > -1 ? Number(process.argv[portIdx + 1]) : DEFAULT_PORT;
|
|
17
|
+
const hostIdx = process.argv.indexOf('--host');
|
|
18
|
+
const remoteHost = hostIdx > -1 && process.argv[hostIdx + 1] ? process.argv[hostIdx + 1] : DEFAULT_HOST;
|
|
19
|
+
|
|
13
20
|
const projectDirIdx = process.argv.indexOf('--codearts-project-dir');
|
|
14
21
|
if (projectDirIdx > -1 && process.argv[projectDirIdx + 1]) {
|
|
15
22
|
process.env.CODEARTS_PROJECT_DIR = process.argv[projectDirIdx + 1];
|
|
@@ -46,197 +53,132 @@ try {
|
|
|
46
53
|
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
47
54
|
} catch {}
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
} catch {}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let buffer = Buffer.alloc(0);
|
|
64
|
-
let useContentLengthFraming = true;
|
|
65
|
-
|
|
66
|
-
// Keep the event loop alive after stdin is closed (Windows Hermes workaround).
|
|
67
|
-
// Node.js exits when no active handles remain; the stdin 'data' listener is
|
|
68
|
-
// the only handle. On Windows, Hermes may close the stdin pipe after the
|
|
69
|
-
// initial handshake, causing the process to exit silently (exit 0).
|
|
70
|
-
//
|
|
71
|
-
// For Hermes on Windows: start a keepalive timer on stdin close, and only exit
|
|
72
|
-
// when stdout also closes.
|
|
73
|
-
// For all other agents (OfficeAce, WorkBuddy, etc.): stdin close is the
|
|
74
|
-
// shutdown signal — exit cleanly so the host does not see CLOSE_TIMEOUT.
|
|
75
|
-
const { harness } = detectAgent();
|
|
76
|
-
const NEEDS_KEEPALIVE = harness === 'hermes' && platform() === 'win32';
|
|
77
|
-
|
|
78
|
-
// 版本升级检测预热:异步、非阻塞;失败静默(离线/超时不影响会话)。
|
|
79
|
-
process.nextTick(() => {
|
|
80
|
-
getCachedUpdateInfo(readInstalledVersion() || '0.0.0').catch(() => {});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// 会话内首个非 check/upgrade 工具调用附加 _updateInfo,只消费一次。
|
|
84
|
-
let hintConsumed = false;
|
|
85
|
-
function decorateResult(name, result) {
|
|
86
|
-
if (hintConsumed) return result;
|
|
87
|
-
try {
|
|
88
|
-
const hint = peekCachedUpdateInfo();
|
|
89
|
-
if (!hint) return result;
|
|
90
|
-
const decorated = applyUpdateHint(result, name, hint);
|
|
91
|
-
if (decorated !== result) hintConsumed = true;
|
|
92
|
-
return decorated;
|
|
93
|
-
} catch {
|
|
94
|
-
return result; // 兜底装饰失败绝不影响工具调用
|
|
95
|
-
}
|
|
56
|
+
if (transport === 'remote') {
|
|
57
|
+
const { startRemoteServer } = await import('./mcp-server-remote.mjs');
|
|
58
|
+
startRemoteServer({ port: remotePort, host: remoteHost }).catch((error) => {
|
|
59
|
+
process.stderr.write(`Failed to start MCP remote server: ${error.message}\n`);
|
|
60
|
+
// eslint-disable-next-line n/no-process-exit -- fatal startup error; no active session to keep alive
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
runStdioServer();
|
|
96
65
|
}
|
|
97
66
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
67
|
+
function runStdioServer() {
|
|
68
|
+
const updatePrewarm = () => {
|
|
69
|
+
getCachedUpdateInfo(readInstalledVersion() || '0.0.0').catch(() => {});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let buffer = Buffer.alloc(0);
|
|
73
|
+
let useContentLengthFraming = true;
|
|
74
|
+
|
|
75
|
+
// Keep the event loop alive after stdin is closed (Windows Hermes workaround).
|
|
76
|
+
// Node.js exits when no active handles remain; the stdin 'data' listener is
|
|
77
|
+
// the only handle. On Windows, Hermes may close the stdin pipe after the
|
|
78
|
+
// initial handshake, causing the process to exit silently (exit 0).
|
|
79
|
+
//
|
|
80
|
+
// For Hermes on Windows: start a keepalive timer on stdin close, and only exit
|
|
81
|
+
// when stdout also closes.
|
|
82
|
+
// For all other agents (OfficeAce, WorkBuddy, etc.): stdin close is the
|
|
83
|
+
// shutdown signal — exit cleanly so the host does not see CLOSE_TIMEOUT.
|
|
84
|
+
const { harness } = detectAgent();
|
|
85
|
+
const NEEDS_KEEPALIVE = harness === 'hermes' && platform() === 'win32';
|
|
86
|
+
|
|
87
|
+
// 版本升级检测预热:异步、非阻塞;失败静默(离线/超时不影响会话)。
|
|
88
|
+
process.nextTick(updatePrewarm);
|
|
89
|
+
|
|
90
|
+
let keepAlive = null;
|
|
91
|
+
function onStdinClose() {
|
|
92
|
+
if (keepAlive) return;
|
|
93
|
+
if (NEEDS_KEEPALIVE) {
|
|
94
|
+
keepAlive = setInterval(() => {}, 60000);
|
|
95
|
+
} else {
|
|
96
|
+
// eslint-disable-next-line n/no-process-exit -- stdin close is the shutdown signal; exit now without waiting for stdout
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
112
99
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
stdout.on('close', onStdoutClose);
|
|
118
|
-
|
|
119
|
-
stdin.on('data', (chunk) => {
|
|
120
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
121
|
-
readFrames();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
function readFrames() {
|
|
125
|
-
while (true) {
|
|
126
|
-
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
127
|
-
if (headerEnd !== -1) {
|
|
128
|
-
useContentLengthFraming = true;
|
|
129
|
-
const consumed = parseContentLengthFrame(headerEnd);
|
|
130
|
-
if (!consumed) return;
|
|
131
|
-
continue;
|
|
100
|
+
function onStdoutClose() {
|
|
101
|
+
if (keepAlive) {
|
|
102
|
+
clearInterval(keepAlive);
|
|
103
|
+
keepAlive = null;
|
|
132
104
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
105
|
+
process.exitCode = 0;
|
|
106
|
+
}
|
|
107
|
+
stdin.on('close', onStdinClose);
|
|
108
|
+
stdin.on('end', onStdinClose);
|
|
109
|
+
stdout.on('close', onStdoutClose);
|
|
110
|
+
|
|
111
|
+
stdin.on('data', (chunk) => {
|
|
112
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
113
|
+
readFrames();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function readFrames() {
|
|
117
|
+
while (true) {
|
|
118
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
119
|
+
if (headerEnd !== -1) {
|
|
120
|
+
useContentLengthFraming = true;
|
|
121
|
+
const consumed = parseContentLengthFrame(headerEnd);
|
|
122
|
+
if (!consumed) return;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const lf = buffer.indexOf('\n');
|
|
127
|
+
if (lf !== -1) {
|
|
128
|
+
useContentLengthFraming = false;
|
|
129
|
+
const line = buffer.subarray(0, lf).toString('utf8').trim();
|
|
130
|
+
buffer = buffer.subarray(lf + 1);
|
|
131
|
+
if (line) void handleMessage(JSON.parse(line));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return;
|
|
141
136
|
}
|
|
142
|
-
|
|
143
|
-
return;
|
|
144
137
|
}
|
|
145
|
-
}
|
|
146
138
|
|
|
147
|
-
function parseContentLengthFrame(headerEnd) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
function parseContentLengthFrame(headerEnd) {
|
|
140
|
+
const header = buffer.subarray(0, headerEnd).toString('utf8');
|
|
141
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
142
|
+
if (!match) {
|
|
143
|
+
buffer = Buffer.alloc(0);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const length = Number(match[1]);
|
|
147
|
+
const bodyStart = headerEnd + 4;
|
|
148
|
+
const bodyEnd = bodyStart + length;
|
|
149
|
+
if (buffer.length < bodyEnd) return false;
|
|
150
|
+
const body = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
151
|
+
buffer = buffer.subarray(bodyEnd);
|
|
152
|
+
void handleMessage(JSON.parse(body));
|
|
152
153
|
return true;
|
|
153
154
|
}
|
|
154
|
-
const length = Number(match[1]);
|
|
155
|
-
const bodyStart = headerEnd + 4;
|
|
156
|
-
const bodyEnd = bodyStart + length;
|
|
157
|
-
if (buffer.length < bodyEnd) return false;
|
|
158
|
-
const body = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
159
|
-
buffer = buffer.subarray(bodyEnd);
|
|
160
|
-
void handleMessage(JSON.parse(body));
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async function handleMessage(message) {
|
|
165
|
-
if (!Object.hasOwn(message, 'id')) {
|
|
166
|
-
if (message.method === 'notifications/initialized') return;
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
const result = await dispatch(message.method, message.params || {});
|
|
171
|
-
writeMessage({ jsonrpc: '2.0', id: message.id, result });
|
|
172
|
-
} catch (error) {
|
|
173
|
-
writeMessage({
|
|
174
|
-
jsonrpc: '2.0',
|
|
175
|
-
id: message.id,
|
|
176
|
-
error: {
|
|
177
|
-
code: -32603,
|
|
178
|
-
message: error.message,
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function dispatch(method, params) {
|
|
185
|
-
if (method === 'initialize') {
|
|
186
|
-
const ci = params.clientInfo || {};
|
|
187
155
|
|
|
156
|
+
async function handleMessage(message) {
|
|
157
|
+
if (!Object.hasOwn(message, 'id')) {
|
|
158
|
+
if (message.method === 'notifications/initialized') return;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
188
161
|
try {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
protocolVersion: params.protocolVersion || '2024-11-05',
|
|
200
|
-
capabilities: {
|
|
201
|
-
tools: {},
|
|
202
|
-
},
|
|
203
|
-
serverInfo: {
|
|
204
|
-
name: 'huaweicloud-devkit',
|
|
205
|
-
version: pkgVersion,
|
|
206
|
-
},
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (method === 'tools/list') {
|
|
211
|
-
return { tools: TOOL_DEFINITIONS };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (method === 'tools/call') {
|
|
215
|
-
const result = await callTool(params.name, params.arguments || {});
|
|
216
|
-
const decorated = decorateResult(params.name, result);
|
|
217
|
-
return {
|
|
218
|
-
content: [
|
|
219
|
-
{
|
|
220
|
-
type: 'text',
|
|
221
|
-
text: JSON.stringify(decorated, null, 2),
|
|
162
|
+
const result = await dispatch(message.method, message.params || {});
|
|
163
|
+
writeMessage({ jsonrpc: '2.0', id: message.id, result });
|
|
164
|
+
} catch (error) {
|
|
165
|
+
writeMessage({
|
|
166
|
+
jsonrpc: '2.0',
|
|
167
|
+
id: message.id,
|
|
168
|
+
error: {
|
|
169
|
+
code: -32603,
|
|
170
|
+
message: error.message,
|
|
222
171
|
},
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (method === 'resources/list') {
|
|
229
|
-
return { resources: [] };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
230
174
|
}
|
|
231
175
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
} else {
|
|
240
|
-
stdout.write(json + '\n');
|
|
176
|
+
function writeMessage(message) {
|
|
177
|
+
const json = JSON.stringify(message);
|
|
178
|
+
if (useContentLengthFraming) {
|
|
179
|
+
stdout.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
|
180
|
+
} else {
|
|
181
|
+
stdout.write(json + '\n');
|
|
182
|
+
}
|
|
241
183
|
}
|
|
242
184
|
}
|
|
@@ -4097,7 +4097,7 @@ async function cmdInstallHcloud() {
|
|
|
4097
4097
|
if (r.status === 0) {
|
|
4098
4098
|
console.log(' \x1b[32mPrivacy agreement accepted. KooCLI ready.\x1b[0m');
|
|
4099
4099
|
} else {
|
|
4100
|
-
console.log(' \x1b[33m
|
|
4100
|
+
console.log(' \x1b[33m无法写入配置目录。请在普通终端运行: echo "y" | hcloud version\x1b[0m');
|
|
4101
4101
|
}
|
|
4102
4102
|
} else {
|
|
4103
4103
|
console.log(' \x1b[33m请手动接受隐私协议:在终端运行 hcloud version 并按提示操作\x1b[0m');
|