@bsbofmusic/agent-reach-mcp 0.1.1 → 0.2.1
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 +153 -252
- package/index.js +165 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,252 +1,153 @@
|
|
|
1
|
-
Agent-Reach MCP
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
agent-reach
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
No stale installs
|
|
156
|
-
|
|
157
|
-
⚠️ If upstream breaks, you get upstream behavior immediately (by design).
|
|
158
|
-
|
|
159
|
-
🛡 Stability Notes
|
|
160
|
-
|
|
161
|
-
This MCP server:
|
|
162
|
-
|
|
163
|
-
Automatically recreates venv if deleted
|
|
164
|
-
|
|
165
|
-
Upgrades pip tooling
|
|
166
|
-
|
|
167
|
-
Fallbacks to python -m agent_reach if console script missing
|
|
168
|
-
|
|
169
|
-
Captures stdout/stderr
|
|
170
|
-
|
|
171
|
-
Handles timeouts
|
|
172
|
-
|
|
173
|
-
Returns structured MCP errors
|
|
174
|
-
|
|
175
|
-
🧩 Requirements
|
|
176
|
-
|
|
177
|
-
Node.js >= 18
|
|
178
|
-
|
|
179
|
-
Python 3 available in PATH
|
|
180
|
-
|
|
181
|
-
pip available
|
|
182
|
-
|
|
183
|
-
Internet connection
|
|
184
|
-
|
|
185
|
-
🔍 Why Use MCP Instead of Direct CLI?
|
|
186
|
-
|
|
187
|
-
Because:
|
|
188
|
-
|
|
189
|
-
OpenCode forgets memory after restart
|
|
190
|
-
|
|
191
|
-
MCP ensures environment is rebuilt automatically
|
|
192
|
-
|
|
193
|
-
Tools become persistent capabilities
|
|
194
|
-
|
|
195
|
-
One-click activation via CC-Switch
|
|
196
|
-
|
|
197
|
-
No manual installation per machine
|
|
198
|
-
|
|
199
|
-
🏗 Design Philosophy
|
|
200
|
-
|
|
201
|
-
Minimal
|
|
202
|
-
|
|
203
|
-
Stateless
|
|
204
|
-
|
|
205
|
-
Self-healing
|
|
206
|
-
|
|
207
|
-
Always current
|
|
208
|
-
|
|
209
|
-
Reusable anywhere via npx
|
|
210
|
-
|
|
211
|
-
📄 License
|
|
212
|
-
|
|
213
|
-
MIT
|
|
214
|
-
|
|
215
|
-
💡 Example OpenCode Prompt
|
|
216
|
-
|
|
217
|
-
After enabling MCP:
|
|
218
|
-
|
|
219
|
-
Use reach_read to analyze https://example.com
|
|
220
|
-
|
|
221
|
-
Or:
|
|
222
|
-
|
|
223
|
-
Run reach_doctor and summarize health status
|
|
224
|
-
🔮 Future Extensions (Optional)
|
|
225
|
-
|
|
226
|
-
Possible future improvements:
|
|
227
|
-
|
|
228
|
-
Version pin mode
|
|
229
|
-
|
|
230
|
-
Automatic rollback on failure
|
|
231
|
-
|
|
232
|
-
Dependency preflight checks
|
|
233
|
-
|
|
234
|
-
Docker isolation mode
|
|
235
|
-
|
|
236
|
-
Logging level control
|
|
237
|
-
|
|
238
|
-
Configurable update strategy
|
|
239
|
-
|
|
240
|
-
✅ Summary
|
|
241
|
-
|
|
242
|
-
You now have:
|
|
243
|
-
|
|
244
|
-
A reusable MCP server
|
|
245
|
-
|
|
246
|
-
Publishable npm package
|
|
247
|
-
|
|
248
|
-
Always-latest Agent-Reach integration
|
|
249
|
-
|
|
250
|
-
Zero manual maintenance workflow
|
|
251
|
-
|
|
252
|
-
One-click OpenCode integration
|
|
1
|
+
# Agent-Reach MCP
|
|
2
|
+
|
|
3
|
+
MCP (stdio) server that provides full access to Agent-Reach for OpenCode / CC-Switch.
|
|
4
|
+
|
|
5
|
+
## What is Agent-Reach?
|
|
6
|
+
|
|
7
|
+
Agent-Reach gives your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
|
|
8
|
+
|
|
9
|
+
**Key Features:**
|
|
10
|
+
- Web page reading (via Jina Reader)
|
|
11
|
+
- YouTube video/subtitle extraction
|
|
12
|
+
- Twitter/X reading and search
|
|
13
|
+
- Reddit search and reading
|
|
14
|
+
- GitHub repository access
|
|
15
|
+
- Bilibili video extraction
|
|
16
|
+
- XiaoHongShu (Little Red Book) search
|
|
17
|
+
- Full-text web search (via Exa)
|
|
18
|
+
- RSS feed parsing
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### Quick Start (CC-Switch / OpenCode)
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "@bsbofmusic/agent-reach-mcp@latest"],
|
|
28
|
+
"type": "stdio"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Local Development
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
node index.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Available Tools
|
|
40
|
+
|
|
41
|
+
| Tool | Description |
|
|
42
|
+
|------|-------------|
|
|
43
|
+
| `reach_ensure` | Create/update Python venv and install latest Agent-Reach |
|
|
44
|
+
| `reach_doctor` | Run `agent-reach doctor` - diagnose all channels |
|
|
45
|
+
| `reach_read` | Read a URL (webpage/video/repo) |
|
|
46
|
+
| `reach_exec` | Execute any `agent-reach <subcommand>` command |
|
|
47
|
+
| `reach_list_commands` | List available subcommands from `agent-reach --help` |
|
|
48
|
+
|
|
49
|
+
## Usage Examples
|
|
50
|
+
|
|
51
|
+
### First Time Setup
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{ "subcommand": "install", "args": ["--env=auto"] }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Read a Webpage
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{ "url": "https://example.com" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Read YouTube Video
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{ "url": "https://youtube.com/watch?v=xxx" }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Configure Twitter Cookies
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"subcommand": "configure",
|
|
74
|
+
"args": ["twitter-cookies", "your_cookie_string"]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Search Twitter
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"subcommand": "search-twitter",
|
|
83
|
+
"args": ["your search query"]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Configure Proxy
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"subcommand": "configure",
|
|
92
|
+
"args": ["proxy", "http://user:pass@ip:port"]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Search XiaoHongShu
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"subcommand": "search-xhs",
|
|
101
|
+
"args": ["keywords"]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Environment Requirements
|
|
106
|
+
|
|
107
|
+
- Node.js >= 18
|
|
108
|
+
- Python 3 (in PATH, or `py` on Windows)
|
|
109
|
+
- Internet access to GitHub
|
|
110
|
+
|
|
111
|
+
## How It Works
|
|
112
|
+
|
|
113
|
+
Every tool call triggers:
|
|
114
|
+
|
|
115
|
+
1. Create/reuse Python venv in user cache directory
|
|
116
|
+
2. Run `pip install -U https://github.com/Panniantong/agent-reach/archive/main.zip`
|
|
117
|
+
3. Execute the requested `agent-reach ...` command
|
|
118
|
+
4. Return stdout/stderr to the AI
|
|
119
|
+
|
|
120
|
+
### Venv Location
|
|
121
|
+
|
|
122
|
+
- Windows: `%LOCALAPPDATA%\agent-reach-mcp\runtime\venv`
|
|
123
|
+
- macOS: `~/Library/Caches/agent-reach-mcp/runtime/venv`
|
|
124
|
+
- Linux: `~/.cache/agent-reach-mcp/runtime/venv`
|
|
125
|
+
|
|
126
|
+
## Recommended Workflow
|
|
127
|
+
|
|
128
|
+
1. **Initial Setup**: Run `reach_ensure` to install/update Agent-Reach
|
|
129
|
+
2. **Diagnosis**: Run `reach_doctor` to check channel status
|
|
130
|
+
3. **Discovery**: Run `reach_list_commands` to see available commands
|
|
131
|
+
4. **Usage**: Use `reach_exec` for any Agent-Reach command
|
|
132
|
+
|
|
133
|
+
## FAQ
|
|
134
|
+
|
|
135
|
+
**Q: Why can't I use a certain platform?**
|
|
136
|
+
|
|
137
|
+
A: Check with `reach_doctor` - you may need cookies, proxy, or docker configured.
|
|
138
|
+
|
|
139
|
+
**Q: Does it update every call?会不会慢?**
|
|
140
|
+
|
|
141
|
+
A: Yes, this is "Always-Latest" strategy. First call or upstream updates may be slow, but you'll always have the latest version.
|
|
142
|
+
|
|
143
|
+
**Q: How to discover available commands after restart?**
|
|
144
|
+
|
|
145
|
+
A: Run `reach_list_commands` - this enables "capability self-discovery" for memory-less models.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
150
|
+
|
|
151
|
+
## Links
|
|
152
|
+
|
|
153
|
+
- [Agent-Reach GitHub](https://github.com/Panniantong/Agent-Reach)
|
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);
|