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.
|
|
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.
|
|
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.
|
|
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" 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
|
-
}
|