searchfetch 3.0.0 → 3.0.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 (2) hide show
  1. package/index.js +42 -6
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -244,6 +244,33 @@ function mapSearchParams(engine, query, region, safeSearch) {
244
244
 
245
245
  // === FETCH ===============================================================
246
246
 
247
+ const FETCH_MAX_ATTEMPTS = 2;
248
+ const HTTP_429_RETRY_DELAY_MS = 2000;
249
+
250
+ function sleep(ms) {
251
+ return new Promise((resolve) => setTimeout(resolve, ms));
252
+ }
253
+
254
+ function parseRetryAfterMs(value) {
255
+ if (!value) return HTTP_429_RETRY_DELAY_MS;
256
+ const seconds = Number(value);
257
+ if (Number.isFinite(seconds) && seconds >= 0) {
258
+ return Math.min(seconds * 1000, 30000);
259
+ }
260
+ const dateMs = Date.parse(value);
261
+ if (Number.isFinite(dateMs)) {
262
+ return Math.min(Math.max(dateMs - Date.now(), 0), 30000);
263
+ }
264
+ return HTTP_429_RETRY_DELAY_MS;
265
+ }
266
+
267
+ function makeHttpStatusError(status, url, retryAfterMs = null) {
268
+ const err = new Error(`Access denied: HTTP ${status} when fetching ${url}`);
269
+ err.httpStatus = status;
270
+ err.retryAfterMs = retryAfterMs;
271
+ return err;
272
+ }
273
+
247
274
  function isAccessDenied($) {
248
275
  const title = ($("title").text() || "").toLowerCase();
249
276
  const bodyText = ($("body").text() || "").replace(/\s+/g, " ").trim().toLowerCase();
@@ -325,8 +352,10 @@ async function fetchHtml(url, template, blockMedia) {
325
352
  if (response) {
326
353
  const status = response.status();
327
354
  if ([401, 403, 429].includes(status)) {
328
- throw new Error(
329
- `Access denied: HTTP ${status} when fetching ${url}`,
355
+ throw makeHttpStatusError(
356
+ status,
357
+ url,
358
+ status === 429 ? parseRetryAfterMs(response.headers()["retry-after"]) : null,
330
359
  );
331
360
  }
332
361
  }
@@ -352,18 +381,25 @@ async function fetchHtml(url, template, blockMedia) {
352
381
 
353
382
  async function fetchHtmlWithRetry(url, template, blockMedia) {
354
383
  let lastError;
355
- for (let attempt = 0; attempt < 2; attempt++) {
384
+ for (let attempt = 0; attempt < FETCH_MAX_ATTEMPTS; attempt++) {
356
385
  try {
357
386
  return await fetchHtml(url, template, blockMedia);
358
387
  } catch (err) {
359
388
  lastError = err;
360
389
  if (
361
- attempt === 0 &&
390
+ attempt < FETCH_MAX_ATTEMPTS - 1 &&
391
+ err.httpStatus === 429
392
+ ) {
393
+ await sleep(err.retryAfterMs ?? HTTP_429_RETRY_DELAY_MS);
394
+ continue;
395
+ }
396
+ if (
397
+ attempt < FETCH_MAX_ATTEMPTS - 1 &&
362
398
  (err.message.includes("net::") ||
363
399
  err.message.includes("ERR_") ||
364
400
  err.message.includes("Navigation failed"))
365
401
  ) {
366
- // Network error — retry once
402
+ await sleep(500);
367
403
  continue;
368
404
  }
369
405
  throw err;
@@ -920,7 +956,7 @@ function resolveSearchTemplate(engine, query, region, safeSearch) {
920
956
 
921
957
  // === MCP SERVER & TOOLS ==================================================
922
958
 
923
- const server = new McpServer({ name: "searchfetch", version: "3.0.0" });
959
+ const server = new McpServer({ name: "searchfetch", version: "3.0.1" });
924
960
 
925
961
  // --- websearch tool ---
926
962
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "searchfetch",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Fault-tolerant MCP Server for Stealth Web Search and Fetching",
5
5
  "type": "module",
6
6
  "bin": {