@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 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
+ }