crw-mcp 0.30.0 → 0.32.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/bin/agents.js +75 -15
- package/bin/init.js +1 -1
- package/bin/install.js +13 -3
- package/package.json +5 -5
- package/skills/SKILL.md +5 -5
package/bin/agents.js
CHANGED
|
@@ -53,21 +53,51 @@ function installSkill(agent, skillContent) {
|
|
|
53
53
|
const dir = path.join(home, agent.configDir, "skills", "crw");
|
|
54
54
|
fs.mkdirSync(dir, { recursive: true });
|
|
55
55
|
const file = path.join(dir, "SKILL.md");
|
|
56
|
-
|
|
56
|
+
atomicWriteText(file, skillContent);
|
|
57
57
|
return file;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function readJson(file) {
|
|
61
|
+
if (!fs.existsSync(file)) return {};
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const value = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
64
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
65
|
+
throw new Error("top-level value must be an object");
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`refusing to overwrite malformed JSON in ${file}: ${error.message}`);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
function
|
|
73
|
+
function ensureObject(value, label, file) {
|
|
74
|
+
if (value === undefined) return {};
|
|
75
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
76
|
+
throw new Error(`refusing to replace non-object ${label} in ${file}`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function backupOnce(file) {
|
|
82
|
+
if (!fs.existsSync(file)) return;
|
|
83
|
+
const backup = `${file}.crw-backup`;
|
|
84
|
+
if (!fs.existsSync(backup)) fs.copyFileSync(file, backup);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function atomicWriteText(file, content) {
|
|
69
88
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
70
|
-
|
|
89
|
+
backupOnce(file);
|
|
90
|
+
const tmp = `${file}.crw-tmp-${process.pid}-${Date.now()}`;
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(tmp, content, { encoding: "utf-8", mode: 0o600 });
|
|
93
|
+
fs.renameSync(tmp, file);
|
|
94
|
+
} finally {
|
|
95
|
+
try { fs.unlinkSync(tmp); } catch { /* already renamed or never created */ }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function writeJson(file, obj) {
|
|
100
|
+
atomicWriteText(file, JSON.stringify(obj, null, 2) + "\n");
|
|
71
101
|
}
|
|
72
102
|
|
|
73
103
|
/**
|
|
@@ -90,8 +120,15 @@ function installMcp(agent, env) {
|
|
|
90
120
|
|
|
91
121
|
if (m.kind === "claude-cli") {
|
|
92
122
|
// add-json avoids the `-e` variadic-name pitfall and merges ~/.claude.json
|
|
93
|
-
// (user scope) correctly.
|
|
123
|
+
// (user scope) correctly. Probe before touching state, then restore the
|
|
124
|
+
// exact prior user config if replacement fails.
|
|
94
125
|
const server = { command: MCP_CMD, args: MCP_ARGS, ...(hasEnv ? { env } : {}) };
|
|
126
|
+
const probe = spawnSync("claude", ["--version"], { stdio: "ignore" });
|
|
127
|
+
if (probe.error || probe.status !== 0) {
|
|
128
|
+
throw new Error("Claude Code CLI was detected but is not launchable");
|
|
129
|
+
}
|
|
130
|
+
const configFile = path.join(home, ".claude.json");
|
|
131
|
+
const original = fs.existsSync(configFile) ? fs.readFileSync(configFile) : null;
|
|
95
132
|
spawnSync("claude", ["mcp", "remove", "--scope", "user", "crw"], { stdio: "ignore" });
|
|
96
133
|
const r = spawnSync(
|
|
97
134
|
"claude",
|
|
@@ -99,7 +136,12 @@ function installMcp(agent, env) {
|
|
|
99
136
|
{ stdio: "ignore" },
|
|
100
137
|
);
|
|
101
138
|
if (r.error || r.status !== 0) {
|
|
102
|
-
|
|
139
|
+
if (original) {
|
|
140
|
+
fs.writeFileSync(configFile, original);
|
|
141
|
+
} else {
|
|
142
|
+
try { fs.unlinkSync(configFile); } catch { /* no file was created */ }
|
|
143
|
+
}
|
|
144
|
+
throw new Error("Claude Code rejected the MCP registration; previous config restored");
|
|
103
145
|
}
|
|
104
146
|
return "MCP via claude CLI (user scope)";
|
|
105
147
|
}
|
|
@@ -107,7 +149,7 @@ function installMcp(agent, env) {
|
|
|
107
149
|
if (m.kind === "json") {
|
|
108
150
|
const file = path.join(home, m.file);
|
|
109
151
|
const cfg = readJson(file);
|
|
110
|
-
cfg.mcpServers = cfg.mcpServers
|
|
152
|
+
cfg.mcpServers = ensureObject(cfg.mcpServers, "mcpServers", file);
|
|
111
153
|
cfg.mcpServers.crw = {
|
|
112
154
|
...(m.withType ? { type: "stdio" } : {}),
|
|
113
155
|
command: MCP_CMD,
|
|
@@ -122,7 +164,7 @@ function installMcp(agent, env) {
|
|
|
122
164
|
const file = path.join(home, m.file);
|
|
123
165
|
const cfg = readJson(file);
|
|
124
166
|
if (!cfg.$schema) cfg.$schema = "https://opencode.ai/config.json";
|
|
125
|
-
cfg.mcp = cfg.mcp
|
|
167
|
+
cfg.mcp = ensureObject(cfg.mcp, "mcp", file);
|
|
126
168
|
cfg.mcp.crw = {
|
|
127
169
|
type: "local",
|
|
128
170
|
command: [MCP_CMD, ...MCP_ARGS],
|
|
@@ -140,20 +182,38 @@ function installMcp(agent, env) {
|
|
|
140
182
|
} catch {
|
|
141
183
|
/* new file */
|
|
142
184
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
// The installer owns the crw section. Replace it so a previous install
|
|
186
|
+
// with embedded env credentials cannot override a later `--from-config`
|
|
187
|
+
// setup (and so re-runs remain idempotent).
|
|
188
|
+
toml = removeOwnedCodexSections(toml);
|
|
146
189
|
const envLines = hasEnv
|
|
147
190
|
? "\n[mcp_servers.crw.env]\n" +
|
|
148
191
|
Object.entries(env).map(([k, v]) => `${k} = "${v}"`).join("\n") + "\n"
|
|
149
192
|
: "";
|
|
150
193
|
const block = `\n[mcp_servers.crw]\ncommand = "${MCP_CMD}"\nargs = [${MCP_ARGS.map((a) => `"${a}"`).join(", ")}]\n${envLines}`;
|
|
151
|
-
|
|
152
|
-
|
|
194
|
+
const next = toml + (toml && !toml.endsWith("\n") ? "\n" : "") + block;
|
|
195
|
+
atomicWriteText(file, next);
|
|
153
196
|
return `MCP → ${m.file}`;
|
|
154
197
|
}
|
|
155
198
|
|
|
156
199
|
return "MCP: unsupported";
|
|
157
200
|
}
|
|
158
201
|
|
|
202
|
+
function removeOwnedCodexSections(toml) {
|
|
203
|
+
const lines = toml.split("\n");
|
|
204
|
+
const kept = [];
|
|
205
|
+
let skipping = false;
|
|
206
|
+
for (const line of lines) {
|
|
207
|
+
const isSection = /^\s*\[[^\]]+\]/.test(line);
|
|
208
|
+
const isOwned = /^\s*\[mcp_servers\.crw(?:\.[^\]]+)?\]\s*(?:#.*)?$/.test(line);
|
|
209
|
+
if (isOwned) {
|
|
210
|
+
skipping = true;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (isSection) skipping = false;
|
|
214
|
+
if (!skipping) kept.push(line);
|
|
215
|
+
}
|
|
216
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
217
|
+
}
|
|
218
|
+
|
|
159
219
|
module.exports = { AGENTS, detectAgents, getApiKey, readSkill, installSkill, installMcp, home };
|
package/bin/init.js
CHANGED
|
@@ -73,6 +73,6 @@ if (apiKey) {
|
|
|
73
73
|
console.log(" export CRW_API_URL=https://api.fastcrw.com");
|
|
74
74
|
} else {
|
|
75
75
|
console.log("\nThe skill works in local mode out of the box (free, embedded).");
|
|
76
|
-
console.log("Cloud mode (managed,
|
|
76
|
+
console.log("Cloud mode (managed, 1000 free credits): https://fastcrw.com");
|
|
77
77
|
}
|
|
78
78
|
console.log("\nDocs: https://fastcrw.com/docs");
|
package/bin/install.js
CHANGED
|
@@ -36,6 +36,7 @@ Options:
|
|
|
36
36
|
--gemini-cli Gemini CLI --windsurf Windsurf
|
|
37
37
|
--api-key <key> fastcrw.com API key → cloud mode (else local/embedded)
|
|
38
38
|
--api-url <url> API base (default https://api.fastcrw.com when a key is set)
|
|
39
|
+
--from-config Read mode from ~/.config/crw/config.toml at runtime
|
|
39
40
|
-h, --help Show this help
|
|
40
41
|
|
|
41
42
|
Without flags, auto-detects installed agents. Installs the skill (SKILL.md) and
|
|
@@ -44,7 +45,8 @@ registers the "crw" MCP server. Run \`crw-mcp init\` for the skill only.
|
|
|
44
45
|
process.exit(0);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
const
|
|
48
|
+
const fromConfig = args.includes("--from-config");
|
|
49
|
+
const apiKey = fromConfig ? null : getApiKey(args);
|
|
48
50
|
const apiUrl = argValue("--api-url") || (apiKey ? "https://api.fastcrw.com" : null);
|
|
49
51
|
const env = apiKey ? { CRW_API_KEY: apiKey, CRW_API_URL: apiUrl } : {};
|
|
50
52
|
|
|
@@ -58,6 +60,7 @@ if (agents.length === 0) {
|
|
|
58
60
|
|
|
59
61
|
const skill = readSkill();
|
|
60
62
|
const results = [];
|
|
63
|
+
const failures = [];
|
|
61
64
|
for (const agent of agents) {
|
|
62
65
|
try {
|
|
63
66
|
const skillPath = installSkill(agent, skill);
|
|
@@ -65,6 +68,7 @@ for (const agent of agents) {
|
|
|
65
68
|
results.push({ name: agent.name, skillPath, mcpLabel });
|
|
66
69
|
} catch (err) {
|
|
67
70
|
console.error(` ${agent.name}: ${err.message}`);
|
|
71
|
+
failures.push(agent.name);
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -80,11 +84,17 @@ for (const r of results) {
|
|
|
80
84
|
console.log(` ${r.mcpLabel.replace(home, "~")}`);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
if (
|
|
87
|
+
if (fromConfig) {
|
|
88
|
+
console.log("\nMode: follows ~/.config/crw/config.toml (credentials are not copied into agent configs).");
|
|
89
|
+
} else if (apiKey) {
|
|
84
90
|
console.log(`\nMode: cloud — CRW_API_KEY ${apiKey.slice(0, 10)}… → ${apiUrl}`);
|
|
85
91
|
} else {
|
|
86
92
|
console.log("\nMode: local (free, embedded binary — no key needed).");
|
|
87
|
-
console.log(" Cloud mode: re-run with --api-key crw_live_… (
|
|
93
|
+
console.log(" Cloud mode: re-run with --api-key crw_live_… (1000 free credits at https://fastcrw.com).");
|
|
88
94
|
}
|
|
89
95
|
console.log("\nRestart your agent to load the MCP server.");
|
|
90
96
|
console.log("Docs: https://fastcrw.com/docs");
|
|
97
|
+
if (failures.length > 0) {
|
|
98
|
+
console.error(`\nFailed targets: ${failures.join(", ")}. Existing configs were preserved.`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crw-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "MCP server for CRW web scraper — scrape, crawl, map, extract, search, and PDF-parse tools for AI agents",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"author": "fastCRW <hello@fastcrw.com>",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"skills/SKILL.md"
|
|
39
39
|
],
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"crw-mcp-darwin-x64": "0.
|
|
42
|
-
"crw-mcp-darwin-arm64": "0.
|
|
43
|
-
"crw-mcp-linux-x64": "0.
|
|
44
|
-
"crw-mcp-linux-arm64": "0.
|
|
41
|
+
"crw-mcp-darwin-x64": "0.32.0",
|
|
42
|
+
"crw-mcp-darwin-arm64": "0.32.0",
|
|
43
|
+
"crw-mcp-linux-x64": "0.32.0",
|
|
44
|
+
"crw-mcp-linux-arm64": "0.32.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/skills/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crw
|
|
3
|
-
description: "Scrape, crawl, map, and search the web using fastCRW's native /v1 API. Use when the user needs web page content, site-wide extraction, URL discovery, or web search results. Single binary,
|
|
3
|
+
description: "Scrape, crawl, map, and search the web using fastCRW's native /v1 API. Use when the user needs web page content, site-wide extraction, URL discovery, or web search results. Single binary, 14 MB RAM; /v2 exists separately for Firecrawl migration."
|
|
4
4
|
license: AGPL-3.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: us
|
|
@@ -32,12 +32,12 @@ npx crw-mcp@latest init # skill only
|
|
|
32
32
|
|
|
33
33
|
## Authentication
|
|
34
34
|
|
|
35
|
-
- **Embedded mode** (default): No key needed — the MCP server runs a self-contained scraper in ~
|
|
36
|
-
- **Cloud mode** (fastcrw.com): Set `CRW_API_KEY=crw_live_...` and `CRW_API_URL=https://api.fastcrw.com`. Get a free key at https://fastcrw.com with
|
|
35
|
+
- **Embedded mode** (default): No key needed — the MCP server runs a self-contained scraper in ~14 MB RAM. No server required.
|
|
36
|
+
- **Cloud mode** (fastcrw.com): Set `CRW_API_KEY=crw_live_...` and `CRW_API_URL=https://api.fastcrw.com`. Get a free key at https://fastcrw.com with 1000 one-time lifetime credits (never resets, not monthly).
|
|
37
37
|
|
|
38
38
|
## MCP Tools
|
|
39
39
|
|
|
40
|
-
> **Output bounds:** By default, content is truncated to ~15 000 chars (`crw_scrape`, `crw_check_crawl_status`, `crw_parse_file`, and any page content inlined into `crw_search` results via `scrapeOptions`) and `crw_map` returns ≤ 100 URLs. Truncated results carry a `truncated: true` marker (`crw_map` also adds `totalDiscovered`). Pass `maxLength: 0` or `limit: 0` to opt out of bounding. `crw_search` does not advertise `maxLength`, so an agent will not discover it, but a hand-written client may still pass it.
|
|
40
|
+
> **Output bounds:** By default, content is truncated to ~15 000 chars (`crw_scrape`, `crw_check_crawl_status`, `crw_parse_file`, and any page content inlined into `crw_search` results via `scrapeOptions`) and `crw_map` returns ≤ 100 URLs and ≤ 100 sitemaps. Truncated results carry a `truncated: true` marker (`crw_map` also adds `totalDiscovered` for links and `totalSitemaps` for sitemaps). Pass `maxLength: 0` or `limit: 0` to opt out of bounding. `crw_search` does not advertise `maxLength`, so an agent will not discover it, but a hand-written client may still pass it.
|
|
41
41
|
|
|
42
42
|
### crw_scrape
|
|
43
43
|
|
|
@@ -194,7 +194,7 @@ Available fields: `title`, `url`, `description`, `snippet`, `position`,
|
|
|
194
194
|
|
|
195
195
|
## Links
|
|
196
196
|
|
|
197
|
-
- Cloud API: https://fastcrw.com —
|
|
197
|
+
- Cloud API: https://fastcrw.com — 1000 one-time lifetime free credits (never resets, not monthly)
|
|
198
198
|
- Docs: https://docs.fastcrw.com
|
|
199
199
|
- GitHub: https://github.com/us/crw
|
|
200
200
|
- Native API: `/v1/scrape`, `/v1/crawl`, `/v1/map`, and `/v1/search` are the recommended routes for new CRW integrations
|