crawlforge-mcp-server 5.2.3 → 5.2.5

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/CLAUDE.md CHANGED
@@ -62,7 +62,7 @@ These guidelines are working if: fewer unnecessary changes in diffs, fewer rewri
62
62
 
63
63
  CrawlForge MCP Server - A professional MCP (Model Context Protocol) server providing 28 web scraping, crawling, and content processing tools (5 inline + 23 advanced).
64
64
 
65
- **Current Version:** 5.2.3
65
+ **Current Version:** 5.2.5
66
66
 
67
67
  ## Development Commands
68
68
 
@@ -184,7 +184,7 @@ search_web, serp_rank, reddit_search, crawl_deep, map_site, extract_content, pro
184
184
  - `serp_rank` — reports where a target domain ranks in Google's REAL organic results for a keyword (the SERP position Google Custom Search / `search_web` cannot give). Backed by the DataForSEO Google Organic SERP API (Live Advanced, `POST /v3/serp/google/organic/live/advanced`, HTTP Basic auth). Credentials via `DATAFORSEO_LOGIN` / `DATAFORSEO_PASSWORD`, billed to the user's own DataForSEO account (~US$0.002 per 10 results of `depth`, so $0.004 at the default `depth:20` and $0.02 at `depth:100`), separate from CrawlForge credits. When unconfigured it returns `{ configured:false }` and charges **0** credits; when configured, **Cost: 5**. Never fabricates a rank.
185
185
 
186
186
  **reddit_search (community archives, v5.1.0):**
187
- - `reddit_search` — searches Reddit posts/comments or reads a full comment thread. reddit.com 403-blocks ALL direct access (fetch, browser-UA, stealth — IP/TLS-reputation based), so this never touches reddit.com; it queries the Arctic Shift archive (`arctic-shift.photon-reddit.com`, near-real-time, comment trees) and PullPush (`api.pullpush.io`, Pushshift-compatible). Routing: Arctic Shift's keyword search REQUIRES a subreddit/author scope (verified live: HTTP 400 without one), so unscoped full-text search goes to PullPush only; scoped searches use Arctic Shift with PullPush as error-only fallback. Modes: `posts` (default), `comments`, `thread` (post + nested comment tree by `link_id`). Free, no credentials; PullPush rate-limits aggressively (~15 req/min). Cost: 2. `REDDIT_SEARCH_TIMEOUT_MS` overrides the 30s default.
187
+ - `reddit_search` — searches Reddit posts/comments or reads a full comment thread. reddit.com 403-blocks ALL direct access (fetch, browser-UA, stealth — IP/TLS-reputation based), so this never touches reddit.com; it queries the Arctic Shift archive (`arctic-shift.photon-reddit.com`, near-real-time, comment trees) and PullPush (`api.pullpush.io`, Pushshift-compatible). Routing: Arctic Shift's keyword search REQUIRES a subreddit/author scope (verified live: HTTP 400 without one), so a scoped search queries it directly, while an unscoped keyword search for posts discovers them with a site-restricted web search and then reads those posts from the archive by ID (`source:"web_discovery"`). PullPush stopped serving automated clients in August 2026 (429 "does not provide free scraping resources for agents" on every UA; Cloudflare 403 from some IPs) and is no longer tried automatically anywhere — `source:"pullpush"` still reaches it. An unscoped COMMENT search has no backend and returns a scope-required error. Modes: `posts` (default), `comments`, `thread` (post + nested comment tree by `link_id`). Free, no Reddit credentials. Cost: 5 — a Reddit-wide search spends the same web search `search_web` does. `REDDIT_SEARCH_TIMEOUT_MS` overrides the 30s default.
188
188
 
189
189
  **v4.6.0 additions (Phase D):**
190
190
  - `scrape` — single fetch + one cheerio load dispatching a `formats` array (markdown/html/rawHtml/text/links/metadata/screenshot/json-schema) + `onlyMainContent`; partial-success via per-format `warnings[]`. Cost: 2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.2.3",
3
+ "version": "5.2.5",
4
4
  "mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
5
5
  "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 28 web scraping, crawling, deep-research, and autonomous-extraction tools. Returns clean Markdown and structured JSON for Claude, Cursor, and any MCP client. Defaults to local Ollama for LLM extraction (no API key needed); OpenAI/Anthropic available as opt-in. Includes a unified multi-format scrape tool, an autonomous agent, pre-built site templates, and Camoufox stealth browsing.",
6
6
  "main": "server.js",
@@ -113,7 +113,7 @@
113
113
  "cheerio": "^1.1.2",
114
114
  "commander": "^14.0.3",
115
115
  "compromise": "^14.14.4",
116
- "crawlforge-extractors": "^1.2.0",
116
+ "crawlforge-extractors": "^1.2.1",
117
117
  "diff": "^9.0.0",
118
118
  "dotenv": "^17.2.1",
119
119
  "franc": "^6.2.0",
package/server.js CHANGED
@@ -100,7 +100,7 @@ const taskStore = createTaskStore({ logger });
100
100
  // Create the server
101
101
  const server = new McpServer({
102
102
  name: "crawlforge",
103
- version: "5.2.3",
103
+ version: "5.2.5",
104
104
  description: "Production-ready MCP server with 28 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, real Google SERP rank tracking, Reddit search via community archives, change tracking, local-LLM extraction via Ollama, unified multi-format scrape, and autonomous agent tool.",
105
105
  homepage: "https://www.crawlforge.dev",
106
106
  icon: "https://www.crawlforge.dev/icon.png",
@@ -560,7 +560,7 @@ class AuthManager {
560
560
  process_document: 2,
561
561
  localization: 2,
562
562
  scrape: 2,
563
- reddit_search: 2, // free community archives (Arctic Shift / PullPush), no external billing
563
+ reddit_search: 5, // a Reddit-wide search spends a web search to discover posts, same as search_web
564
564
 
565
565
  // 3 credits
566
566
  track_changes: 3,
@@ -312,14 +312,22 @@ export class ChangeTracker extends EventEmitter {
312
312
 
313
313
  this.emit('changeDetected', changeRecord);
314
314
 
315
+ const ignoredOptions = this.findIgnoredCompareOptions(options, baseline.options);
316
+
315
317
  return {
316
318
  hasChanges: significance !== 'none',
317
319
  significance,
318
320
  changeType: changeRecord.changeType,
319
- summary: this.generateChangeSummary(changeAnalysis),
321
+ summary: this.generateChangeSummary(changeAnalysis, significance),
320
322
  details: changeAnalysis,
321
323
  metrics: changeRecord.metrics,
322
- recommendations: this.generateChangeRecommendations(changeRecord)
324
+ recommendations: this.generateChangeRecommendations(changeRecord),
325
+ ...(ignoredOptions.length ? {
326
+ warnings: [
327
+ `${ignoredOptions.join(', ')} passed to this compare ${ignoredOptions.length === 1 ? 'was' : 'were'} ignored — ` +
328
+ `the baseline's options are applied to both sides of the diff. Recreate the baseline to change them.`
329
+ ]
330
+ } : {})
323
331
  };
324
332
 
325
333
  } catch (error) {
@@ -328,6 +336,27 @@ export class ChangeTracker extends EventEmitter {
328
336
  }
329
337
  }
330
338
 
339
+ /**
340
+ * Report analysis options supplied at compare time that differ from the
341
+ * baseline's and were therefore not applied.
342
+ *
343
+ * Ignoring them is deliberate — both sides of a diff have to be analyzed
344
+ * identically, and once customSelectors scoped the baseline it no longer
345
+ * holds the full document to re-scope. But doing it silently let a caller
346
+ * scope a compare and read the resulting whole-page churn as real change:
347
+ * the result is byte-identical to an unscoped run, with nothing saying so.
348
+ *
349
+ * @param {Object} callerOptions - trackingOptions passed to this compare
350
+ * @param {Object} baselineOptions - options stored with the baseline
351
+ * @returns {string[]} - names of the ignored options
352
+ */
353
+ findIgnoredCompareOptions(callerOptions = {}, baselineOptions = {}) {
354
+ return ['granularity', 'customSelectors', 'excludeSelectors'].filter(key => {
355
+ if (callerOptions[key] === undefined) return false;
356
+ return JSON.stringify(callerOptions[key]) !== JSON.stringify(baselineOptions[key]);
357
+ });
358
+ }
359
+
331
360
  /**
332
361
  * Analyze content structure and create hierarchical hashes
333
362
  * @param {string} content - Content to analyze
@@ -701,6 +730,29 @@ export class ChangeTracker extends EventEmitter {
701
730
  }
702
731
  });
703
732
  });
733
+
734
+ // Scoping to a tag outside that list (address, td, li, tr, dd) otherwise
735
+ // indexes ZERO elements: the scoped document is hashed, but nothing in it
736
+ // matches the allowlist, so every compare sees an empty element map and
737
+ // can never report an element-level change. Hash the scoped elements
738
+ // themselves for any tag the loop above does not already cover.
739
+ if (options.customSelectors?.length) {
740
+ const alreadyHashed = new Set(importantElements);
741
+ options.customSelectors.forEach((selector, selectorIndex) => {
742
+ $(selector).each((index, element) => {
743
+ const tag = element.tagName?.toLowerCase();
744
+ if (!tag || alreadyHashed.has(tag)) return;
745
+
746
+ const elementKey = `custom_${selectorIndex}_${index}`;
747
+ analysis.hashes.elements[elementKey] = this.hashContent($(element).html() || '');
748
+
749
+ if (options.trackAttributes) {
750
+ const attributes = element.attribs || {};
751
+ analysis.hashes.elements[`${elementKey}_attr`] = this.hashContent(JSON.stringify(attributes));
752
+ }
753
+ });
754
+ });
755
+ }
704
756
  }
705
757
 
706
758
  async analyzeTextLevel($, analysis, options) {
@@ -1160,7 +1212,7 @@ export class ChangeTracker extends EventEmitter {
1160
1212
  return 'text_change';
1161
1213
  }
1162
1214
 
1163
- generateChangeSummary(changeAnalysis) {
1215
+ generateChangeSummary(changeAnalysis, significance) {
1164
1216
  const { addedElements, removedElements, modifiedElements, similarity } = changeAnalysis;
1165
1217
 
1166
1218
  const total = addedElements.length + removedElements.length + modifiedElements.length;
@@ -1171,7 +1223,13 @@ export class ChangeTracker extends EventEmitter {
1171
1223
  added: addedElements.length,
1172
1224
  removed: removedElements.length,
1173
1225
  modified: modifiedElements.length,
1174
- changeDescription: this.generateChangeDescription(changeAnalysis)
1226
+ // Sub-threshold text noise (a rotating session token, a base64 timestamp)
1227
+ // still lands in textChanges, so the description read "Text content
1228
+ // changed" on a compare that reported hasChanges:false and
1229
+ // totalChanges:0. Defer to the verdict the caller is given.
1230
+ changeDescription: significance === 'none'
1231
+ ? 'No significant changes detected'
1232
+ : this.generateChangeDescription(changeAnalysis)
1175
1233
  };
1176
1234
  }
1177
1235
 
@@ -51,7 +51,7 @@ Returns titles, URLs, snippets. Supports `lang`, `site` (domain filter),
51
51
  `enable_ranking`, and `enable_deduplication`. CLI:
52
52
  `crawlforge search "MCP server tutorial" --limit 5`.
53
53
 
54
- ## reddit_search (cost: 2)
54
+ ## reddit_search (cost: 5)
55
55
 
56
56
  reddit.com 403-blocks direct scraping, so this queries the Arctic Shift and
57
57
  PullPush community archives instead (free, no Reddit credentials).
@@ -25,7 +25,6 @@ metered; there is no free tier. Tools marked "scales" cost more as work grows.
25
25
  | `map_site` | URL discovery / sitemap. |
26
26
  | `process_document` | PDF / DOCX / TXT parsing. |
27
27
  | `localization` | Locale / geo emulation. |
28
- | `reddit_search` | Reddit posts/comments/threads via community archives. |
29
28
 
30
29
  ## 3 credits
31
30
 
@@ -51,6 +50,7 @@ metered; there is no free tier. Tools marked "scales" cost more as work grows.
51
50
  | `scrape_with_actions` | Browser automation then scrape. |
52
51
  | `batch_scrape` | Many URLs; projection scales with URL count. |
53
52
  | `search_web` | Web search. |
53
+ | `reddit_search` | Reddit posts/comments/threads; a Reddit-wide search spends a web search to discover posts. |
54
54
  | `generate_llms_txt` | AI-compliance file. |
55
55
 
56
56
  ## 8 credits
@@ -281,6 +281,7 @@ export class TrackChangesTool extends EventEmitter {
281
281
  details: comparisonResult.details,
282
282
  metrics: comparisonResult.metrics,
283
283
  recommendations: comparisonResult.recommendations,
284
+ ...(comparisonResult.warnings ? { warnings: comparisonResult.warnings } : {}),
284
285
  snapshot: snapshotInfo, timestamp: Date.now()
285
286
  };
286
287
  }