@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.
Files changed (3) hide show
  1. package/README.md +153 -252
  2. package/index.js +165 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,252 +1,153 @@
1
- Agent-Reach MCP (stdio)
2
-
3
- A production-ready MCP (Model Context Protocol) stdio server for OpenCode / CC-Switch.
4
-
5
- This package automatically installs and upgrades Agent-Reach on every tool call, ensuring you always run the latest version from GitHub main.
6
-
7
- Designed for:
8
-
9
- One-click activation via npx
10
-
11
- ✅ Always-latest behavior
12
-
13
- ✅ Self-healing Python environment
14
-
15
- Reusable across machines
16
-
17
- Zero manual dependency management
18
-
19
- ✅ Works with OpenCode + CC-Switch
20
-
21
- 🚀 What This Package Does
22
-
23
- When enabled as an MCP server:
24
-
25
- Creates a dedicated Python virtual environment under your user cache directory
26
-
27
- Automatically upgrades Agent-Reach from:
28
-
29
- https://github.com/Panniantong/agent-reach/archive/main.zip
30
-
31
- Executes Agent-Reach CLI commands
32
-
33
- Returns structured output to the LLM via MCP
34
-
35
- This guarantees:
36
-
37
- 🔁 Always running latest Agent-Reach
38
-
39
- 🔧 No manual pip management
40
-
41
- ♻️ Environment automatically recreated if broken
42
-
43
- 💡 Fully stateless from OpenCode’s perspective
44
-
45
- 🧠 Architecture Overview
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)
55
-
56
- The Python environment is stored in:
57
-
58
- Windows:
59
-
60
- %LOCALAPPDATA%\agent-reach-mcp\runtime\venv
61
-
62
- macOS:
63
-
64
- ~/Library/Caches/agent-reach-mcp/runtime/venv
65
-
66
- Linux:
67
-
68
- ~/.cache/agent-reach-mcp/runtime/venv
69
- 📦 Installation (Local Development)
70
- npm install
71
- node index.js
72
-
73
- If the process starts without crashing, it is working correctly (stdio server waits for input).
74
-
75
- 📤 Publish to npm
76
-
77
- 1️⃣ Edit package.json name:
78
-
79
- "name": "@your-scope/agent-reach-mcp"
80
-
81
- or without scope:
82
-
83
- "name": "agent-reach-mcp-yourname"
84
-
85
- 2️⃣ Publish:
86
-
87
- npm login
88
- npm publish --access public
89
- ⚙️ Using With CC-Switch
90
-
91
- Add this configuration:
92
-
93
- {
94
- "command": "npx",
95
- "args": ["-y", "@your-scope/agent-reach-mcp@latest"],
96
- "type": "stdio"
97
- }
98
-
99
- Then:
100
-
101
- Enable MCP
102
-
103
- Restart OpenCode
104
-
105
- Tools will be automatically available
106
-
107
- 🛠 Available MCP Tools
108
- 1️⃣ reach_ensure
109
-
110
- Ensures:
111
-
112
- Python venv exists
113
-
114
- Agent-Reach upgraded to latest
115
-
116
- Returns installation logs.
117
-
118
- 2️⃣ reach_doctor
119
-
120
- Runs:
121
-
122
- agent-reach doctor
123
-
124
- Automatically upgrades before execution.
125
-
126
- 3️⃣ reach_read
127
-
128
- Runs:
129
-
130
- agent-reach read <url>
131
-
132
- Automatically upgrades before execution.
133
-
134
- Example input:
135
-
136
- {
137
- "url": "https://example.com"
138
- }
139
- 🔄 Always Latest Behavior
140
-
141
- This package intentionally prioritizes:
142
-
143
- Latest > Stability
144
-
145
- On every tool call:
146
-
147
- pip install -U https://github.com/Panniantong/agent-reach/archive/main.zip
148
-
149
- This ensures:
150
-
151
- New features immediately available
152
-
153
- Bug fixes applied instantly
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
- * A minimal, robust MCP stdio server that:
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
- * NOTE: This favors "latest" over strict stability, per your requirement.
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.map((c) => `${c.cmd} ${c.args.join(" ")}`).join(" | ");
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(pythonExe, ["-m", "pip", "install", "-U", "pip", "setuptools", "wheel"], {
137
- timeoutMs: 5 * 60 * 1000,
138
- });
140
+ await run(
141
+ pythonExe,
142
+ ["-m", "pip", "install", "-U", "pip", "setuptools", "wheel"],
143
+ { timeoutMs: 5 * 60 * 1000 }
144
+ );
139
145
 
140
- // Your "always latest" requirement:
146
+ // ALWAYS LATEST (main branch zip)
141
147
  const pkgUrl = "https://github.com/Panniantong/agent-reach/archive/main.zip";
142
- const installRes = await run(pythonExe, ["-m", "pip", "install", "-U", pkgUrl], {
143
- timeoutMs: 10 * 60 * 1000,
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(root);
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: 10 * 60 * 1000 });
178
+ execRes = await run(agentReachExe, args, { timeoutMs });
167
179
  } else {
168
- // Fallback: module import. (May vary; best effort.)
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.1.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 { content: text("Invalid url. Must start with http(s)://"), isError: true };
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsbofmusic/agent-reach-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "MCP stdio server that auto-installs/updates Agent-Reach and exposes reach_* tools.",
5
5
  "license": "MIT",
6
6
  "type": "module",