crw-mcp 0.27.0 → 0.28.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/crw-mcp.js +102 -40
- package/package.json +13 -7
package/bin/crw-mcp.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Handle install/init subcommands before delegating to the Rust binary.
|
|
4
|
-
// `init` = skill only; `install` = skill + MCP server.
|
|
5
|
-
if (process.argv[2] === "init") {
|
|
6
|
-
require("./init.js");
|
|
7
|
-
process.exit(0);
|
|
8
|
-
}
|
|
9
|
-
if (process.argv[2] === "install") {
|
|
10
|
-
require("./install.js");
|
|
11
|
-
process.exit(0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
3
|
const { spawnSync } = require("child_process");
|
|
4
|
+
const crypto = require("crypto");
|
|
15
5
|
const fs = require("fs");
|
|
16
6
|
const path = require("path");
|
|
17
7
|
const os = require("os");
|
|
@@ -37,13 +27,6 @@ const PLATFORMS = {
|
|
|
37
27
|
const key = `${process.platform}-${process.arch}`;
|
|
38
28
|
const plat = PLATFORMS[key];
|
|
39
29
|
|
|
40
|
-
if (!plat) {
|
|
41
|
-
console.error(
|
|
42
|
-
`crw-mcp: unsupported platform ${key}. Supported: ${Object.keys(PLATFORMS).join(", ")}`
|
|
43
|
-
);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
30
|
const binName = `crw-mcp${process.platform === "win32" ? ".exe" : ""}`;
|
|
48
31
|
|
|
49
32
|
// 1. Explicit override.
|
|
@@ -73,28 +56,41 @@ function cacheDir() {
|
|
|
73
56
|
return path.join(base, "crw-mcp", `v${VERSION}`);
|
|
74
57
|
}
|
|
75
58
|
|
|
76
|
-
|
|
59
|
+
// Always resolves a Buffer: the archive is verified in memory, so an unverified
|
|
60
|
+
// byte never reaches disk at all.
|
|
61
|
+
function httpsGet(url, redirects = 0) {
|
|
77
62
|
return new Promise((resolve, reject) => {
|
|
78
63
|
https
|
|
79
64
|
.get(url, { headers: { "User-Agent": `crw-mcp/${VERSION}` } }, (res) => {
|
|
80
65
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
81
66
|
res.resume();
|
|
82
67
|
if (redirects > 5) return reject(new Error("too many redirects"));
|
|
83
|
-
return resolve(httpsGet(res.headers.location,
|
|
68
|
+
return resolve(httpsGet(res.headers.location, redirects + 1));
|
|
84
69
|
}
|
|
85
70
|
if (res.statusCode !== 200) {
|
|
86
71
|
res.resume();
|
|
87
72
|
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
88
73
|
}
|
|
89
|
-
const
|
|
90
|
-
res.
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
const chunks = [];
|
|
75
|
+
res.on("data", (c) => chunks.push(c));
|
|
76
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
77
|
+
res.on("error", reject);
|
|
93
78
|
})
|
|
94
79
|
.on("error", reject);
|
|
95
80
|
});
|
|
96
81
|
}
|
|
97
82
|
|
|
83
|
+
// Parse one entry out of a coreutils-style SHA256SUMS. Written on Linux, read
|
|
84
|
+
// here on every platform, so tolerate CRLF and the "*" binary-mode marker.
|
|
85
|
+
function digestFor(sumsText, asset) {
|
|
86
|
+
for (const line of sumsText.split(/\r?\n/)) {
|
|
87
|
+
const parts = line.trim().split(/\s+/);
|
|
88
|
+
if (parts.length === 2 && parts[1].replace(/^\*/, "") === asset)
|
|
89
|
+
return parts[0].toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
98
94
|
// 3. Download the matching release asset and extract it (cached per version).
|
|
99
95
|
async function fromDownload() {
|
|
100
96
|
const dir = cacheDir();
|
|
@@ -102,26 +98,69 @@ async function fromDownload() {
|
|
|
102
98
|
if (fs.existsSync(bin)) return bin;
|
|
103
99
|
|
|
104
100
|
fs.mkdirSync(dir, { recursive: true });
|
|
105
|
-
const
|
|
106
|
-
const url =
|
|
101
|
+
const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
102
|
+
const url = `${base}/${plat.asset}`;
|
|
103
|
+
|
|
104
|
+
// Fail closed: fetch the expected digest first, so a release without one is
|
|
105
|
+
// refused before anything is downloaded rather than after.
|
|
106
|
+
let sums;
|
|
107
|
+
try {
|
|
108
|
+
sums = (await httpsGet(`${base}/SHA256SUMS`)).toString("utf8");
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// Only a 404 means the release genuinely lacks checksums. Reporting a
|
|
111
|
+
// proxy or DNS failure as "our release is broken" sends users to file
|
|
112
|
+
// issues against a release that is fine.
|
|
113
|
+
throw new Error(
|
|
114
|
+
/^HTTP 404 /.test(e.message)
|
|
115
|
+
? `release v${VERSION} publishes no SHA256SUMS, so ${plat.asset} cannot be verified`
|
|
116
|
+
: `could not fetch SHA256SUMS for v${VERSION}: ${e.message}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
const expected = digestFor(sums, plat.asset);
|
|
120
|
+
if (!expected) {
|
|
121
|
+
throw new Error(`${plat.asset} is not listed in SHA256SUMS for v${VERSION}`);
|
|
122
|
+
}
|
|
107
123
|
|
|
108
124
|
// stderr only: stdout is the MCP (JSON-RPC) channel.
|
|
109
125
|
console.error(`crw-mcp: downloading ${plat.asset} (v${VERSION})...`);
|
|
110
|
-
await httpsGet(url
|
|
111
|
-
|
|
112
|
-
const tarArgs = plat.asset.endsWith(".zip")
|
|
113
|
-
? ["-xf", archive, "-C", dir]
|
|
114
|
-
: ["-xzf", archive, "-C", dir];
|
|
115
|
-
const x = spawnSync("tar", tarArgs, { stdio: "inherit" });
|
|
116
|
-
if (x.status !== 0) {
|
|
117
|
-
throw new Error(`failed to extract ${plat.asset} (tar exit ${x.status})`);
|
|
118
|
-
}
|
|
119
|
-
fs.rmSync(archive, { force: true });
|
|
126
|
+
const data = await httpsGet(url);
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
const actual = crypto.createHash("sha256").update(data).digest("hex");
|
|
129
|
+
if (actual !== expected) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`checksum mismatch for ${plat.asset}: expected ${expected}, got ${actual}. ` +
|
|
132
|
+
`Refusing to run it.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
// Stage per process, so two cold starts (MCP clients do spawn several at
|
|
136
|
+
// once on first configure) cannot truncate each other's archive or delete
|
|
137
|
+
// each other's extraction. The binary is moved in only once it is whole:
|
|
138
|
+
// `tar` applies the archive's mode from the first byte and the cache hit
|
|
139
|
+
// above gates on bare existsSync, so an interrupted extraction (Ctrl-C, disk
|
|
140
|
+
// full, OOM) would otherwise leave a truncated executable that every later
|
|
141
|
+
// run spawns. Python needs no staging: it writes the member itself, so the
|
|
142
|
+
// exec bit only arrives after the copy and its os.access(X_OK) gate rejects
|
|
143
|
+
// a partial file and re-downloads.
|
|
144
|
+
const stage = fs.mkdtempSync(path.join(dir, ".stage-"));
|
|
145
|
+
const archive = path.join(stage, plat.asset);
|
|
146
|
+
try {
|
|
147
|
+
fs.writeFileSync(archive, data);
|
|
148
|
+
const tarArgs = plat.asset.endsWith(".zip")
|
|
149
|
+
? ["-xf", archive, "-C", stage]
|
|
150
|
+
: ["-xzf", archive, "-C", stage];
|
|
151
|
+
const x = spawnSync("tar", tarArgs, { stdio: "inherit" });
|
|
152
|
+
if (x.status !== 0) {
|
|
153
|
+
throw new Error(`failed to extract ${plat.asset} (tar exit ${x.status})`);
|
|
154
|
+
}
|
|
155
|
+
const staged = path.join(stage, binName);
|
|
156
|
+
if (!fs.existsSync(staged)) {
|
|
157
|
+
throw new Error(`binary ${binName} not found in ${plat.asset}`);
|
|
158
|
+
}
|
|
159
|
+
if (process.platform !== "win32") fs.chmodSync(staged, 0o755);
|
|
160
|
+
fs.renameSync(staged, bin);
|
|
161
|
+
} finally {
|
|
162
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
123
163
|
}
|
|
124
|
-
if (process.platform !== "win32") fs.chmodSync(bin, 0o755);
|
|
125
164
|
return bin;
|
|
126
165
|
}
|
|
127
166
|
|
|
@@ -129,7 +168,29 @@ async function resolveBinary() {
|
|
|
129
168
|
return fromEnv() || fromPackage() || (await fromDownload());
|
|
130
169
|
}
|
|
131
170
|
|
|
132
|
-
|
|
171
|
+
module.exports = { digestFor, fromDownload, cacheDir, plat, binName };
|
|
172
|
+
|
|
173
|
+
// Only run when invoked as the CLI, so tests can require this file.
|
|
174
|
+
if (require.main === module) {
|
|
175
|
+
// Handle install/init subcommands before delegating to the Rust binary.
|
|
176
|
+
// `init` = skill only; `install` = skill + MCP server. Both are file copies
|
|
177
|
+
// that need no binary, so they must run before the platform check: a 32-bit
|
|
178
|
+
// Pi or FreeBSD user can still install the skill.
|
|
179
|
+
if (process.argv[2] === "init") {
|
|
180
|
+
require("./init.js");
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
if (process.argv[2] === "install") {
|
|
184
|
+
require("./install.js");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
if (!plat) {
|
|
188
|
+
console.error(
|
|
189
|
+
`crw-mcp: unsupported platform ${key}. Supported: ${Object.keys(PLATFORMS).join(", ")}`
|
|
190
|
+
);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
resolveBinary()
|
|
133
194
|
.then((bin) => {
|
|
134
195
|
const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
|
|
135
196
|
process.exit(result.status ?? 1);
|
|
@@ -142,3 +203,4 @@ resolveBinary()
|
|
|
142
203
|
);
|
|
143
204
|
process.exit(1);
|
|
144
205
|
});
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crw-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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>",
|
|
7
|
+
"homepage": "https://fastcrw.com",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "https://github.com/us/crw.git"
|
|
10
11
|
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/us/crw/issues"
|
|
14
|
+
},
|
|
11
15
|
"keywords": [
|
|
12
16
|
"mcp",
|
|
13
17
|
"web-scraping",
|
|
14
18
|
"ai-agent",
|
|
15
19
|
"claude",
|
|
16
20
|
"cursor",
|
|
17
|
-
"firecrawl",
|
|
18
21
|
"crawl",
|
|
19
22
|
"scrape"
|
|
20
23
|
],
|
|
@@ -24,6 +27,9 @@
|
|
|
24
27
|
"engines": {
|
|
25
28
|
"node": ">=18"
|
|
26
29
|
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test test/*.test.js"
|
|
32
|
+
},
|
|
27
33
|
"files": [
|
|
28
34
|
"bin/crw-mcp.js",
|
|
29
35
|
"bin/init.js",
|
|
@@ -32,9 +38,9 @@
|
|
|
32
38
|
"skills/SKILL.md"
|
|
33
39
|
],
|
|
34
40
|
"optionalDependencies": {
|
|
35
|
-
"crw-mcp-darwin-x64": "0.
|
|
36
|
-
"crw-mcp-darwin-arm64": "0.
|
|
37
|
-
"crw-mcp-linux-x64": "0.
|
|
38
|
-
"crw-mcp-linux-arm64": "0.
|
|
41
|
+
"crw-mcp-darwin-x64": "0.28.0",
|
|
42
|
+
"crw-mcp-darwin-arm64": "0.28.0",
|
|
43
|
+
"crw-mcp-linux-x64": "0.28.0",
|
|
44
|
+
"crw-mcp-linux-arm64": "0.28.0"
|
|
39
45
|
}
|
|
40
46
|
}
|