crawlforge-mcp-server 6.25.0 → 6.25.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.
- package/package.json +1 -1
- package/server.js +1 -1
- package/src/skills/agent-skills/crawlforge-batch-automation/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-brand-style-guide/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-browser-sessions/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-change-tracking/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-deep-research/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-getting-started/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-stealth-browsing/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-structured-extraction/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-web-scraping/SKILL.md +1 -1
- package/src/tools/search/adapters/dataforseoSearch.js +70 -8
- package/src/tools/search/queryExpander.js +16 -1
- package/src/tools/search/serpRank.js +22 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-mcp-server",
|
|
3
|
-
"version": "6.25.
|
|
3
|
+
"version": "6.25.1",
|
|
4
4
|
"mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
|
|
5
5
|
"description": "CrawlForge MCP Server - Professional Model Context Protocol server with 31 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",
|
package/server.js
CHANGED
|
@@ -114,7 +114,7 @@ if (configErrors.length > 0 && config.server.nodeEnv === 'production') {
|
|
|
114
114
|
// Create the server
|
|
115
115
|
const server = new McpServer({
|
|
116
116
|
name: "crawlforge",
|
|
117
|
-
version: "6.25.
|
|
117
|
+
version: "6.25.1",
|
|
118
118
|
description: "Production-ready MCP server with 31 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, stateful browser sessions with element refs, 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.",
|
|
119
119
|
homepage: "https://www.crawlforge.dev",
|
|
120
120
|
icon: "https://www.crawlforge.dev/icon.png",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-batch-automation
|
|
3
3
|
description: "Automates large scraping jobs and browser interactions with CrawlForge's batch_scrape, get_batch_results, scrape_with_actions, and generate_llms_txt tools. Use when the user wants to scrape many URLs at once, batch-scrape a list of pages, collect dozens of product, news, or competitor pages, run browser actions (click, type, scroll, wait) before scraping, log in or fill a form before extracting, or generate an llms.txt for a site. Use sync mode for up to about 25 URLs and async with a webhook for large batches; retrieve paginated output with get_batch_results."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-brand-style-guide
|
|
3
3
|
description: "Builds a brand style guide or a design-token file from a live website with the branding format of CrawlForge's scrape tool. Use when the user wants a company's brand colours, fonts, logo, button styles or design tokens, asks to extract a website's style, design system or brand kit, wants a style guide made from a URL, wants to match a site's look, or wants tokens for Figma, Tokens Studio or Style Dictionary (W3C DTCG JSON). One scrape call returns colour roles, the primary and secondary buttons and type read from the rendered page, the logo and favicons, font families and grouped CSS tokens, each value labelled with where it came from."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-browser-sessions
|
|
3
3
|
description: "Keeps one browser page alive across several tool calls with CrawlForge's browser_session tool. Use when the user needs to log in and then read pages behind that login, work through a multi-step flow (search, filter, paginate, fill a wizard), or look at a page before deciding what to click. The loop is: open a session on a URL, snapshot it to list the interactive elements with stable refs (@e1, @e2), act on those refs, read the content, close. Unlike scrape_with_actions, which is one-shot and closes its browser when the call returns, a session survives between calls, so one login is paid for once and a wrong selector costs one call instead of a whole chain."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-change-tracking
|
|
3
3
|
description: "Monitors web pages for changes over time with CrawlForge's track_changes tool. Use when the user wants to track changes to a page, watch a URL, monitor competitor pricing, detect when content updates, get notified of regulation or product-availability changes, or diff a page against a saved baseline. Workflow: create a baseline with operation create_baseline, then periodically compare with operation compare to get a change percentage and a diff; supports CSS-selector scoping, webhooks, and scheduled monitoring."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-deep-research
|
|
3
3
|
description: "Runs multi-source web research and autonomous question-answering with CrawlForge's deep_research, agent, search_web, and reddit_search tools. Use when the user wants to research a topic, do a deep dive, compare competitors, gather facts with citations, answer a question from the web, search the web, or get a synthesized report from many sources, or search Reddit posts, comments, and threads. deep_research expands queries, fetches and dedupes sources, then synthesizes; agent autonomously plans searches and navigation from a plain-English prompt with no URLs needed; search_web returns ranked results; reddit_search searches Reddit via community archives (reddit.com blocks scrapers). Caps costs via max_urls and confirms before expensive runs."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-getting-started
|
|
3
3
|
description: "Orientation and tool-selection guide for the CrawlForge MCP server's 31 web tools. Use when the user is getting started with CrawlForge, asks which CrawlForge tool to use, how to set up the API key, how skills or the CLI work, what a tool costs in credits, or when one tool fails and a fallback is needed. Routes requests to the right specialized skill (web scraping, deep research, stealth, structured extraction, change tracking, batch automation, browser sessions), and explains MCP-tools-vs-CLI, the Ollama-first LLM fallback chain, and per-tool credit costs."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-stealth-browsing
|
|
3
3
|
description: "Bypasses bot detection with CrawlForge's stealth_mode tool and looks up a country's locale settings with localization. Use when a site returns 403 or 429, CAPTCHAs, 'please enable JavaScript', or empty content, or is protected by Cloudflare, DataDome, or PerimeterX, or when the user needs a page in a specific locale, language, or timezone. stealth_mode runs a stealth browser (engine auto by default: camoufox/Firefox for advanced fingerprinting, playwright/Chromium when pinned for speed) and can screenshot; localization returns a country's Accept-Language, timezone and currency for you to pass to fetch_url or stealth_mode - it applies nothing itself and uses no proxy. Explains when to escalate from a normal scrape to stealth, and why a hard block needs a residential proxy."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-structured-extraction
|
|
3
3
|
description: "Extracts structured JSON and analyzes content with CrawlForge's extract_structured, extract_with_llm, extract_embedded_state, scrape_structured, scrape_template, process_document, analyze_content, summarize_content, and list_ollama_models tools. Use when the user wants to extract specific fields, pull data into a JSON schema, extract by natural-language prompt, scrape with CSS selectors, read a page's embedded JavaScript state (__NEXT_DATA__, React Server Components, Nuxt, Apollo, Redux), get product, profile, or repo data from known sites (Amazon, LinkedIn, GitHub, YouTube, Reddit, and more), parse a PDF or DOCX, summarize a page, or analyze sentiment, entities, or keywords. Defaults to local Ollama for LLM extraction; OpenAI and Anthropic optional."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: crawlforge-web-scraping
|
|
3
3
|
description: "Scrapes web pages and returns clean Markdown, HTML, plain text, links, or metadata using CrawlForge's scrape, fetch_url, extract_content, extract_text, extract_links, extract_metadata, map_site, and crawl_deep tools. Use when the user wants to scrape a URL, fetch a page, get the markdown or text of a website, extract links or metadata, read an article, map a site's URLs, generate a sitemap, or crawl a whole website or documentation site. Prefer the unified scrape tool to get multiple formats in one call; use map_site to discover URLs and crawl_deep to walk an entire site."
|
|
4
4
|
metadata:
|
|
5
|
-
version: 6.25.
|
|
5
|
+
version: 6.25.1
|
|
6
6
|
source: crawlforge-mcp-server
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -20,6 +20,55 @@
|
|
|
20
20
|
* poll if cost matters.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
const GOOGLE_HOSTS = new Set(['google.com', 'www.google.com']);
|
|
24
|
+
const BREADCRUMB_HOST = /^https?:\/\/([^\s/›?#]+)/i;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recover the real destination of an organic result whose link Google wrapped
|
|
28
|
+
* in its own redirect.
|
|
29
|
+
*
|
|
30
|
+
* Since about 2026-10-30 Google serves organic links as
|
|
31
|
+
* `https://google.com/goto?url=<opaque token>`, and DataForSEO passes that
|
|
32
|
+
* through as `url` with `domain: "google.com"`, so no target ever matched. The
|
|
33
|
+
* token is not decodable. The destination host survives in the display
|
|
34
|
+
* `breadcrumb` ("https://www.crawlforge.dev › Docs"); the page URL does not,
|
|
35
|
+
* because breadcrumb segments are labels, not a path.
|
|
36
|
+
*
|
|
37
|
+
* Mirrors `unwrapSerpItem` in the website's `src/lib/tools/serp-unwrap.ts`;
|
|
38
|
+
* keep the two in step.
|
|
39
|
+
*
|
|
40
|
+
* @returns {{ domain: string, url: string|null, wrapped: boolean }}
|
|
41
|
+
*/
|
|
42
|
+
export function unwrapSerpItem(item) {
|
|
43
|
+
const domain = (item.domain || '').toLowerCase();
|
|
44
|
+
const url = item.url || null;
|
|
45
|
+
|
|
46
|
+
let parsed = null;
|
|
47
|
+
try {
|
|
48
|
+
parsed = url ? new URL(url) : null;
|
|
49
|
+
} catch {
|
|
50
|
+
parsed = null;
|
|
51
|
+
}
|
|
52
|
+
const isRedirect =
|
|
53
|
+
parsed !== null &&
|
|
54
|
+
GOOGLE_HOSTS.has(parsed.hostname.toLowerCase()) &&
|
|
55
|
+
(parsed.pathname === '/goto' || parsed.pathname === '/url');
|
|
56
|
+
if (!isRedirect) return { domain, url, wrapped: false };
|
|
57
|
+
|
|
58
|
+
// The older /url?q=<destination> form still carries the real URL.
|
|
59
|
+
const q = parsed.searchParams.get('q');
|
|
60
|
+
if (q && /^https?:\/\//i.test(q)) {
|
|
61
|
+
try {
|
|
62
|
+
return { domain: new URL(q).hostname.toLowerCase(), url: q, wrapped: true };
|
|
63
|
+
} catch {
|
|
64
|
+
// fall through to the breadcrumb
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const match = BREADCRUMB_HOST.exec((item.breadcrumb || '').trim());
|
|
69
|
+
return { domain: match?.[1]?.toLowerCase() ?? '', url: null, wrapped: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
23
72
|
export class DataForSEOSearchAdapter {
|
|
24
73
|
constructor(login, password, options = {}) {
|
|
25
74
|
if (!login || !password) {
|
|
@@ -135,16 +184,27 @@ export class DataForSEOSearchAdapter {
|
|
|
135
184
|
// Keep only true organic results and normalize to a small, stable shape.
|
|
136
185
|
// rank_group = position within the organic results (the "rank" most people mean)
|
|
137
186
|
// rank_absolute = position across ALL SERP elements (ads, snippets, packs, …)
|
|
187
|
+
// Google may wrap result links in its own redirect; unwrapSerpItem recovers
|
|
188
|
+
// the real host so a wrapped result can still match the target.
|
|
189
|
+
let wrappedCount = 0;
|
|
190
|
+
let unresolvedCount = 0;
|
|
138
191
|
const items = rawItems
|
|
139
192
|
.filter((it) => it && it.type === 'organic')
|
|
140
|
-
.map((it) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
193
|
+
.map((it) => {
|
|
194
|
+
const link = unwrapSerpItem(it);
|
|
195
|
+
if (link.wrapped) {
|
|
196
|
+
wrappedCount += 1;
|
|
197
|
+
if (!link.domain) unresolvedCount += 1;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
position: it.rank_group ?? null,
|
|
201
|
+
rankAbsolute: it.rank_absolute ?? null,
|
|
202
|
+
domain: link.domain,
|
|
203
|
+
url: link.url,
|
|
204
|
+
title: it.title || null,
|
|
205
|
+
snippet: it.description || null,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
148
208
|
|
|
149
209
|
return {
|
|
150
210
|
items,
|
|
@@ -155,6 +215,8 @@ export class DataForSEOSearchAdapter {
|
|
|
155
215
|
device,
|
|
156
216
|
depth,
|
|
157
217
|
organicCount: items.length,
|
|
218
|
+
wrappedCount, // results whose link Google replaced with its own redirect
|
|
219
|
+
unresolvedCount, // wrapped results whose breadcrumb showed no host either
|
|
158
220
|
// Google's "About N results" total. Mobile SERPs (and many desktop
|
|
159
221
|
// ones) show no total, and DataForSEO then reports 0; a 0 beside
|
|
160
222
|
// organic results is unknown, not zero.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
|
|
3
|
+
/** A search operator term: `site:example.com`, `-site:x.com`, `filetype:pdf`. */
|
|
4
|
+
const SEARCH_OPERATOR = /^-?[a-z]+:\S+$/i;
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* QueryExpander - Advanced query expansion and refinement capabilities
|
|
5
8
|
* Provides synonym expansion, misspelling correction, stemming, phrase detection, and boolean operators
|
|
@@ -33,9 +36,21 @@ export class QueryExpander {
|
|
|
33
36
|
throw new Error('Query must be a non-empty string');
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
// Search operators (site:example.com, -site:, filetype:pdf, inurl:...) are
|
|
40
|
+
// not words: tokenising one drops its restriction, so the search returns
|
|
41
|
+
// pages from anywhere. Expand the free text and carry the operators whole.
|
|
42
|
+
const terms = query.trim().split(/\s+/);
|
|
43
|
+
const operators = terms.filter(term => SEARCH_OPERATOR.test(term));
|
|
44
|
+
if (operators.length > 0) {
|
|
45
|
+
const text = terms.filter(term => !SEARCH_OPERATOR.test(term)).join(' ');
|
|
46
|
+
if (!text) return [query.trim()];
|
|
47
|
+
const [, ...variations] = await this.expandQuery(text, options);
|
|
48
|
+
return [query.trim(), ...variations.map(variation => `${variation} ${operators.join(' ')}`)];
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
const expansionOptions = { ...this.options, ...options };
|
|
37
52
|
const expansions = new Set();
|
|
38
|
-
|
|
53
|
+
|
|
39
54
|
try {
|
|
40
55
|
// Always include the original query
|
|
41
56
|
expansions.add(query.trim());
|
|
@@ -161,8 +161,28 @@ export class SerpRankTool {
|
|
|
161
161
|
snippet: it.snippet,
|
|
162
162
|
}));
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
// A wrapped result has no URL; its recovered domain still says what it is.
|
|
165
|
+
const relevance = assessSerpRelevance(
|
|
166
|
+
validated.keyword,
|
|
167
|
+
results.map((r) => ({ ...r, url: r.url ?? (r.domain || null) })),
|
|
168
|
+
);
|
|
165
169
|
const suspect = relevance?.suspect ?? false;
|
|
170
|
+
const warnings = [];
|
|
171
|
+
if (suspect) {
|
|
172
|
+
warnings.push(
|
|
173
|
+
`These results look unrelated to the keyword (${relevance.matching} of the top ${relevance.checked} mention it). ` +
|
|
174
|
+
'Google may have served a decoy results page. Re-run the lookup, or check Search Console, before acting on this rank.',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (meta.wrappedCount > 0) {
|
|
178
|
+
warnings.push(
|
|
179
|
+
`Google hid the destination links of ${meta.wrappedCount} of the ${items.length} results behind its own redirect. ` +
|
|
180
|
+
'Their domains were read from the displayed breadcrumb, so rank matching still works, but their page URLs are unavailable (url is null)' +
|
|
181
|
+
(meta.unresolvedCount > 0
|
|
182
|
+
? `, and ${meta.unresolvedCount} showed no domain at all, so they cannot match any target.`
|
|
183
|
+
: '.'),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
166
186
|
|
|
167
187
|
return {
|
|
168
188
|
configured: true,
|
|
@@ -185,14 +205,7 @@ export class SerpRankTool {
|
|
|
185
205
|
checkedAt: new Date().toISOString(),
|
|
186
206
|
// true when the top results barely mention the keyword: a likely decoy SERP
|
|
187
207
|
suspect,
|
|
188
|
-
...(
|
|
189
|
-
? {
|
|
190
|
-
warnings: [
|
|
191
|
-
`These results look unrelated to the keyword (${relevance.matching} of the top ${relevance.checked} mention it). ` +
|
|
192
|
-
'Google may have served a decoy results page. Re-run the lookup, or check Search Console, before acting on this rank.',
|
|
193
|
-
],
|
|
194
|
-
}
|
|
195
|
-
: {}),
|
|
208
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
196
209
|
};
|
|
197
210
|
}
|
|
198
211
|
}
|