@bsbofmusic/agent-reach-mcp 0.1.1 → 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 +246 -130
- package/index.js +165 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,252 +1,368 @@
|
|
|
1
|
-
|
|
1
|
+
````md
|
|
2
|
+
# Agent-Reach MCP (stdio) — Full Mirror Edition
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
这是一个给 **OpenCode / CC-Switch** 用的 MCP(Model Context Protocol)stdio 服务器。目标很明确:
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
> ✅ 导入 MCP 后,就能 **完整复刻 Agent-Reach 的全部能力**(通过 `reach_exec` 透传任意子命令)
|
|
7
|
+
> ✅ 并且满足你要求的:**简单 / 快捷 / 强大 / 永远最新**(每次调用前自动更新 Agent-Reach)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
---
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## 你将得到什么
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
启用这个 MCP 后,OpenCode 会多出一组工具(tools):
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
- `reach_ensure`:自愈环境 + 更新到最新 Agent-Reach
|
|
16
|
+
- `reach_doctor`:执行 `agent-reach doctor`
|
|
17
|
+
- `reach_read`:执行 `agent-reach read <url>`
|
|
18
|
+
- ✅ `reach_exec`:执行 **任意** `agent-reach <subcommand> ...args`(全功能复刻关键)
|
|
19
|
+
- `reach_list_commands`:执行 `agent-reach --help` 并解析出可用子命令列表(帮助模型“知道自己能干什么”)
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
你不需要让 OpenCode 记住安装过程;它每次调用都会先把 Agent-Reach 升级到最新再执行。
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
---
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
## 核心理念:MCP = “能力注入”,不是“记忆依赖”
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
OpenCode 重启会“失忆”,但 MCP 是持久配置。
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
- CC-Switch 负责:开关 / 配置 / 切换
|
|
30
|
+
- MCP server 负责:环境自愈 + 能力暴露
|
|
31
|
+
- Agent-Reach 负责:实际的多平台读/搜/配置能力
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
所以正确心智模型是:
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
> OpenCode 不需要记住怎么安装
|
|
36
|
+
> 它只要知道:有一个 MCP tool 叫 `reach_exec`,能运行 Agent-Reach 的任何命令
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
---
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
## 工作原理(非常重要)
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
每次你调用任意一个 tool(包括 `reach_exec`),MCP 都会:
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
1) 在用户缓存目录创建/复用 Python venv
|
|
45
|
+
2) 执行(永远最新策略):
|
|
46
|
+
- `pip install -U https://github.com/Panniantong/agent-reach/archive/main.zip`
|
|
47
|
+
3) 然后运行对应的 `agent-reach ...` 命令
|
|
48
|
+
4) 把 stdout/stderr 回传给 OpenCode
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
### venv 位置(默认)
|
|
51
|
+
- Windows:`%LOCALAPPDATA%\agent-reach-mcp\runtime\venv`
|
|
52
|
+
- macOS:`~/Library/Caches/agent-reach-mcp/runtime/venv`
|
|
53
|
+
- Linux:`~/.cache/agent-reach-mcp/runtime/venv`
|
|
38
54
|
|
|
39
|
-
|
|
55
|
+
---
|
|
40
56
|
|
|
41
|
-
|
|
57
|
+
## 环境要求
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
- Node.js >= 18
|
|
60
|
+
- Python 3(系统 PATH 里能找到 `python`,Windows 也可用 `py`)
|
|
61
|
+
- 能访问 GitHub(用于拉 main.zip)
|
|
44
62
|
|
|
45
|
-
|
|
46
|
-
OpenCode
|
|
47
|
-
↓
|
|
48
|
-
MCP (stdio)
|
|
49
|
-
↓
|
|
50
|
-
Node.js server (this package)
|
|
51
|
-
↓
|
|
52
|
-
Auto-managed Python venv (user cache)
|
|
53
|
-
↓
|
|
54
|
-
Latest Agent-Reach (pip install -U main.zip)
|
|
63
|
+
> 说明:某些平台能力(例如需要 cookie / 代理 / docker 的)属于 Agent-Reach 上游能力,你需要按 doctor 输出提示去配。
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
---
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
## 安装与本地验证(开发者)
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
在项目目录:
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
~/Library/Caches/agent-reach-mcp/runtime/venv
|
|
65
|
-
|
|
66
|
-
Linux:
|
|
67
|
-
|
|
68
|
-
~/.cache/agent-reach-mcp/runtime/venv
|
|
69
|
-
📦 Installation (Local Development)
|
|
71
|
+
```bash
|
|
70
72
|
npm install
|
|
71
73
|
node index.js
|
|
74
|
+
````
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
stdio MCP server 启动后会等待输入,这属于正常现象。
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
---
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
## 发布到 npm(手动,无 CI)
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
### 1) 修改 package.json
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
确保:
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
* `name` 是你自己的包名(建议 scope 包:`@你的scope/agent-reach-mcp`)
|
|
87
|
+
* `version` 每次发布都 +1(npm 禁止同版本覆盖)
|
|
84
88
|
|
|
85
|
-
2
|
|
89
|
+
### 2) 发布
|
|
86
90
|
|
|
91
|
+
```bash
|
|
87
92
|
npm login
|
|
88
93
|
npm publish --access public
|
|
89
|
-
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## CC-Switch / OpenCode 配置方式(你要的“一键复活”)
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
在 CC-Switch 新建 MCP 配置:
|
|
92
101
|
|
|
102
|
+
```json
|
|
93
103
|
{
|
|
94
104
|
"command": "npx",
|
|
95
105
|
"args": ["-y", "@your-scope/agent-reach-mcp@latest"],
|
|
96
106
|
"type": "stdio"
|
|
97
107
|
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
然后重启 OpenCode。
|
|
111
|
+
|
|
112
|
+
> 之后只要你更新 npm 包版本,用户侧 `@latest` 会在下次启动时拉到新版本(通常如此;依赖 npx 缓存策略)。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
# Tools 使用说明(强烈建议认真看)
|
|
117
|
+
|
|
118
|
+
## 0) 第一次使用推荐流程
|
|
98
119
|
|
|
99
|
-
|
|
120
|
+
1. `reach_ensure`(确保环境就绪)
|
|
121
|
+
2. `reach_doctor`(看哪些渠道通、怎么配)
|
|
122
|
+
3. `reach_list_commands`(让 OpenCode 知道有哪些子命令)
|
|
123
|
+
4. 用 `reach_exec` 跑你要的平台命令
|
|
100
124
|
|
|
101
|
-
|
|
125
|
+
---
|
|
102
126
|
|
|
103
|
-
|
|
127
|
+
## 1) reach_ensure
|
|
104
128
|
|
|
105
|
-
|
|
129
|
+
用途:只做环境准备 + 更新。
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
1️⃣ reach_ensure
|
|
131
|
+
它等价于:
|
|
109
132
|
|
|
110
|
-
|
|
133
|
+
* 创建 venv
|
|
134
|
+
* pip 工具链更新
|
|
135
|
+
* pip 安装/升级 Agent-Reach main.zip
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
适用场景:
|
|
113
138
|
|
|
114
|
-
|
|
139
|
+
* 第一次启用 MCP
|
|
140
|
+
* 你怀疑环境坏了
|
|
141
|
+
* 你想强制刷新到最新
|
|
115
142
|
|
|
116
|
-
|
|
143
|
+
---
|
|
117
144
|
|
|
118
|
-
2
|
|
145
|
+
## 2) reach_doctor
|
|
119
146
|
|
|
120
|
-
|
|
147
|
+
用途:诊断所有渠道状态。
|
|
121
148
|
|
|
149
|
+
它会执行:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
122
152
|
agent-reach doctor
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
适用场景:
|
|
156
|
+
|
|
157
|
+
* 为什么某个平台读不到?
|
|
158
|
+
* 需要 cookie 吗?
|
|
159
|
+
* 需要代理吗?
|
|
160
|
+
* 需要 docker 吗?
|
|
161
|
+
|
|
162
|
+
doctor 是 Agent-Reach 官方推荐的“总检命令”。
|
|
163
|
+
|
|
164
|
+
---
|
|
123
165
|
|
|
124
|
-
|
|
166
|
+
## 3) reach_read
|
|
125
167
|
|
|
126
|
-
|
|
168
|
+
用途:读一个链接(网页/视频/仓库等,交给 Agent-Reach 判断)。
|
|
127
169
|
|
|
128
|
-
|
|
170
|
+
它会执行:
|
|
129
171
|
|
|
172
|
+
```bash
|
|
130
173
|
agent-reach read <url>
|
|
174
|
+
```
|
|
131
175
|
|
|
132
|
-
|
|
176
|
+
输入:
|
|
133
177
|
|
|
134
|
-
|
|
178
|
+
```json
|
|
179
|
+
{ "url": "https://example.com" }
|
|
180
|
+
```
|
|
135
181
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
182
|
+
### 常见例子
|
|
183
|
+
|
|
184
|
+
* 读普通网页:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{ "url": "https://example.com" }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
* 获取 YouTube 教程字幕/内容(Agent-Reach README 的示例就是 read YouTube):
|
|
140
191
|
|
|
141
|
-
|
|
192
|
+
```json
|
|
193
|
+
{ "url": "https://youtube.com/watch?v=xxx" }
|
|
194
|
+
```
|
|
142
195
|
|
|
143
|
-
|
|
196
|
+
---
|
|
144
197
|
|
|
145
|
-
|
|
198
|
+
## 4) reach_exec ✅(全功能复刻关键)
|
|
146
199
|
|
|
147
|
-
|
|
200
|
+
这是“完整复刻”的核心。它可以透传执行任何 Agent-Reach 子命令:
|
|
148
201
|
|
|
149
|
-
|
|
202
|
+
等价于:
|
|
150
203
|
|
|
151
|
-
|
|
204
|
+
```bash
|
|
205
|
+
agent-reach <subcommand> ...args
|
|
206
|
+
```
|
|
152
207
|
|
|
153
|
-
|
|
208
|
+
输入格式:
|
|
154
209
|
|
|
155
|
-
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"subcommand": "doctor",
|
|
213
|
+
"args": [],
|
|
214
|
+
"timeoutMs": 600000
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 4.1 安装(对应 Agent-Reach README 的安装方式)
|
|
219
|
+
|
|
220
|
+
> 虽然 MCP 自己会安装 main.zip,但 Agent-Reach 还有 “install” 用来装系统依赖/配置渠道(很重要)
|
|
221
|
+
|
|
222
|
+
* 全自动安装(默认推荐):
|
|
156
223
|
|
|
157
|
-
|
|
224
|
+
```json
|
|
225
|
+
{ "subcommand": "install", "args": ["--env=auto"] }
|
|
226
|
+
```
|
|
158
227
|
|
|
159
|
-
|
|
228
|
+
* 安全模式(不自动改系统,只提示缺什么):
|
|
160
229
|
|
|
161
|
-
|
|
230
|
+
```json
|
|
231
|
+
{ "subcommand": "install", "args": ["--env=auto", "--safe"] }
|
|
232
|
+
```
|
|
162
233
|
|
|
163
|
-
|
|
234
|
+
* Dry Run(预览将做什么,不执行):
|
|
164
235
|
|
|
165
|
-
|
|
236
|
+
```json
|
|
237
|
+
{ "subcommand": "install", "args": ["--env=auto", "--dry-run"] }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 4.2 Twitter/X:配置 cookie + 搜索推文(来自官方 README FAQ)
|
|
241
|
+
|
|
242
|
+
* 配置 twitter cookie(你需要把 Cookie-Editor 导出的 cookie 字符串填进去):
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"subcommand": "configure",
|
|
247
|
+
"args": ["twitter-cookies", "your_cookies_here"]
|
|
248
|
+
}
|
|
249
|
+
```
|
|
166
250
|
|
|
167
|
-
|
|
251
|
+
* 搜索推文:
|
|
168
252
|
|
|
169
|
-
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"subcommand": "search-twitter",
|
|
256
|
+
"args": ["关键词"]
|
|
257
|
+
}
|
|
258
|
+
```
|
|
170
259
|
|
|
171
|
-
|
|
260
|
+
### 4.3 代理配置(解决 Reddit 数据中心 IP 403 等,来自官方 README FAQ)
|
|
172
261
|
|
|
173
|
-
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"subcommand": "configure",
|
|
265
|
+
"args": ["proxy", "http://user:pass@ip:port"]
|
|
266
|
+
}
|
|
267
|
+
```
|
|
174
268
|
|
|
175
|
-
|
|
269
|
+
### 4.4 小红书:搜索(来自官方 README FAQ)
|
|
176
270
|
|
|
177
|
-
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"subcommand": "search-xhs",
|
|
274
|
+
"args": ["关键词"]
|
|
275
|
+
}
|
|
276
|
+
```
|
|
178
277
|
|
|
179
|
-
|
|
278
|
+
> 小红书往往需要 docker / MCP 服务配套,先跑 `reach_doctor` 看提示。
|
|
180
279
|
|
|
181
|
-
|
|
280
|
+
### 4.5 读任意链接(等价 reach_read,但更通用)
|
|
182
281
|
|
|
183
|
-
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"subcommand": "read",
|
|
285
|
+
"args": ["https://github.com/Panniantong/Agent-Reach"]
|
|
286
|
+
}
|
|
287
|
+
```
|
|
184
288
|
|
|
185
|
-
|
|
289
|
+
---
|
|
186
290
|
|
|
187
|
-
|
|
291
|
+
## 5) reach_list_commands(推荐让 OpenCode“自发现”能力)
|
|
188
292
|
|
|
189
|
-
|
|
293
|
+
用途:列出当前安装的 Agent-Reach 版本支持哪些子命令。
|
|
190
294
|
|
|
191
|
-
|
|
295
|
+
为什么需要它?
|
|
192
296
|
|
|
193
|
-
|
|
297
|
+
* Agent-Reach 在快速迭代
|
|
298
|
+
* 你坚持“永远最新”
|
|
299
|
+
* 所以命令集合可能随时变化
|
|
300
|
+
* `reach_list_commands` 能让 OpenCode 在**没有记忆**的情况下,仍然准确知道有哪些子命令
|
|
194
301
|
|
|
195
|
-
|
|
302
|
+
推荐用法:
|
|
196
303
|
|
|
197
|
-
|
|
304
|
+
1. 启用 MCP 后先跑一次 `reach_list_commands`
|
|
305
|
+
2. OpenCode 会得到一份命令列表
|
|
306
|
+
3. 后续就用 `reach_exec` 调用列表里的命令
|
|
198
307
|
|
|
199
|
-
|
|
308
|
+
---
|
|
200
309
|
|
|
201
|
-
|
|
310
|
+
# 推荐验证计划(你可以照抄给 OpenCode 执行)
|
|
202
311
|
|
|
203
|
-
|
|
312
|
+
## Phase 1:基础健康
|
|
204
313
|
|
|
205
|
-
|
|
314
|
+
1. `reach_ensure`
|
|
315
|
+
2. `reach_doctor`
|
|
316
|
+
3. `reach_read` 读一个普通网页
|
|
206
317
|
|
|
207
|
-
|
|
318
|
+
预期:能成功安装/更新 + doctor 输出渠道状态 + read 有返回内容
|
|
208
319
|
|
|
209
|
-
|
|
320
|
+
## Phase 2:全能力复刻
|
|
210
321
|
|
|
211
|
-
|
|
322
|
+
1. `reach_list_commands`
|
|
323
|
+
2. 从列表里挑 3 个子命令用 `reach_exec` 跑(例如 install / configure / search-* / read)
|
|
212
324
|
|
|
213
|
-
|
|
325
|
+
## Phase 3:按需补齐配置
|
|
214
326
|
|
|
215
|
-
|
|
327
|
+
* Twitter 需要 cookie:用 `configure twitter-cookies ...`
|
|
328
|
+
* 服务器访问 Reddit/B站可能需要代理:用 `configure proxy ...`
|
|
329
|
+
* 小红书可能需要 docker:按 doctor 指引
|
|
216
330
|
|
|
217
|
-
|
|
331
|
+
---
|
|
218
332
|
|
|
219
|
-
|
|
333
|
+
# 常见问题(FAQ)
|
|
220
334
|
|
|
221
|
-
|
|
335
|
+
## Q1: 为什么我明明启用了 MCP,但某个平台用不了?
|
|
222
336
|
|
|
223
|
-
|
|
224
|
-
🔮 Future Extensions (Optional)
|
|
337
|
+
A:平台能力取决于 Agent-Reach 的“渠道状态”,不是 MCP 本身。跑 `reach_doctor` 看缺什么(cookie、代理、docker、登录等)。
|
|
225
338
|
|
|
226
|
-
|
|
339
|
+
## Q2: 为什么每次调用都要更新?会不会慢?
|
|
227
340
|
|
|
228
|
-
|
|
341
|
+
A:这是你选择的策略:Always-Latest。首次或上游更新时会慢,但能保证永远最新。如果以后你要“快且稳”,可以改成“每日一次更新/或失败回滚”。
|
|
229
342
|
|
|
230
|
-
|
|
343
|
+
## Q3: 如何避免 OpenCode 重启后不知道有哪些命令?
|
|
231
344
|
|
|
232
|
-
|
|
345
|
+
A:用 `reach_list_commands`。这是为“无记忆模型”专门做的能力自发现入口。
|
|
233
346
|
|
|
234
|
-
|
|
347
|
+
## Q4: 我想要更友好的工具(比如 reach_search_twitter / reach_search_xhs)要不要做?
|
|
235
348
|
|
|
236
|
-
|
|
349
|
+
A:可以做,但不是必须。`reach_exec` 已经完整复刻;单独工具只是“更强的 schema 引导”。
|
|
237
350
|
|
|
238
|
-
|
|
351
|
+
---
|
|
239
352
|
|
|
240
|
-
|
|
353
|
+
# License
|
|
241
354
|
|
|
242
|
-
|
|
355
|
+
MIT
|
|
243
356
|
|
|
244
|
-
|
|
357
|
+
```
|
|
245
358
|
|
|
246
|
-
|
|
359
|
+
---
|
|
247
360
|
|
|
248
|
-
|
|
361
|
+
### 我对齐了哪些“真实命令名”(可核对)
|
|
362
|
+
这些命令名来自 Agent-Reach 最新 README 中的安装方式与 FAQ:`configure twitter-cookies`、`search-twitter`、`configure proxy`、`search-xhs`、`read`、`doctor`、`install --env=auto/--safe/--dry-run`。 :contentReference[oaicite:1]{index=1}
|
|
249
363
|
|
|
250
|
-
|
|
364
|
+
如果你愿意,我还可以再给你一个**更强的 README 增强版**:把 `reach_list_commands` 的输出示例也写进去,并给一份“OpenCode Prompt 模板”(让模型自动:list_commands → 选命令 → exec)。
|
|
365
|
+
::contentReference[oaicite:2]{index=2}
|
|
366
|
+
```
|
|
251
367
|
|
|
252
|
-
|
|
368
|
+
[1]: https://github.com/Panniantong/Agent-Reach "GitHub - Panniantong/Agent-Reach: Give your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees."
|
package/index.js
CHANGED
|
@@ -12,12 +12,16 @@ import {
|
|
|
12
12
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* - Creates a Python venv under user cache dir
|
|
17
|
-
* - On EVERY tool call, upgrades Agent-Reach from GitHub main.zip
|
|
18
|
-
* - Executes `agent-reach` via the venv (fallback to python -m ...)
|
|
15
|
+
* Agent-Reach MCP (stdio) — Full capability proxy
|
|
19
16
|
*
|
|
20
|
-
*
|
|
17
|
+
* Goals:
|
|
18
|
+
* - Import MCP => "full Agent-Reach" via reach_exec (generic passthrough)
|
|
19
|
+
* - Still provide ergonomic basics: ensure / doctor / read
|
|
20
|
+
* - On EVERY tool call: auto-upgrade Agent-Reach from GitHub main.zip (always latest)
|
|
21
|
+
*
|
|
22
|
+
* Notes:
|
|
23
|
+
* - This favors "latest" over strict stability, per your requirement.
|
|
24
|
+
* - It manages its own python venv in user cache.
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
function isWindows() {
|
|
@@ -101,8 +105,6 @@ async function createVenvIfMissing(root) {
|
|
|
101
105
|
|
|
102
106
|
if (existsSync(pythonExe)) return;
|
|
103
107
|
|
|
104
|
-
// Try creating venv using system python.
|
|
105
|
-
// Windows: python, then py -3
|
|
106
108
|
const candidates = isWindows()
|
|
107
109
|
? [
|
|
108
110
|
{ cmd: "python", args: ["-m", "venv", venvDir] },
|
|
@@ -120,7 +122,9 @@ async function createVenvIfMissing(root) {
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
if (!existsSync(pythonExe)) {
|
|
123
|
-
const tried = candidates
|
|
125
|
+
const tried = candidates
|
|
126
|
+
.map((c) => `${c.cmd} ${c.args.join(" ")}`)
|
|
127
|
+
.join(" | ");
|
|
124
128
|
const err = last?.stderr || "";
|
|
125
129
|
throw new Error(
|
|
126
130
|
`Failed to create Python venv.\nTried: ${tried}\nLast stderr:\n${err}`
|
|
@@ -133,15 +137,19 @@ async function ensureLatestAgentReach(root) {
|
|
|
133
137
|
const { pythonExe, agentReachExe } = venvPaths(root);
|
|
134
138
|
|
|
135
139
|
// Upgrade pip tooling (best effort)
|
|
136
|
-
await run(
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
await run(
|
|
141
|
+
pythonExe,
|
|
142
|
+
["-m", "pip", "install", "-U", "pip", "setuptools", "wheel"],
|
|
143
|
+
{ timeoutMs: 5 * 60 * 1000 }
|
|
144
|
+
);
|
|
139
145
|
|
|
140
|
-
//
|
|
146
|
+
// ALWAYS LATEST (main branch zip)
|
|
141
147
|
const pkgUrl = "https://github.com/Panniantong/agent-reach/archive/main.zip";
|
|
142
|
-
const installRes = await run(
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
const installRes = await run(
|
|
149
|
+
pythonExe,
|
|
150
|
+
["-m", "pip", "install", "-U", pkgUrl],
|
|
151
|
+
{ timeoutMs: 10 * 60 * 1000 }
|
|
152
|
+
);
|
|
145
153
|
|
|
146
154
|
let log = `pip install -U ${pkgUrl}\nexit=${installRes.code}\n`;
|
|
147
155
|
if (installRes.stdout.trim()) log += `stdout:\n${installRes.stdout}\n`;
|
|
@@ -158,17 +166,19 @@ async function ensureLatestAgentReach(root) {
|
|
|
158
166
|
return { pythonExe, agentReachExe, ensureLog: log };
|
|
159
167
|
}
|
|
160
168
|
|
|
161
|
-
async function runAgentReach(root, args) {
|
|
162
|
-
const { pythonExe, agentReachExe, ensureLog } = await ensureLatestAgentReach(
|
|
169
|
+
async function runAgentReach(root, args, extraOpts = {}) {
|
|
170
|
+
const { pythonExe, agentReachExe, ensureLog } = await ensureLatestAgentReach(
|
|
171
|
+
root
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const timeoutMs = extraOpts.timeoutMs ?? 10 * 60 * 1000;
|
|
163
175
|
|
|
164
176
|
let execRes;
|
|
165
177
|
if (existsSync(agentReachExe)) {
|
|
166
|
-
execRes = await run(agentReachExe, args, { timeoutMs
|
|
178
|
+
execRes = await run(agentReachExe, args, { timeoutMs });
|
|
167
179
|
} else {
|
|
168
|
-
// Fallback: module import
|
|
169
|
-
execRes = await run(pythonExe, ["-m", "agent_reach", ...args], {
|
|
170
|
-
timeoutMs: 10 * 60 * 1000,
|
|
171
|
-
});
|
|
180
|
+
// Fallback: module import (best effort)
|
|
181
|
+
execRes = await run(pythonExe, ["-m", "agent_reach", ...args], { timeoutMs });
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
let out = `# ensure_latest\n${ensureLog}\n`;
|
|
@@ -179,13 +189,55 @@ async function runAgentReach(root, args) {
|
|
|
179
189
|
return { out, code: execRes.code };
|
|
180
190
|
}
|
|
181
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Best-effort command listing:
|
|
194
|
+
* - Try: `agent-reach --help`
|
|
195
|
+
* - Parse lines that look like subcommands.
|
|
196
|
+
*/
|
|
197
|
+
function parseCommandsFromHelp(helpText) {
|
|
198
|
+
const lines = helpText.split(/\r?\n/);
|
|
199
|
+
const commands = new Set();
|
|
200
|
+
|
|
201
|
+
// Common patterns:
|
|
202
|
+
// "Commands:" section in Click/Typer
|
|
203
|
+
// indented: "read ..." or "doctor ..."
|
|
204
|
+
let inCommands = false;
|
|
205
|
+
for (const raw of lines) {
|
|
206
|
+
const line = raw.trimEnd();
|
|
207
|
+
|
|
208
|
+
if (/^Commands:\s*$/i.test(line.trim())) {
|
|
209
|
+
inCommands = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (inCommands && line.trim() === "") {
|
|
213
|
+
// stop after an empty line (often ends section)
|
|
214
|
+
// (but some help formats include blank lines; be conservative)
|
|
215
|
+
// We'll not hard-stop here.
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (inCommands) {
|
|
219
|
+
const m = line.match(/^\s{0,4}([a-zA-Z0-9][a-zA-Z0-9_-]+)\s{2,}.*$/);
|
|
220
|
+
if (m?.[1]) commands.add(m[1]);
|
|
221
|
+
} else {
|
|
222
|
+
// Some CLIs list commands without explicit "Commands:" header
|
|
223
|
+
const m = line.match(/^\s{0,4}([a-zA-Z0-9][a-zA-Z0-9_-]+)\s{2,}.*$/);
|
|
224
|
+
if (m?.[1] && !/^(Usage|Options|Arguments):?$/i.test(m[1])) {
|
|
225
|
+
// Only add if it doesn't look like a header.
|
|
226
|
+
// But to avoid noise, we won't add outside commands section.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return Array.from(commands).sort();
|
|
232
|
+
}
|
|
233
|
+
|
|
182
234
|
function text(content) {
|
|
183
235
|
return [{ type: "text", text: content }];
|
|
184
236
|
}
|
|
185
237
|
|
|
186
238
|
async function main() {
|
|
187
239
|
const server = new Server(
|
|
188
|
-
{ name: "agent-reach-mcp", version: "0.
|
|
240
|
+
{ name: "agent-reach-mcp", version: "0.2.0" },
|
|
189
241
|
{ capabilities: { tools: {} } }
|
|
190
242
|
);
|
|
191
243
|
|
|
@@ -218,6 +270,38 @@ async function main() {
|
|
|
218
270
|
required: ["url"],
|
|
219
271
|
},
|
|
220
272
|
},
|
|
273
|
+
{
|
|
274
|
+
name: "reach_exec",
|
|
275
|
+
description:
|
|
276
|
+
"Execute ANY Agent-Reach subcommand. Example: {subcommand:'search-xhs', args:['关键词']} or {subcommand:'read', args:['https://...']}. Auto-updates Agent-Reach first.",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
subcommand: {
|
|
281
|
+
type: "string",
|
|
282
|
+
description:
|
|
283
|
+
"Agent-Reach subcommand name, e.g. 'search-xhs', 'read', 'doctor', etc.",
|
|
284
|
+
},
|
|
285
|
+
args: {
|
|
286
|
+
type: "array",
|
|
287
|
+
items: { type: "string" },
|
|
288
|
+
description: "Arguments passed to the subcommand",
|
|
289
|
+
},
|
|
290
|
+
timeoutMs: {
|
|
291
|
+
type: "number",
|
|
292
|
+
description:
|
|
293
|
+
"Optional timeout in milliseconds for the command execution (default ~10 minutes).",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
required: ["subcommand"],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "reach_list_commands",
|
|
301
|
+
description:
|
|
302
|
+
"List available Agent-Reach subcommands by parsing `agent-reach --help`. Auto-updates Agent-Reach first.",
|
|
303
|
+
inputSchema: { type: "object", properties: {} },
|
|
304
|
+
},
|
|
221
305
|
],
|
|
222
306
|
};
|
|
223
307
|
});
|
|
@@ -240,12 +324,68 @@ async function main() {
|
|
|
240
324
|
if (name === "reach_read") {
|
|
241
325
|
const url = String(input.url ?? "");
|
|
242
326
|
if (!url || !/^https?:\/\//i.test(url)) {
|
|
243
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
content: text("Invalid url. Must start with http(s)://"),
|
|
329
|
+
isError: true,
|
|
330
|
+
};
|
|
244
331
|
}
|
|
245
|
-
const { out, code } = await runAgentReach(runtimeRoot, ["read", url]
|
|
332
|
+
const { out, code } = await runAgentReach(runtimeRoot, ["read", url], {
|
|
333
|
+
timeoutMs: 10 * 60 * 1000,
|
|
334
|
+
});
|
|
246
335
|
return { content: text(out), isError: code !== 0 };
|
|
247
336
|
}
|
|
248
337
|
|
|
338
|
+
if (name === "reach_exec") {
|
|
339
|
+
const subcommand = String(input.subcommand ?? "").trim();
|
|
340
|
+
const args = Array.isArray(input.args) ? input.args.map(String) : [];
|
|
341
|
+
const timeoutMs =
|
|
342
|
+
typeof input.timeoutMs === "number" && input.timeoutMs > 0
|
|
343
|
+
? Math.floor(input.timeoutMs)
|
|
344
|
+
: 10 * 60 * 1000;
|
|
345
|
+
|
|
346
|
+
if (!subcommand) {
|
|
347
|
+
return {
|
|
348
|
+
content: text("Missing required field: subcommand"),
|
|
349
|
+
isError: true,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Minimal safety: prevent command injection by disallowing whitespace in subcommand
|
|
354
|
+
// (args are passed as array so they are safe).
|
|
355
|
+
if (/\s/.test(subcommand)) {
|
|
356
|
+
return {
|
|
357
|
+
content: text("Invalid subcommand (contains whitespace)."),
|
|
358
|
+
isError: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { out, code } = await runAgentReach(
|
|
363
|
+
runtimeRoot,
|
|
364
|
+
[subcommand, ...args],
|
|
365
|
+
{ timeoutMs }
|
|
366
|
+
);
|
|
367
|
+
return { content: text(out), isError: code !== 0 };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (name === "reach_list_commands") {
|
|
371
|
+
const { out, code } = await runAgentReach(runtimeRoot, ["--help"], {
|
|
372
|
+
timeoutMs: 2 * 60 * 1000,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Parse only from stdout/stderr combined output we already include.
|
|
376
|
+
const helpText = out;
|
|
377
|
+
const commands = parseCommandsFromHelp(helpText);
|
|
378
|
+
|
|
379
|
+
const payload = [
|
|
380
|
+
"# parsed_commands",
|
|
381
|
+
commands.length ? commands.join("\n") : "(no commands parsed; see help output below)",
|
|
382
|
+
"",
|
|
383
|
+
out,
|
|
384
|
+
].join("\n");
|
|
385
|
+
|
|
386
|
+
return { content: text(payload), isError: code !== 0 };
|
|
387
|
+
}
|
|
388
|
+
|
|
249
389
|
return { content: text(`Unknown tool: ${name}`), isError: true };
|
|
250
390
|
} catch (e) {
|
|
251
391
|
const msg = e?.stack || e?.message || String(e);
|