crw-mcp 0.37.0 → 0.37.2

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 (2) hide show
  1. package/bin/crw-mcp.js +79 -10
  2. package/package.json +5 -5
package/bin/crw-mcp.js CHANGED
@@ -5,7 +5,9 @@ const crypto = require("crypto");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
7
  const os = require("os");
8
+ const http = require("http");
8
9
  const https = require("https");
10
+ const tls = require("tls");
9
11
 
10
12
  const VERSION = require("../package.json").version;
11
13
  const REPO = "fastcrw/crw";
@@ -56,16 +58,55 @@ function cacheDir() {
56
58
  return path.join(base, "crw-mcp", `v${VERSION}`);
57
59
  }
58
60
 
61
+ // HTTPS_PROXY / HTTP_PROXY support: Node's https ignores them, and restricted
62
+ // networks often only allow egress through one. Plain-http proxies only (the
63
+ // usual corporate setup), tunnelled with CONNECT. ponytail: NO_PROXY is not
64
+ // honoured, add when someone needs a bypass list.
65
+ function proxyAgent() {
66
+ const raw =
67
+ process.env.HTTPS_PROXY || process.env.https_proxy ||
68
+ process.env.HTTP_PROXY || process.env.http_proxy;
69
+ if (!raw) return undefined;
70
+ const proxy = new URL(raw);
71
+ const headers = {};
72
+ if (proxy.username) {
73
+ const cred = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;
74
+ headers["Proxy-Authorization"] = `Basic ${Buffer.from(cred).toString("base64")}`;
75
+ }
76
+ const agent = new https.Agent();
77
+ agent.createConnection = (opts, cb) => {
78
+ const req = http.request({
79
+ host: proxy.hostname,
80
+ port: proxy.port || 80,
81
+ method: "CONNECT",
82
+ path: `${opts.host}:${opts.port || 443}`,
83
+ headers,
84
+ });
85
+ req.once("connect", (res, socket) => {
86
+ if (res.statusCode !== 200) {
87
+ socket.destroy();
88
+ return cb(new Error(`proxy CONNECT failed: HTTP ${res.statusCode}`));
89
+ }
90
+ cb(null, tls.connect({ socket, servername: opts.servername || opts.host }));
91
+ });
92
+ req.once("error", cb);
93
+ req.end();
94
+ };
95
+ return agent;
96
+ }
97
+
59
98
  // Always resolves a Buffer: the archive is verified in memory, so an unverified
60
99
  // byte never reaches disk at all.
61
- function httpsGet(url, redirects = 0) {
100
+ function httpsGet(url, extraHeaders = {}, redirects = 0) {
62
101
  return new Promise((resolve, reject) => {
102
+ const headers = { "User-Agent": `crw-mcp/${VERSION}`, ...extraHeaders };
63
103
  https
64
- .get(url, { headers: { "User-Agent": `crw-mcp/${VERSION}` } }, (res) => {
104
+ .get(url, { headers, agent: proxyAgent() }, (res) => {
65
105
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
66
106
  res.resume();
67
107
  if (redirects > 5) return reject(new Error("too many redirects"));
68
- return resolve(httpsGet(res.headers.location, redirects + 1));
108
+ // Extra headers are not forwarded: the redirect target is another host.
109
+ return resolve(httpsGet(res.headers.location, {}, redirects + 1));
69
110
  }
70
111
  if (res.statusCode !== 200) {
71
112
  res.resume();
@@ -80,6 +121,34 @@ function httpsGet(url, redirects = 0) {
80
121
  });
81
122
  }
82
123
 
124
+ // Same release file through api.github.com, for networks where the
125
+ // github.com download host is blocked but the API host is reachable.
126
+ async function viaApi(name) {
127
+ const tag = `https://api.github.com/repos/${REPO}/releases/tags/v${VERSION}`;
128
+ const release = JSON.parse((await httpsGet(tag)).toString("utf8"));
129
+ const asset = (release.assets || []).find((a) => a.name === name);
130
+ if (!asset) throw new Error(`${name} is not an asset of v${VERSION}`);
131
+ return httpsGet(
132
+ `https://api.github.com/repos/${REPO}/releases/assets/${asset.id}`,
133
+ { Accept: "application/octet-stream" }
134
+ );
135
+ }
136
+
137
+ // A 404 means the release lacks the file, so the API would say the same. Any
138
+ // other failure is a network problem worth one retry through the API host.
139
+ async function fetchAsset(name) {
140
+ try {
141
+ return await httpsGet(`https://github.com/${REPO}/releases/download/v${VERSION}/${name}`);
142
+ } catch (e) {
143
+ if (/^HTTP 404 /.test(e.message)) throw e;
144
+ try {
145
+ return await viaApi(name);
146
+ } catch (e2) {
147
+ throw new Error(`${e.message}; api.github.com fallback: ${e2.message}`);
148
+ }
149
+ }
150
+ }
151
+
83
152
  // Parse one entry out of a coreutils-style SHA256SUMS. Written on Linux, read
84
153
  // here on every platform, so tolerate CRLF and the "*" binary-mode marker.
85
154
  function digestFor(sumsText, asset) {
@@ -98,14 +167,12 @@ async function fromDownload() {
98
167
  if (fs.existsSync(bin)) return bin;
99
168
 
100
169
  fs.mkdirSync(dir, { recursive: true });
101
- const base = `https://github.com/${REPO}/releases/download/v${VERSION}`;
102
- const url = `${base}/${plat.asset}`;
103
170
 
104
171
  // Fail closed: fetch the expected digest first, so a release without one is
105
172
  // refused before anything is downloaded rather than after.
106
173
  let sums;
107
174
  try {
108
- sums = (await httpsGet(`${base}/SHA256SUMS`)).toString("utf8");
175
+ sums = (await fetchAsset("SHA256SUMS")).toString("utf8");
109
176
  } catch (e) {
110
177
  // Only a 404 means the release genuinely lacks checksums. Reporting a
111
178
  // proxy or DNS failure as "our release is broken" sends users to file
@@ -123,7 +190,7 @@ async function fromDownload() {
123
190
 
124
191
  // stderr only: stdout is the MCP (JSON-RPC) channel.
125
192
  console.error(`crw-mcp: downloading ${plat.asset} (v${VERSION})...`);
126
- const data = await httpsGet(url);
193
+ const data = await fetchAsset(plat.asset);
127
194
 
128
195
  const actual = crypto.createHash("sha256").update(data).digest("hex");
129
196
  if (actual !== expected) {
@@ -168,7 +235,7 @@ async function resolveBinary() {
168
235
  return fromEnv() || fromPackage() || (await fromDownload());
169
236
  }
170
237
 
171
- module.exports = { digestFor, fromDownload, cacheDir, plat, binName };
238
+ module.exports = { digestFor, fromDownload, cacheDir, plat, binName, proxyAgent };
172
239
 
173
240
  // Only run when invoked as the CLI, so tests can require this file.
174
241
  if (require.main === module) {
@@ -198,8 +265,10 @@ if (require.main === module) {
198
265
  .catch((err) => {
199
266
  console.error(
200
267
  `crw-mcp: could not locate or download the ${key} binary.\n ${err.message}\n` +
201
- ` Set CRW_MCP_BINARY=/path/to/crw-mcp to use a local build, or install\n` +
202
- ` from https://github.com/${REPO}/releases.`
268
+ ` Manual install: download ${plat.asset} from\n` +
269
+ ` https://github.com/${REPO}/releases/tag/v${VERSION}, check it against SHA256SUMS,\n` +
270
+ ` and put ${binName} in ${cacheDir()}\n` +
271
+ ` (or set CRW_MCP_BINARY=/path/to/${binName}). HTTPS_PROXY is honoured.`
203
272
  );
204
273
  process.exit(1);
205
274
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crw-mcp",
3
- "version": "0.37.0",
3
+ "version": "0.37.2",
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.37.0",
42
- "crw-mcp-darwin-arm64": "0.37.0",
43
- "crw-mcp-linux-x64": "0.37.0",
44
- "crw-mcp-linux-arm64": "0.37.0"
41
+ "crw-mcp-darwin-x64": "0.37.2",
42
+ "crw-mcp-darwin-arm64": "0.37.2",
43
+ "crw-mcp-linux-x64": "0.37.2",
44
+ "crw-mcp-linux-arm64": "0.37.2"
45
45
  }
46
46
  }