crawlforge-mcp-server 5.6.9 → 5.6.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.6.9",
3
+ "version": "5.6.10",
4
4
  "mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
5
5
  "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 29 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",
@@ -116,7 +116,7 @@
116
116
  "cheerio": "^1.1.2",
117
117
  "commander": "^14.0.3",
118
118
  "compromise": "^14.14.4",
119
- "crawlforge-extractors": "^1.6.4",
119
+ "crawlforge-extractors": "^1.6.5",
120
120
  "diff": "^9.0.0",
121
121
  "dotenv": "^17.2.1",
122
122
  "franc": "^6.2.0",
package/server.js CHANGED
@@ -107,7 +107,7 @@ const taskStore = createTaskStore({ logger });
107
107
  // Create the server
108
108
  const server = new McpServer({
109
109
  name: "crawlforge",
110
- version: "5.6.9",
110
+ version: "5.6.10",
111
111
  description: "Production-ready MCP server with 29 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, embedded JavaScript state 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.",
112
112
  homepage: "https://www.crawlforge.dev",
113
113
  icon: "https://www.crawlforge.dev/icon.png",
@@ -11,12 +11,11 @@
11
11
  * one call — the registry builds the URL, this tool fetches it.
12
12
  */
13
13
 
14
- import { TemplateRegistry, retiredTemplate } from 'crawlforge-extractors';
14
+ import { TemplateRegistry, retiredTemplate, shopifyProductFromJsonLd } from 'crawlforge-extractors';
15
15
  import { safeFetch } from '../../utils/ssrfGuard.js';
16
16
  import { preflightFetch } from '../../utils/robotsGate.js';
17
17
  import { noteRetryAfter } from '../../utils/hostRateLimiter.js';
18
18
  import { markPreflightRefusal } from '../../server/requestContext.js';
19
- import { shopifyProductFromJsonLd } from './shopifyJsonLdFallback.js';
20
19
 
21
20
  /**
22
21
  * A caller mistake caught before anything is fetched: no template matches the
@@ -1,160 +0,0 @@
1
- /**
2
- * shopifyJsonLdFallback.js — a Shopify product from the storefront's own
3
- * JSON-LD when /products/<handle>.json is refused.
4
- *
5
- * gymshark.com answers the JSON endpoint with 403 while the product page is
6
- * public and carries a schema.org ProductGroup whose hasVariant Products each
7
- * carry an Offer (R18, 2026-09-04). allbirds.com redirected a retired handle
8
- * to a collection page, so the .json URL was a 404 with no product behind it.
9
- * Both used to surface as a bare "HTTP 403/404" from the template. This reads
10
- * the page instead and says what it fell back to; per-variant inventory is not
11
- * in JSON-LD, so `available` is the offer's availability and variants carry
12
- * no stock count.
13
- */
14
-
15
- import { load } from 'cheerio';
16
-
17
- function asArray(value) {
18
- if (value == null) return [];
19
- return Array.isArray(value) ? value : [value];
20
- }
21
-
22
- function flattenLd(node, out = []) {
23
- if (!node || typeof node !== 'object') return out;
24
- if (Array.isArray(node)) { node.forEach((n) => flattenLd(n, out)); return out; }
25
- out.push(node);
26
- if (node['@graph']) flattenLd(node['@graph'], out);
27
- return out;
28
- }
29
-
30
- const typed = (re) => (node) => asArray(node?.['@type']).some((t) => re.test(String(t)));
31
- const isProduct = typed(/^product$/i);
32
- const isProductGroup = typed(/^productgroup$/i);
33
-
34
- function toNumber(value) {
35
- if (value == null || value === '') return null;
36
- const n = Number(String(value).replace(/[^\d.,-]/g, '').replace(/,(?=\d{3}\b)/g, '').replace(',', '.'));
37
- return Number.isFinite(n) ? n : null;
38
- }
39
-
40
- function money(n) {
41
- return n == null ? null : n.toFixed(2);
42
- }
43
-
44
- // Gymshark writes its name as "Arrival 5&quot; Shorts" inside the JSON.
45
- function decodeEntities(value) {
46
- if (typeof value !== 'string') return null;
47
- const text = /&(?:[a-z]+|#\d+|#x[0-9a-f]+);/i.test(value) ? load(`<x>${value}</x>`)('x').text() : value;
48
- return text.replace(/\s+/g, ' ').trim() || null;
49
- }
50
-
51
- function availabilityOf(offer) {
52
- const a = String(offer?.availability || '');
53
- if (!a) return null;
54
- if (/InStock|PreOrder|BackOrder|LimitedAvailability|OnlineOnly/i.test(a)) return true;
55
- if (/OutOfStock|SoldOut|Discontinued/i.test(a)) return false;
56
- return null;
57
- }
58
-
59
- /** Offers of a node, an AggregateOffer's own list included. */
60
- function offersOf(node) {
61
- return asArray(node?.offers)
62
- .flatMap((o) => (o && typed(/AggregateOffer/i)(o) && o.offers ? asArray(o.offers) : [o]))
63
- .filter(Boolean);
64
- }
65
-
66
- function offerPrice(offer) {
67
- return toNumber(offer.price ?? offer.lowPrice);
68
- }
69
-
70
- /**
71
- * @param {string} html - the product page
72
- * @param {string} url - the product page URL
73
- * @returns {{ found: boolean, data?: object, reason?: string }}
74
- */
75
- export function shopifyProductFromJsonLd(html, url) {
76
- const $ = load(html || '');
77
- const nodes = [];
78
- $('script[type="application/ld+json"]').each((_, el) => {
79
- const raw = $(el).html();
80
- if (!raw) return;
81
- try { flattenLd(JSON.parse(raw), nodes); } catch { /* one bad block does not lose the rest */ }
82
- });
83
- const product = nodes.find(isProductGroup) || nodes.find(isProduct);
84
- if (!product) {
85
- return {
86
- found: false,
87
- reason: `${url} carries no schema.org Product JSON-LD` +
88
- (/\/collections\//.test(url) ? ' — the product URL redirected to a collection page, so the product handle no longer exists' : '')
89
- };
90
- }
91
-
92
- // A ProductGroup (or a Product with hasVariant) prices each variant on the
93
- // variant; a plain Product prices its offers directly.
94
- const variantNodes = asArray(product.hasVariant).filter(isProduct);
95
- const variants = variantNodes.length > 0
96
- ? variantNodes.map((v) => {
97
- const offers = offersOf(v);
98
- const prices = offers.map(offerPrice).filter((n) => n != null);
99
- const availabilities = offers.map(availabilityOf).filter((a) => a !== null);
100
- const options = ['size', 'color', 'material', 'pattern'].map((k) => decodeEntities(v[k])).filter(Boolean);
101
- return {
102
- id: v.sku || v.mpn || v.productID || null,
103
- title: decodeEntities(v.name) || options.join(' / ') || v.sku || null,
104
- price: money(prices.length ? Math.min(...prices) : null),
105
- compare_at_price: null,
106
- sku: v.sku || null,
107
- available: availabilities.length ? availabilities.some(Boolean) : null,
108
- inventory_quantity: null,
109
- options
110
- };
111
- })
112
- : offersOf(product).map((o) => ({
113
- id: o.sku || o.mpn || null,
114
- title: decodeEntities(o.name) || o.sku || null,
115
- price: money(offerPrice(o)),
116
- compare_at_price: null,
117
- sku: o.sku || null,
118
- available: availabilityOf(o),
119
- inventory_quantity: null,
120
- options: []
121
- }));
122
-
123
- const allOffers = variantNodes.length > 0 ? variantNodes.flatMap(offersOf) : offersOf(product);
124
- const prices = allOffers.map(offerPrice).filter((n) => n != null);
125
- const highs = allOffers.map((o) => toNumber(o.highPrice)).filter((n) => n != null);
126
- const priceMin = prices.length ? Math.min(...prices) : null;
127
- const priceMax = prices.length ? Math.max(...prices, ...highs) : (highs.length ? Math.max(...highs) : null);
128
- const currency = allOffers.map((o) => o.priceCurrency).find(Boolean) || null;
129
- const availabilities = allOffers.map(availabilityOf).filter((v) => v !== null);
130
- const available = availabilities.length ? availabilities.some(Boolean) : null;
131
-
132
- let handle = null;
133
- try { handle = (new URL(url).pathname.match(/\/products\/([^/?#]+)/) || [])[1] || null; } catch { /* keep null */ }
134
-
135
- const brand = product.brand && typeof product.brand === 'object' ? product.brand.name : product.brand;
136
- const images = asArray(product.image).map((i) => (i && typeof i === 'object' ? i.url || i.contentUrl : i)).filter(Boolean);
137
- const options = asArray(product.variesBy).map((v) => String(v).split('/').pop().toLowerCase()).filter(Boolean);
138
-
139
- const data = {
140
- title: decodeEntities(product.name),
141
- vendor: decodeEntities(brand),
142
- product_type: decodeEntities(product.category),
143
- handle,
144
- product_id: product.productGroupID || product.productID || product.sku || null,
145
- price: money(priceMin),
146
- compare_at_price: null,
147
- on_sale: null,
148
- currency,
149
- price_min: money(priceMin),
150
- price_max: money(priceMax),
151
- available,
152
- variants,
153
- options,
154
- description: decodeEntities(product.description),
155
- images,
156
- url: product.url || url,
157
- source: 'json-ld'
158
- };
159
- return { found: true, data };
160
- }