@sstar/boardlinker_host 0.2.0
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 +103 -0
- package/dist/.platform +1 -0
- package/dist/grpc.js +195 -0
- package/dist/index.js +1514 -0
- package/dist/logger.js +204 -0
- package/dist/proto/agent.proto +473 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @sstar/boardlinker_host
|
|
2
|
+
|
|
3
|
+
BoardLinker Host - MCP (Model Context Protocol) 服务器,让 AI 工具能够直接与嵌入式硬件交互。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
通过标准的 MCP 协议,为 AI 工具(如 Claude Code)提供以下硬件交互能力:
|
|
8
|
+
|
|
9
|
+
- **板卡 UART 操作**: 多会话串口管理、命令发送、信号控制
|
|
10
|
+
- **文件传输**: Host 与 Agent 之间的双向文件传输
|
|
11
|
+
- **固件烧录**: 镜像准备和 ISP 恢复烧录
|
|
12
|
+
- **Agent 状态查询**: 获取 Agent 运行状态和网络信息
|
|
13
|
+
- **文档系统**: 内置硬件操作文档和指南
|
|
14
|
+
|
|
15
|
+
## 使用
|
|
16
|
+
|
|
17
|
+
### npx 直接运行
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @sstar/boardlinker_host
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 全局安装
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install -g @sstar/boardlinker_host
|
|
27
|
+
|
|
28
|
+
# 安装后运行
|
|
29
|
+
boardlinker-host
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 前置条件
|
|
33
|
+
|
|
34
|
+
在使用 Host 之前,需要先在同一台机器上启动 Agent:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 终端 1: 启动 Agent
|
|
38
|
+
boardlinker
|
|
39
|
+
|
|
40
|
+
# 终端 2: 启动 Host
|
|
41
|
+
npx @sstar/boardlinker_host
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 环境变量
|
|
45
|
+
|
|
46
|
+
| 变量名 | 说明 | 默认值 |
|
|
47
|
+
|--------|------|--------|
|
|
48
|
+
| `BOARDLINKER_AGENT_ADDR` | Agent 连接地址 | `127.0.0.1:10000` |
|
|
49
|
+
| `BOARDLINKER_HOST_DEBUG` | Host 调试模式 | `false` |
|
|
50
|
+
| `BOARDLINKER_DEBUG` | 全局调试模式 | `false` |
|
|
51
|
+
|
|
52
|
+
## MCP 工具列表
|
|
53
|
+
|
|
54
|
+
Host 向 AI 工具暴露以下 MCP 工具:
|
|
55
|
+
|
|
56
|
+
### 板卡 UART 操作
|
|
57
|
+
- `board_uart-sessions` - 列出所有 UART 会话
|
|
58
|
+
- `board_uart-run_command` - 发送命令到 UART
|
|
59
|
+
- `board_uart-send_signal` - 发送控制信号(ENTER, Ctrl+C 等)
|
|
60
|
+
- `board_uart-read` - 从 UART 读取数据
|
|
61
|
+
- `board_uart-status` - 获取 UART 硬件状态
|
|
62
|
+
|
|
63
|
+
### 文件操作
|
|
64
|
+
- `files-put` - 上传文件到 Agent
|
|
65
|
+
- `files-get` - 从 Agent 下载文件
|
|
66
|
+
- `files-list` - 列出 Agent 文件
|
|
67
|
+
- `files-stat` - 获取文件状态
|
|
68
|
+
- `files-mkdir` - 创建目录
|
|
69
|
+
- `files-rm` - 删除文件/目录
|
|
70
|
+
|
|
71
|
+
### 固件操作
|
|
72
|
+
- `firmware-prepare_images` - 准备固件镜像
|
|
73
|
+
- `firmware-burn_recover` - 执行固件烧录/恢复
|
|
74
|
+
|
|
75
|
+
### 其他
|
|
76
|
+
- `agent-status` - 获取 Agent 状态
|
|
77
|
+
- `docs-list` - 列出可用文档
|
|
78
|
+
- `docs-read` - 读取文档内容
|
|
79
|
+
|
|
80
|
+
## 在 Claude Code 中使用
|
|
81
|
+
|
|
82
|
+
在 Claude Code 的 MCP 配置中添加:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"boardlinker": {
|
|
88
|
+
"command": "npx",
|
|
89
|
+
"args": ["-y", "@sstar/boardlinker_host"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 系统要求
|
|
96
|
+
|
|
97
|
+
- Node.js >= 18.0.0
|
|
98
|
+
- 需要先启动 [@sstar/boardlinker_agent](../agent/)
|
|
99
|
+
- 支持 macOS、Linux、Windows
|
|
100
|
+
|
|
101
|
+
## 许可证
|
|
102
|
+
|
|
103
|
+
MIT
|
package/dist/.platform
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
linux-x64
|
package/dist/grpc.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { Metadata, credentials, loadPackageDefinition } from '@grpc/grpc-js';
|
|
5
|
+
import { loadSync } from '@grpc/proto-loader';
|
|
6
|
+
export function createAgentClient(addr, opts) {
|
|
7
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const candidates = [
|
|
9
|
+
// 优先:构建输出目录中的 proto (dist/proto/agent.proto)
|
|
10
|
+
path.join(here, 'proto', 'agent.proto'),
|
|
11
|
+
// 兼容旧模式:Host 包自身目录下的 proto (packages/host/proto)
|
|
12
|
+
path.join(here, '..', 'proto', 'agent.proto'),
|
|
13
|
+
// 兼容当前 monorepo 结构:packages/host/dist/../.. -> repo 根
|
|
14
|
+
path.join(here, '..', '..', 'proto', 'agent.proto'),
|
|
15
|
+
];
|
|
16
|
+
const protoPath = candidates.find((p) => fs.existsSync(p)) || candidates[0];
|
|
17
|
+
const def = loadSync(protoPath, {
|
|
18
|
+
longs: String,
|
|
19
|
+
enums: String,
|
|
20
|
+
defaults: true,
|
|
21
|
+
oneofs: true,
|
|
22
|
+
keepCase: true,
|
|
23
|
+
});
|
|
24
|
+
const pkg = loadPackageDefinition(def);
|
|
25
|
+
const AgentCls = pkg.boardlinker.agent.v1.AgentService;
|
|
26
|
+
const PairingCls = pkg.boardlinker.agent.v1.PairingService;
|
|
27
|
+
const FilesCls = pkg.boardlinker.agent.v1.FilesService;
|
|
28
|
+
const client = new AgentCls(addr, credentials.createInsecure());
|
|
29
|
+
const pairingClient = new PairingCls(addr, credentials.createInsecure());
|
|
30
|
+
const filesClient = new FilesCls(addr, credentials.createInsecure());
|
|
31
|
+
const meta = new Metadata();
|
|
32
|
+
const hostInstanceId = (opts?.hostInstanceId || '').trim();
|
|
33
|
+
if (hostInstanceId) {
|
|
34
|
+
meta.set('x-boardlinker-host-instance-id', hostInstanceId);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
pairCheck(params) {
|
|
38
|
+
const payload = {
|
|
39
|
+
pairCode: params.pairCode,
|
|
40
|
+
clientId: params.clientId || 'boardlinker-host',
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const deadlineMs = typeof params.timeoutMs === 'number' && params.timeoutMs > 0 ? params.timeoutMs : 3000;
|
|
45
|
+
const deadline = new Date(Date.now() + deadlineMs);
|
|
46
|
+
pairingClient.PairCheck(payload, { deadline }, (e, r) => e ? reject(e) : resolve(r));
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
status() {
|
|
50
|
+
return new Promise((resolve, reject) => client.Status({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
51
|
+
},
|
|
52
|
+
boardUartWrite(params) {
|
|
53
|
+
return new Promise((resolve, reject) => client.BoardUartWrite(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
54
|
+
},
|
|
55
|
+
boardUartListSessions() {
|
|
56
|
+
return new Promise((resolve, reject) => client.BoardUartListSessions({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
57
|
+
},
|
|
58
|
+
boardUartSessionWrite(params) {
|
|
59
|
+
return new Promise((resolve, reject) => client.BoardUartSessionWrite(params, meta, (e, r) => e ? reject(e) : resolve(r)));
|
|
60
|
+
},
|
|
61
|
+
boardUartSessionRead(params) {
|
|
62
|
+
return new Promise((resolve, reject) => client.BoardUartSessionRead(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
63
|
+
},
|
|
64
|
+
boardUartOpenManual(params) {
|
|
65
|
+
return new Promise((resolve, reject) => client.BoardUartOpenManual(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
66
|
+
},
|
|
67
|
+
boardUartCloseSession(params) {
|
|
68
|
+
return new Promise((resolve, reject) => client.BoardUartCloseSession(params, meta, (e, r) => e ? reject(e) : resolve(r)));
|
|
69
|
+
},
|
|
70
|
+
boardUartStatus() {
|
|
71
|
+
return new Promise((resolve, reject) => client.BoardUartStatus({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
72
|
+
},
|
|
73
|
+
boardUartForceClose() {
|
|
74
|
+
return new Promise((resolve, reject) => client.BoardUartForceClose({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
75
|
+
},
|
|
76
|
+
tftpUpload(params) {
|
|
77
|
+
return new Promise((resolve, reject) => client.TftpUpload(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
78
|
+
},
|
|
79
|
+
tftpDownload(params) {
|
|
80
|
+
return new Promise((resolve, reject) => client.TftpDownload(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
81
|
+
},
|
|
82
|
+
tftpList(params) {
|
|
83
|
+
return new Promise((resolve, reject) => client.TftpList(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
84
|
+
},
|
|
85
|
+
tftpUserguide() {
|
|
86
|
+
return new Promise((resolve, reject) => client.TftpUserguide({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
87
|
+
},
|
|
88
|
+
ubootBreak(params) {
|
|
89
|
+
return new Promise((resolve, reject) => client.UBootBreak(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
90
|
+
},
|
|
91
|
+
ubootRunCommand(params) {
|
|
92
|
+
return new Promise((resolve, reject) => client.UBootRunCommand(params, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
93
|
+
},
|
|
94
|
+
boardUartListPorts() {
|
|
95
|
+
return new Promise((resolve, reject) => client.BoardUartListPorts({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
96
|
+
},
|
|
97
|
+
listSshCandidates() {
|
|
98
|
+
return new Promise((resolve, reject) => client.ListSshCandidates({}, meta, (e, r) => (e ? reject(e) : resolve(r))));
|
|
99
|
+
},
|
|
100
|
+
nfsUpload(params) {
|
|
101
|
+
return new Promise((resolve, reject) => client.NfsUpload(params, (e, r) => (e ? reject(e) : resolve(r))));
|
|
102
|
+
},
|
|
103
|
+
nfsDownload(params) {
|
|
104
|
+
return new Promise((resolve, reject) => client.NfsDownload(params, (e, r) => (e ? reject(e) : resolve(r))));
|
|
105
|
+
},
|
|
106
|
+
nfsList(params) {
|
|
107
|
+
// 转换mode为protobuf枚举值
|
|
108
|
+
const protoParams = {
|
|
109
|
+
remoteSubpath: params.remoteSubpath || '',
|
|
110
|
+
mode: params.mode === 'detailed' ? 2 : 1, // 2 = NFS_LIST_MODE_DETAILED, 1 = NFS_LIST_MODE_SIMPLE
|
|
111
|
+
limit: params.limit,
|
|
112
|
+
};
|
|
113
|
+
return new Promise((resolve, reject) => client.NfsList(protoParams, (e, r) => (e ? reject(e) : resolve(r))));
|
|
114
|
+
},
|
|
115
|
+
nfsInfo() {
|
|
116
|
+
return new Promise((resolve, reject) => client.NfsInfo({}, (e, r) => (e ? reject(e) : resolve(r))));
|
|
117
|
+
},
|
|
118
|
+
nfsUserguide() {
|
|
119
|
+
return new Promise((resolve, reject) => client.NfsUserguide({}, (e, r) => (e ? reject(e) : resolve(r))));
|
|
120
|
+
},
|
|
121
|
+
boardNotes(params) {
|
|
122
|
+
return new Promise((resolve, reject) => client.BoardNotes(params, (e, r) => (e ? reject(e) : resolve(r))));
|
|
123
|
+
},
|
|
124
|
+
firmwarePrepareImages(params) {
|
|
125
|
+
const req = {
|
|
126
|
+
imagesRoot: params.imagesRoot,
|
|
127
|
+
};
|
|
128
|
+
return new Promise((resolve, reject) => client.FirmwarePrepareImages(req, (e, r) => (e ? reject(e) : resolve(r))));
|
|
129
|
+
},
|
|
130
|
+
firmwareBurnRecover(params) {
|
|
131
|
+
const req = {
|
|
132
|
+
imagesRoot: params.imagesRoot,
|
|
133
|
+
args: params.args ?? [],
|
|
134
|
+
timeoutMs: params.timeoutMs ?? 0,
|
|
135
|
+
force: params.force ?? false,
|
|
136
|
+
};
|
|
137
|
+
return new Promise((resolve, reject) => client.FirmwareBurnRecover(req, (e, r) => (e ? reject(e) : resolve(r))));
|
|
138
|
+
},
|
|
139
|
+
firmwareUserGuide() {
|
|
140
|
+
return new Promise((resolve, reject) => client.FirmwareUserGuide({}, (e, r) => (e ? reject(e) : resolve(r))));
|
|
141
|
+
},
|
|
142
|
+
tunnelStart(params) {
|
|
143
|
+
const req = {
|
|
144
|
+
host: params.host,
|
|
145
|
+
user: params.user,
|
|
146
|
+
port: params.port ?? 22,
|
|
147
|
+
password: params.password ?? '',
|
|
148
|
+
privateKeyPath: params.privateKeyPath ?? '',
|
|
149
|
+
localPort: params.localPort ?? 0,
|
|
150
|
+
forwardHost: params.forwardHost ?? '',
|
|
151
|
+
forwardPort: params.forwardPort ?? 0,
|
|
152
|
+
};
|
|
153
|
+
return new Promise((resolve, reject) => client.TunnelStart(req, (e, r) => (e ? reject(e) : resolve(r))));
|
|
154
|
+
},
|
|
155
|
+
tunnelStop() {
|
|
156
|
+
return new Promise((resolve, reject) => client.TunnelStop({}, (e, r) => (e ? reject(e) : resolve(r))));
|
|
157
|
+
},
|
|
158
|
+
hostLogStream() {
|
|
159
|
+
let resolver = () => { };
|
|
160
|
+
let rejecter = () => { };
|
|
161
|
+
const response = new Promise((resolve, reject) => {
|
|
162
|
+
resolver = resolve;
|
|
163
|
+
rejecter = reject;
|
|
164
|
+
});
|
|
165
|
+
const call = client.HostLogStream(meta, (e, r) => (e ? rejecter(e) : resolver(r)));
|
|
166
|
+
return { call, response };
|
|
167
|
+
},
|
|
168
|
+
// Files Service 客户端方法
|
|
169
|
+
filesPutPath() {
|
|
170
|
+
let resolver = () => { };
|
|
171
|
+
let rejecter = () => { };
|
|
172
|
+
const response = new Promise((resolve, reject) => {
|
|
173
|
+
resolver = resolve;
|
|
174
|
+
rejecter = reject;
|
|
175
|
+
});
|
|
176
|
+
const call = filesClient.PutPath((e, r) => (e ? rejecter(e) : resolver(r)));
|
|
177
|
+
return { call, response };
|
|
178
|
+
},
|
|
179
|
+
filesGetPath(params) {
|
|
180
|
+
return filesClient.GetPath(params);
|
|
181
|
+
},
|
|
182
|
+
filesList(params) {
|
|
183
|
+
return new Promise((resolve, reject) => filesClient.ListFiles(params || {}, (e, r) => (e ? reject(e) : resolve(r))));
|
|
184
|
+
},
|
|
185
|
+
filesRemove(params) {
|
|
186
|
+
return new Promise((resolve, reject) => filesClient.RemoveFiles(params, (e, r) => (e ? reject(e) : resolve(r))));
|
|
187
|
+
},
|
|
188
|
+
filesMakeDir(params) {
|
|
189
|
+
return new Promise((resolve, reject) => filesClient.MakeDirectory(params, (e, r) => (e ? reject(e) : resolve(r))));
|
|
190
|
+
},
|
|
191
|
+
filesGetStat(params) {
|
|
192
|
+
return new Promise((resolve, reject) => filesClient.GetStat(params, (e, r) => (e ? reject(e) : resolve(r))));
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|