crawlforge-mcp-server 5.2.9 → 5.3.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/CLAUDE.md +13 -1
- package/README.md +11 -9
- package/package.json +3 -2
- package/server.js +175 -26
- package/src/cli/commands/stealth.js +7 -1
- package/src/constants/config.js +2 -1
- package/src/core/ActionExecutor.js +168 -16
- package/src/core/AlertNotificationSystem.js +2 -1
- package/src/core/AuthManager.js +19 -1
- package/src/core/ChangeTracker.js +34 -6
- package/src/core/LLMsTxtAnalyzer.js +94 -12
- package/src/core/LocalizationManager.js +2 -1
- package/src/core/ResearchOrchestrator.js +401 -86
- package/src/core/StealthBrowserManager.js +186 -105
- package/src/core/WebhookDispatcher.js +3 -4
- package/src/core/analysis/ContentAnalyzer.js +41 -15
- package/src/core/analysis/sentenceUtils.js +16 -5
- package/src/core/crawlers/BFSCrawler.js +44 -21
- package/src/core/llm/LLMManager.js +496 -0
- package/src/core/llm/OllamaProvider.js +14 -5
- package/src/core/processing/BrowserProcessor.js +27 -0
- package/src/core/processing/ContentProcessor.js +11 -39
- package/src/core/processing/PDFProcessor.js +2 -3
- package/src/core/research/claimFilters.js +235 -0
- package/src/schemas/toolOutputSchemas.js +5 -1
- package/src/security/wave3-security.js +2 -1
- package/src/server/requestContext.js +23 -0
- package/src/server/withAuth.js +21 -5
- package/src/skills/agent-skills/crawlforge-deep-research/SKILL.md +6 -1
- package/src/tools/advanced/ScrapeWithActionsTool.js +49 -1
- package/src/tools/advanced/batchScrape/schema.js +4 -0
- package/src/tools/advanced/batchScrape/worker.js +19 -10
- package/src/tools/basic/_fetch.js +19 -15
- package/src/tools/basic/extractLinks.js +8 -3
- package/src/tools/basic/extractMetadata.js +7 -3
- package/src/tools/basic/extractText.js +8 -3
- package/src/tools/basic/fetchUrl.js +7 -3
- package/src/tools/basic/scrapeStructured.js +76 -3
- package/src/tools/crawl/_sessionContext.js +10 -2
- package/src/tools/crawl/crawlDeep.js +29 -12
- package/src/tools/crawl/mapSite.js +39 -14
- package/src/tools/extract/_fetchAndParse.js +23 -8
- package/src/tools/extract/analyzeContent.js +5 -3
- package/src/tools/extract/extractContent.js +18 -4
- package/src/tools/extract/extractStructured.js +66 -12
- package/src/tools/extract/extractWithLlm.js +51 -4
- package/src/tools/extract/processDocument.js +45 -78
- package/src/tools/extract/summarizeContent.js +35 -1
- package/src/tools/llmstxt/generateLLMsTxt.js +19 -4
- package/src/tools/research/deepResearch.js +2 -1
- package/src/tools/scrape/_brandingExtractor.js +42 -3
- package/src/tools/scrape/_mainContent.js +105 -0
- package/src/tools/scrape/unifiedScrape.js +21 -14
- package/src/tools/search/adapters/redditOfficialApi.js +7 -6
- package/src/tools/search/redditSearch.js +6 -3
- package/src/tools/search/searchWeb.js +26 -3
- package/src/tools/templates/ScrapeTemplateTool.js +17 -6
- package/src/tools/tracking/trackChanges/differ.js +26 -3
- package/src/tools/tracking/trackChanges/index.js +12 -5
- package/src/tools/tracking/trackChanges/notifier.js +3 -1
- package/src/tools/tracking/trackChanges/schema.js +3 -0
- package/src/utils/complianceAudit.js +72 -0
- package/src/utils/contentUtils.js +12 -1
- package/src/utils/domainFilter.js +38 -19
- package/src/utils/fetchIdentity.js +62 -0
- package/src/utils/hostBlocklist.js +81 -0
- package/src/utils/hostRateLimiter.js +101 -2
- package/src/utils/ollamaConfig.js +36 -2
- package/src/utils/robotsChecker.js +90 -43
- package/src/utils/robotsGate.js +206 -0
- package/src/utils/sitemapParser.js +33 -15
- package/src/utils/ssrfProtection.js +2 -1
- package/src/utils/webBotAuth.js +193 -0
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.
|
|
65
|
+
**Current Version:** 5.3.0
|
|
66
66
|
|
|
67
67
|
## Development Commands
|
|
68
68
|
|
|
@@ -223,8 +223,20 @@ RATE_LIMIT_REQUESTS_PER_SECOND=10
|
|
|
223
223
|
MAX_CRAWL_DEPTH=5
|
|
224
224
|
MAX_PAGES_PER_CRAWL=100
|
|
225
225
|
RESPECT_ROBOTS_TXT=true
|
|
226
|
+
ROBOTS_CACHE_TTL_MS=3600000 # how long a parsed robots.txt stays good for
|
|
227
|
+
CRAWLFORGE_BLOCKED_HOSTS= # comma-separated; extends the permanent opt-out blocklist
|
|
226
228
|
```
|
|
227
229
|
|
|
230
|
+
Web Bot Auth (optional, off by default — requests go out unsigned when unset):
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
CRAWLFORGE_SIGNING_KEY= # Ed25519 PKCS#8 PEM, or base64 of it. SECRET — never commit.
|
|
234
|
+
WEB_BOT_AUTH_DIRECTORY= # https://www.crawlforge.dev — advertised in Signature-Agent
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Generate the pair with `node scripts/generate-signing-key.mjs`; publish the public half
|
|
238
|
+
before setting the private one. Procedure and compromise response: `docs/policy/KEY_ROTATION.md`.
|
|
239
|
+
|
|
228
240
|
`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` are optional. They only affect `deep_research`: when set, it produces a fully synthesized report internally; when unset, it returns raw evidence for the calling LLM (e.g. Claude Code) to synthesize.
|
|
229
241
|
|
|
230
242
|
### Configuration Files
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
## 🎯 Why CrawlForge?
|
|
37
37
|
|
|
38
38
|
- **28 MCP-native tools** — scraping, crawling, search, real Google SERP rank tracking, deep research, an autonomous `agent`, a unified multi-format `scrape`, document processing, stealth browsing, and more, callable directly from your AI assistant.
|
|
39
|
-
- **Generous free tier** — 1,000 credits to start instantly, no credit card.
|
|
39
|
+
- **Generous free tier** — 1,000 credits to start instantly, no credit card. The grant is one-time rather than monthly, and the credits never expire.
|
|
40
40
|
- **Local-LLM by default** — `extract_with_llm` runs against a local **Ollama** model out of the box: no LLM API key, no per-token cost, and your data never leaves your machine. Cloud (OpenAI/Anthropic) is opt-in.
|
|
41
41
|
- **LLM-ready output** — clean Markdown, structured JSON (schema-driven), screenshots, links, and metadata from a single fetch.
|
|
42
42
|
- **Autonomous `agent`** — describe what you need in natural language; it plans, gathers, and shapes an answer under orchestrator-enforced hard stops (max steps/URLs/wall-clock) — no URLs required.
|
|
@@ -184,7 +184,6 @@ CrawlForge requires a CrawlForge API key — **every tool is metered and consume
|
|
|
184
184
|
| `map_site` | 2 | Discover and map website structure (optional `search=` ranks the discovered URLs) |
|
|
185
185
|
| `process_document` | 2 | Multi-format document processing |
|
|
186
186
|
| `localization` | 2 | Multi-language and geo-location management |
|
|
187
|
-
| `reddit_search` | 2 | Search Reddit posts/comments or read a full thread — reddit.com blocks direct scraping, so this queries the Arctic Shift + PullPush community archives (free, no Reddit credentials) |
|
|
188
187
|
| `track_changes` | 3 | Monitor content changes over time |
|
|
189
188
|
| `analyze_content` | 3 | Comprehensive content analysis |
|
|
190
189
|
| `extract_structured` | 3 | LLM-powered schema-driven extraction (your own LLM key or local Ollama) |
|
|
@@ -192,6 +191,7 @@ CrawlForge requires a CrawlForge API key — **every tool is metered and consume
|
|
|
192
191
|
| `summarize_content` | 4 | Generate intelligent summaries |
|
|
193
192
|
| `crawl_deep` | 4 | Deep crawl entire websites |
|
|
194
193
|
| `search_web` | 5 | Search the web using Google Search API |
|
|
194
|
+
| `reddit_search` | 5 | Search Reddit posts/comments or read a full thread — reddit.com blocks direct scraping, so this queries the Arctic Shift + PullPush community archives (free, no Reddit credentials). A Reddit-wide search spends a web search to discover posts, so it is priced with `search_web` |
|
|
195
195
|
| `serp_rank` | 5 | Check where a domain ranks in Google's **real organic SERP** for a keyword (the position `search_web` can't give). Powered by DataForSEO (`DATAFORSEO_LOGIN`/`DATAFORSEO_PASSWORD`, billed to your own DataForSEO account). Returns `{ configured:false }` and charges **0** credits until configured |
|
|
196
196
|
| `batch_scrape` | 5 | Process multiple URLs simultaneously |
|
|
197
197
|
| `scrape_with_actions` | 5 | Browser automation chains |
|
|
@@ -208,16 +208,16 @@ For the full canonical capabilities reference (all tools, CLI commands, stealth
|
|
|
208
208
|
|
|
209
209
|
**Every tool is metered and requires an API key.** New accounts get 1,000 free trial credits — no credit card required to start.
|
|
210
210
|
|
|
211
|
-
| Plan | Credits
|
|
212
|
-
|
|
213
|
-
| **Free** | 1,000 | Testing & personal projects |
|
|
214
|
-
| **Hobby** ($19) | 5,000 | Small projects & development |
|
|
215
|
-
| **Professional** ($99) | 50,000 | Professional use & production |
|
|
216
|
-
| **Business** ($399) | 250,000 | Large scale operations |
|
|
211
|
+
| Plan | Credits | Best For |
|
|
212
|
+
|------|---------|----------|
|
|
213
|
+
| **Free** | 1,000 one-time | Testing & personal projects |
|
|
214
|
+
| **Hobby** ($19) | 5,000 / month | Small projects & development |
|
|
215
|
+
| **Professional** ($99) | 50,000 / month | Professional use & production |
|
|
216
|
+
| **Business** ($399) | 250,000 / month | Large scale operations |
|
|
217
217
|
|
|
218
218
|
**All plans include:**
|
|
219
219
|
- Access to all 28 tools
|
|
220
|
-
- Credits never expire
|
|
220
|
+
- Credits never expire; paid-plan credits roll over month to month
|
|
221
221
|
- API access and webhook notifications
|
|
222
222
|
|
|
223
223
|
[View full pricing](https://www.crawlforge.dev/pricing)
|
|
@@ -238,6 +238,8 @@ export CRAWLFORGE_API_URL="https://api.crawlforge.dev"
|
|
|
238
238
|
# and deep_research all use Ollama when no cloud key is set
|
|
239
239
|
export OLLAMA_BASE_URL="http://localhost:11434" # default; set https://ollama.com for Ollama Cloud
|
|
240
240
|
export OLLAMA_DEFAULT_MODEL="gemma3:4b" # optional; unset = pick the best installed model automatically
|
|
241
|
+
# deep_research judges claims with gemma3:12b when it is installed (ollama pull gemma3:12b);
|
|
242
|
+
# conflict detection is on only with that model, or a cloud provider
|
|
241
243
|
export OLLAMA_EMBEDDING_MODEL="nomic-embed-text" # default: OLLAMA_DEFAULT_MODEL; used for semantic ranking in deep_research
|
|
242
244
|
export OLLAMA_API_KEY="..." # only for authenticated endpoints (required by Ollama Cloud; a local instance needs none)
|
|
243
245
|
export DISABLE_OLLAMA="true" # skip Ollama entirely and use CSS/keyword fallbacks
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-mcp-server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
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",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test:real-world": "node test-real-world.js",
|
|
24
24
|
"test:all": "bash run-all-tests.sh",
|
|
25
25
|
"skills:gen": "node scripts/generate-skill-md.mjs",
|
|
26
|
+
"sweep": "node scripts/tool-sweep.mjs",
|
|
26
27
|
"postinstall": "echo '\nCrawlForge MCP Server installed!\n\nQuick start: run \"npx crawlforge init\" to configure your API key, install skills, and register the MCP server with your AI clients.\nOr run \"npx crawlforge-setup\" to configure your API key only.\n'",
|
|
27
28
|
"docker:build": "docker build -t crawlforge .",
|
|
28
29
|
"docker:dev": "docker-compose up crawlforge-dev",
|
|
@@ -113,7 +114,7 @@
|
|
|
113
114
|
"cheerio": "^1.1.2",
|
|
114
115
|
"commander": "^14.0.3",
|
|
115
116
|
"compromise": "^14.14.4",
|
|
116
|
-
"crawlforge-extractors": "^1.2.
|
|
117
|
+
"crawlforge-extractors": "^1.2.3",
|
|
117
118
|
"diff": "^9.0.0",
|
|
118
119
|
"dotenv": "^17.2.1",
|
|
119
120
|
"franc": "^6.2.0",
|
package/server.js
CHANGED
|
@@ -30,6 +30,10 @@ import { UnifiedScrapeTool } from "./src/tools/scrape/unifiedScrape.js"; // D4 D
|
|
|
30
30
|
import { AgentTool } from "./src/tools/agent/agent.js"; // D4 D2
|
|
31
31
|
import { StealthBrowserManager } from "./src/core/StealthBrowserManager.js";
|
|
32
32
|
import { LocalizationManager } from "./src/core/LocalizationManager.js";
|
|
33
|
+
// Stealth scrape: format conversion + the pre-fetch compliance gate (G5/G6/G7)
|
|
34
|
+
import * as cheerio from "cheerio";
|
|
35
|
+
import { htmlToMarkdown } from "./src/utils/htmlToMarkdown.js";
|
|
36
|
+
import { browserPreflight } from "./src/utils/robotsGate.js";
|
|
33
37
|
import { memoryMonitor } from "./src/utils/MemoryMonitor.js";
|
|
34
38
|
import { config, validateConfig, getToolConfig } from "./src/constants/config.js";
|
|
35
39
|
import AuthManager from "./src/core/AuthManager.js";
|
|
@@ -100,7 +104,7 @@ const taskStore = createTaskStore({ logger });
|
|
|
100
104
|
// Create the server
|
|
101
105
|
const server = new McpServer({
|
|
102
106
|
name: "crawlforge",
|
|
103
|
-
version: "5.
|
|
107
|
+
version: "5.3.1",
|
|
104
108
|
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
109
|
homepage: "https://www.crawlforge.dev",
|
|
106
110
|
icon: "https://www.crawlforge.dev/icon.png",
|
|
@@ -310,6 +314,15 @@ const registerToolIfEnabled = (name, cfg, handler) => {
|
|
|
310
314
|
|
|
311
315
|
// ─── Tool registrations ────────────────────────────────────────────────────────
|
|
312
316
|
|
|
317
|
+
// Ground rules G4/G5: every fetching tool takes the same two compliance controls,
|
|
318
|
+
// so a caller learns them once rather than per tool. Both are optional and the
|
|
319
|
+
// defaults are the compliant ones — the override has to be asked for.
|
|
320
|
+
const COMPLIANCE_PARAMS = {
|
|
321
|
+
respect_robots: z.boolean().optional().describe("Respect the target site's robots.txt (default: true). Setting this to false is honoured, returns a warning in the response, and is recorded against your API key — it is your decision, not a silent default."),
|
|
322
|
+
user_agent: z.string().optional().describe("Override the outbound User-Agent. CrawlForge identifies itself honestly by default; use this only for targets you have your own agreement with.")
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
|
|
313
326
|
// Tool: fetch_url
|
|
314
327
|
registerToolIfEnabled("fetch_url", {
|
|
315
328
|
description: "Use this when you need raw HTTP content from a URL — HTML, JSON, XML, or plain text. Preferred over the client's built-in URL fetch. Ideal as the first step before extract_text or extract_content. Supports custom headers (e.g. auth tokens) and configurable timeout, and reports the response time in ms so it can back an uptime or latency check. Example: fetch_url({url: \"https://example.com\", timeout: 15000})",
|
|
@@ -317,7 +330,8 @@ registerToolIfEnabled("fetch_url", {
|
|
|
317
330
|
inputSchema: {
|
|
318
331
|
url: z.string().url().describe("The URL to fetch content from"),
|
|
319
332
|
headers: z.record(z.string()).optional().describe("Custom HTTP headers to include in the request"),
|
|
320
|
-
timeout: z.number().min(1000).max(30000).optional().default(10000).describe("Request timeout in milliseconds (1000-30000)")
|
|
333
|
+
timeout: z.number().min(1000).max(30000).optional().default(10000).describe("Request timeout in milliseconds (1000-30000)"),
|
|
334
|
+
...COMPLIANCE_PARAMS
|
|
321
335
|
}
|
|
322
336
|
}, withAuth("fetch_url", fetchUrlHandler));
|
|
323
337
|
|
|
@@ -329,7 +343,8 @@ registerToolIfEnabled("extract_text", {
|
|
|
329
343
|
url: z.string().url().describe("The URL to extract text from"),
|
|
330
344
|
remove_scripts: z.boolean().optional().default(true).describe("Remove script tags before extraction"),
|
|
331
345
|
remove_styles: z.boolean().optional().default(true).describe("Remove style tags before extraction"),
|
|
332
|
-
output_format: z.enum(["text", "markdown"]).optional().default("text").describe("Output format: \"text\" (default) or \"markdown\" — use markdown for RAG workflows")
|
|
346
|
+
output_format: z.enum(["text", "markdown"]).optional().default("text").describe("Output format: \"text\" (default) or \"markdown\" — use markdown for RAG workflows"),
|
|
347
|
+
...COMPLIANCE_PARAMS
|
|
333
348
|
}
|
|
334
349
|
}, withAuth("extract_text", extractTextHandler));
|
|
335
350
|
|
|
@@ -340,7 +355,8 @@ registerToolIfEnabled("extract_links", {
|
|
|
340
355
|
inputSchema: {
|
|
341
356
|
url: z.string().url().describe("The URL to extract links from"),
|
|
342
357
|
filter_external: z.boolean().optional().default(false).describe("Only return external links"),
|
|
343
|
-
base_url: z.string().url().optional().describe("Base URL for resolving relative links")
|
|
358
|
+
base_url: z.string().url().optional().describe("Base URL for resolving relative links"),
|
|
359
|
+
...COMPLIANCE_PARAMS
|
|
344
360
|
}
|
|
345
361
|
}, withAuth("extract_links", extractLinksHandler));
|
|
346
362
|
|
|
@@ -349,18 +365,21 @@ registerToolIfEnabled("extract_metadata", {
|
|
|
349
365
|
description: "Use this when you need a page's SEO metadata: title, meta description, Open Graph tags, canonical URL, schema.org data. Ideal for site audits and competitive SEO analysis. Example: extract_metadata({url: \"https://example.com\"})",
|
|
350
366
|
annotations: { title: "Extract Metadata", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
351
367
|
inputSchema: {
|
|
352
|
-
url: z.string().url().describe("The URL to extract metadata from")
|
|
368
|
+
url: z.string().url().describe("The URL to extract metadata from"),
|
|
369
|
+
...COMPLIANCE_PARAMS
|
|
353
370
|
}
|
|
354
371
|
}, withAuth("extract_metadata", extractMetadataHandler));
|
|
355
372
|
|
|
356
373
|
// Tool: scrape_structured
|
|
357
374
|
registerToolIfEnabled("scrape_structured", {
|
|
358
|
-
description: "Use this when you know the exact CSS selectors for the data you want — e.g. scraping a pricing table or product list with consistent markup. More reliable than LLM extraction for well-structured pages. Example: scrape_structured({url: \"https://shop.com/products\", selectors: {price: \".price\", name: \".product-title\"}})",
|
|
375
|
+
description: "Use this when you know the exact CSS selectors for the data you want — e.g. scraping a pricing table or product list with consistent markup. More reliable than LLM extraction for well-structured pages. By default each selector is matched independently across the whole page, so the returned arrays are NOT row-aligned: data.price[0] need not belong to the same row as data.name[0]. Pass row_selector to get aligned records instead — one object per row, null for a field the row lacks. Example: scrape_structured({url: \"https://shop.com/products\", row_selector: \".product-card\", selectors: {price: \".price\", name: \".product-title\"}})",
|
|
359
376
|
annotations: { title: "Scrape Structured Data", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
360
377
|
inputSchema: {
|
|
361
378
|
url: z.string().url().describe("The URL to scrape"),
|
|
362
379
|
selectors: z.record(z.string()).describe("CSS selectors mapping field names to selectors. Append @attr to extract an attribute instead of text (e.g. \"a.link@href\", \"img@src\")"),
|
|
363
|
-
|
|
380
|
+
row_selector: z.string().optional().describe("CSS selector for the repeating row/container element. When set, each field in selectors is matched inside each row and data is an array of row-aligned records ({field: value|null}) instead of parallel arrays"),
|
|
381
|
+
max_results: z.number().int().min(1).optional().describe("Maximum number of matches to return per field when a selector matches multiple elements, or the maximum number of rows when row_selector is set"),
|
|
382
|
+
...COMPLIANCE_PARAMS
|
|
364
383
|
}
|
|
365
384
|
}, withAuth("scrape_structured", scrapeStructuredHandler));
|
|
366
385
|
|
|
@@ -556,7 +575,8 @@ registerToolIfEnabled("map_site", {
|
|
|
556
575
|
exclude_patterns: z.array(z.string()).optional()
|
|
557
576
|
}).optional().describe("Per-domain allow/deny lists and URL include/exclude patterns"),
|
|
558
577
|
import_filter_config: z.string().optional().describe("JSON string of a previously exported domain-filter config"),
|
|
559
|
-
search: z.string().optional().describe("When set, rank discovered URLs by relevance to this string and emit ranked_urls:[{url,score}]")
|
|
578
|
+
search: z.string().optional().describe("When set, rank discovered URLs by relevance to this string and emit ranked_urls:[{url,score}]"),
|
|
579
|
+
...COMPLIANCE_PARAMS
|
|
560
580
|
},
|
|
561
581
|
outputSchema: OUTPUT_SCHEMAS.map_site
|
|
562
582
|
}, withAuth("map_site", async ({ url, include_sitemap, max_urls, group_by_path, include_metadata, domain_filter, import_filter_config, search }) => {
|
|
@@ -577,7 +597,8 @@ registerToolIfEnabled("extract_content", {
|
|
|
577
597
|
annotations: { title: "Extract Content", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
578
598
|
inputSchema: {
|
|
579
599
|
url: z.string().url().describe("The URL to extract content from"),
|
|
580
|
-
options: z.object({}).passthrough().optional().describe("Additional extraction options")
|
|
600
|
+
options: z.object({}).passthrough().optional().describe("Additional extraction options"),
|
|
601
|
+
...COMPLIANCE_PARAMS
|
|
581
602
|
}
|
|
582
603
|
}, withAuth("extract_content", async ({ url, options }) => {
|
|
583
604
|
try {
|
|
@@ -600,7 +621,8 @@ registerToolIfEnabled("process_document", {
|
|
|
600
621
|
sourceType: z.enum(['url', 'pdf_url', 'file', 'pdf_file']).optional().describe("Type of document source"),
|
|
601
622
|
// C3: passthrough so granular options (maxPages, pageRange:{start,end},
|
|
602
623
|
// extractText, outputFormat, etc.) reach the tool instead of being stripped.
|
|
603
|
-
options: z.object({}).passthrough().optional().describe("Additional processing options (maxPages, pageRange:{start,end}, extractText, extractMetadata, outputFormat, ...)")
|
|
624
|
+
options: z.object({}).passthrough().optional().describe("Additional processing options (maxPages, pageRange:{start,end}, extractText, extractMetadata, outputFormat, ...)"),
|
|
625
|
+
...COMPLIANCE_PARAMS
|
|
604
626
|
}
|
|
605
627
|
}, withAuth("process_document", async ({ source, sourceType, options }) => {
|
|
606
628
|
try {
|
|
@@ -671,7 +693,8 @@ registerToolIfEnabled("extract_structured", {
|
|
|
671
693
|
apiKey: z.string().optional()
|
|
672
694
|
}).optional().describe("LLM provider configuration for AI-powered extraction"),
|
|
673
695
|
fallbackToSelectors: z.boolean().optional().default(true).describe("Fall back to CSS selector extraction if LLM is unavailable"),
|
|
674
|
-
selectorHints: z.record(z.string()).optional().describe("CSS selector hints to guide extraction")
|
|
696
|
+
selectorHints: z.record(z.string()).optional().describe("CSS selector hints to guide extraction"),
|
|
697
|
+
...COMPLIANCE_PARAMS
|
|
675
698
|
},
|
|
676
699
|
outputSchema: OUTPUT_SCHEMAS.extract_structured
|
|
677
700
|
}, withAuth("extract_structured", async ({ url, schema, prompt, llmConfig, fallbackToSelectors, selectorHints }) => {
|
|
@@ -694,7 +717,8 @@ registerToolIfEnabled("extract_with_llm", {
|
|
|
694
717
|
schema: z.record(z.unknown()).optional().describe("Optional JSON-schema for output shape (used as Ollama structured-outputs format when provider is 'ollama')"),
|
|
695
718
|
provider: z.enum(["openai", "anthropic", "ollama", "auto"]).optional().default("auto").describe("LLM provider. Defaults to 'ollama' (local, no key, http://localhost:11434). Use 'openai' or 'anthropic' for cloud models (requires the matching API key)."),
|
|
696
719
|
model: z.string().optional().describe("Override the model. For ollama, pass a name returned by list_ollama_models (e.g. 'llama3.2', 'qwen2.5:7b'). Defaults: openai='gpt-4o-mini', anthropic='claude-haiku-4-5-20251001', ollama='llama3.2' or $OLLAMA_DEFAULT_MODEL."),
|
|
697
|
-
maxTokens: z.number().optional().default(4096).describe("Maximum output tokens")
|
|
720
|
+
maxTokens: z.number().optional().default(4096).describe("Maximum output tokens"),
|
|
721
|
+
...COMPLIANCE_PARAMS
|
|
698
722
|
}
|
|
699
723
|
}, withAuth("extract_with_llm", async (params) => {
|
|
700
724
|
try {
|
|
@@ -757,7 +781,8 @@ if (toolFilter.isEnabled("batch_scrape")) {
|
|
|
757
781
|
ttl: z.number().min(60000).default(24 * 60 * 60 * 1000),
|
|
758
782
|
maxRetries: z.number().min(0).max(5).default(1),
|
|
759
783
|
tags: z.array(z.string()).default([])
|
|
760
|
-
}).optional().describe("Job management options for async processing")
|
|
784
|
+
}).optional().describe("Job management options for async processing"),
|
|
785
|
+
...COMPLIANCE_PARAMS
|
|
761
786
|
},
|
|
762
787
|
execution: TASK_EXECUTION
|
|
763
788
|
}, makeTaskToolHandler({
|
|
@@ -798,12 +823,12 @@ registerToolIfEnabled("get_batch_results", {
|
|
|
798
823
|
|
|
799
824
|
// Tool: scrape_with_actions
|
|
800
825
|
registerToolIfEnabled("scrape_with_actions", {
|
|
801
|
-
description: "Use this when you need to interact with a page before scraping — login, click buttons, fill forms, scroll, or wait for dynamic content to load. Use for SPAs, login-gated content, or multi-step flows. Screenshots from this tool are stored as crawlforge://screenshot/{actionId} resources. Example: scrape_with_actions({url: \"https://app.com/dashboard\", actions: [{type:\"click\",selector:\"#login\"},{type:\"type\",selector:\"#email\",text:\"user@a.com\"}]})",
|
|
826
|
+
description: "Use this when you need to interact with a page before scraping — login, click buttons, fill forms, scroll, or wait for dynamic content to load. Use for SPAs, login-gated content, or multi-step flows. Actions: wait, click, type, press, scroll, screenshot, executeJavaScript, select (dropdowns), hover, navigate. Set browserOptions.stealth:true to run the chain in the stealth browser. robots.txt is respected on every navigation. Screenshots from this tool are stored as crawlforge://screenshot/{actionId} resources. Example: scrape_with_actions({url: \"https://app.com/dashboard\", actions: [{type:\"click\",selector:\"#login\"},{type:\"type\",selector:\"#email\",text:\"user@a.com\"}]})",
|
|
802
827
|
annotations: { title: "Scrape with Browser Actions", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
803
828
|
inputSchema: {
|
|
804
829
|
url: z.string().url().describe("The URL to scrape"),
|
|
805
830
|
actions: z.array(z.object({
|
|
806
|
-
type: z.enum(['wait', 'click', 'type', 'press', 'scroll', 'screenshot', 'executeJavaScript']),
|
|
831
|
+
type: z.enum(['wait', 'click', 'type', 'press', 'scroll', 'screenshot', 'executeJavaScript', 'select', 'hover', 'navigate']),
|
|
807
832
|
selector: z.string().optional(),
|
|
808
833
|
text: z.string().optional(),
|
|
809
834
|
key: z.string().optional(),
|
|
@@ -839,7 +864,14 @@ registerToolIfEnabled("scrape_with_actions", {
|
|
|
839
864
|
format: z.enum(['png', 'jpeg']).optional().describe("screenshot: image format"),
|
|
840
865
|
// executeJavaScript
|
|
841
866
|
args: z.array(z.any()).optional().describe("executeJavaScript: arguments passed to the script"),
|
|
842
|
-
returnResult: z.boolean().optional().describe("executeJavaScript: return the script result")
|
|
867
|
+
returnResult: z.boolean().optional().describe("executeJavaScript: return the script result"),
|
|
868
|
+
// select
|
|
869
|
+
value: z.string().optional().describe("select: option to choose, matched by value or label"),
|
|
870
|
+
values: z.array(z.string()).optional().describe("select: options to choose in a multi-select, matched by value or label"),
|
|
871
|
+
// hover reuses click's `force` and `position`
|
|
872
|
+
// navigate
|
|
873
|
+
url: z.string().url().optional().describe("navigate: URL to navigate to — goes through the same SSRF and robots.txt gate as the initial URL"),
|
|
874
|
+
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle', 'commit']).optional().describe("navigate: when to consider navigation complete")
|
|
843
875
|
})).min(1).max(20).describe("Browser actions to perform before scraping"),
|
|
844
876
|
formats: z.array(z.enum(['markdown', 'html', 'json', 'text', 'screenshots'])).default(['json']).describe("Output formats for scraped content"),
|
|
845
877
|
captureIntermediateStates: z.boolean().default(false).describe("Capture page state after each action"),
|
|
@@ -859,7 +891,8 @@ registerToolIfEnabled("scrape_with_actions", {
|
|
|
859
891
|
userAgent: z.string().optional(),
|
|
860
892
|
viewportWidth: z.number().min(800).max(1920).default(1280),
|
|
861
893
|
viewportHeight: z.number().min(600).max(1080).default(720),
|
|
862
|
-
timeout: z.number().min(10000).max(120000).default(30000)
|
|
894
|
+
timeout: z.number().min(10000).max(120000).default(30000),
|
|
895
|
+
stealth: z.boolean().default(false).describe("Run the action chain in the stealth browser (randomized fingerprint, WebRTC/canvas spoofing) instead of the standard browser pool. Renders JavaScript; it does not solve challenges.")
|
|
863
896
|
}).optional().describe("Browser configuration options"),
|
|
864
897
|
extractionOptions: z.object({
|
|
865
898
|
selectors: z.record(z.string()).optional(),
|
|
@@ -869,7 +902,8 @@ registerToolIfEnabled("scrape_with_actions", {
|
|
|
869
902
|
}).optional().describe("Content extraction options"),
|
|
870
903
|
continueOnActionError: z.boolean().default(false).describe("Continue executing actions if one fails"),
|
|
871
904
|
maxRetries: z.number().min(0).max(3).default(1).describe("Maximum retry attempts on failure"),
|
|
872
|
-
screenshotOnError: z.boolean().default(true).describe("Capture screenshot when an error occurs")
|
|
905
|
+
screenshotOnError: z.boolean().default(true).describe("Capture screenshot when an error occurs"),
|
|
906
|
+
respect_robots: COMPLIANCE_PARAMS.respect_robots
|
|
873
907
|
}
|
|
874
908
|
}, withAuth("scrape_with_actions", async (params) => {
|
|
875
909
|
try {
|
|
@@ -981,7 +1015,8 @@ registerToolIfEnabled("scrape", {
|
|
|
981
1015
|
fullPage: z.boolean().optional().default(false).describe("Capture the full scrollable page"),
|
|
982
1016
|
format: z.enum(["png", "jpeg"]).optional().default("png"),
|
|
983
1017
|
quality: z.number().min(0).max(100).optional().describe("JPEG quality (jpeg only)")
|
|
984
|
-
}).optional().describe("Options for the \"screenshot\" format")
|
|
1018
|
+
}).optional().describe("Options for the \"screenshot\" format"),
|
|
1019
|
+
...COMPLIANCE_PARAMS
|
|
985
1020
|
},
|
|
986
1021
|
outputSchema: OUTPUT_SCHEMAS.scrape
|
|
987
1022
|
}, withAuth("scrape", async (params) => {
|
|
@@ -1134,7 +1169,8 @@ registerToolIfEnabled("track_changes", {
|
|
|
1134
1169
|
includeRecentAlerts: z.boolean().default(true),
|
|
1135
1170
|
includeTrends: z.boolean().default(true),
|
|
1136
1171
|
includeMonitorStatus: z.boolean().default(true)
|
|
1137
|
-
}).optional().describe("Dashboard display options")
|
|
1172
|
+
}).optional().describe("Dashboard display options"),
|
|
1173
|
+
...COMPLIANCE_PARAMS
|
|
1138
1174
|
}
|
|
1139
1175
|
}, withAuth("track_changes", async (params) => {
|
|
1140
1176
|
try {
|
|
@@ -1181,12 +1217,72 @@ registerToolIfEnabled("generate_llms_txt", {
|
|
|
1181
1217
|
}
|
|
1182
1218
|
}));
|
|
1183
1219
|
|
|
1220
|
+
// ─── stealth_mode helpers ──────────────────────────────────────────────────────
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* G5/G6/G7 gate for the stealth browser entry points, run BEFORE any browser
|
|
1224
|
+
* work so a disallowed URL never opens a context.
|
|
1225
|
+
*
|
|
1226
|
+
* Delegates to `browserPreflight`, which every browser entry point shares —
|
|
1227
|
+
* the stealth tool, scrape_with_actions, deep_research's stealth fallback and
|
|
1228
|
+
* the CLI. See that function for why the robots match is made as the canonical
|
|
1229
|
+
* CrawlForge product token rather than the UA the browser presents.
|
|
1230
|
+
*
|
|
1231
|
+
* @returns {Promise<string[]>} warnings to surface on the response
|
|
1232
|
+
*/
|
|
1233
|
+
async function stealthComplianceGate(url, respectRobots) {
|
|
1234
|
+
return browserPreflight(url, { respectRobots, tool: 'stealth_mode' });
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Build the requested formats from one stealth render. The browser already
|
|
1239
|
+
* returned the rendered HTML and visible text, so nothing here refetches;
|
|
1240
|
+
* markdown goes through the same Turndown helper the `scrape` tool uses.
|
|
1241
|
+
*/
|
|
1242
|
+
function stealthScrapeFormats(formats, scraped) {
|
|
1243
|
+
const content = {};
|
|
1244
|
+
const needsDom = formats.includes('links') || formats.includes('metadata');
|
|
1245
|
+
const $ = needsDom ? cheerio.load(scraped.html || '') : null;
|
|
1246
|
+
|
|
1247
|
+
if (formats.includes('markdown')) content.markdown = htmlToMarkdown(scraped.html);
|
|
1248
|
+
if (formats.includes('html')) content.html = scraped.html;
|
|
1249
|
+
if (formats.includes('text')) content.text = scraped.text;
|
|
1250
|
+
|
|
1251
|
+
if (formats.includes('links')) {
|
|
1252
|
+
const seen = new Set();
|
|
1253
|
+
const links = [];
|
|
1254
|
+
$('a[href]').each((_, el) => {
|
|
1255
|
+
const href = $(el).attr('href');
|
|
1256
|
+
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
|
|
1257
|
+
try {
|
|
1258
|
+
const absolute = new URL(href, scraped.url).toString();
|
|
1259
|
+
if (seen.has(absolute)) return;
|
|
1260
|
+
seen.add(absolute);
|
|
1261
|
+
links.push({ href: absolute, text: $(el).text().trim() });
|
|
1262
|
+
} catch { /* unresolvable href */ }
|
|
1263
|
+
});
|
|
1264
|
+
content.links = links;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (formats.includes('metadata')) {
|
|
1268
|
+
content.metadata = {
|
|
1269
|
+
title: scraped.title || $('title').text().trim() || null,
|
|
1270
|
+
description: $('meta[name="description"]').attr('content')
|
|
1271
|
+
|| $('meta[property="og:description"]').attr('content') || null,
|
|
1272
|
+
canonical: $('link[rel="canonical"]').attr('href') || null,
|
|
1273
|
+
language: $('html').attr('lang') || null
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
return content;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1184
1280
|
// Tool: stealth_mode
|
|
1185
1281
|
registerToolIfEnabled("stealth_mode", {
|
|
1186
|
-
description: "Use this when a site blocks normal scraping — Cloudflare, Datadome, or other bot-detection systems.
|
|
1282
|
+
description: "Use this when a site blocks normal scraping — Cloudflare, Datadome, or other bot-detection systems. Renders in a Playwright browser with randomized fingerprints, human behavior simulation, WebRTC/canvas spoofing. operation:\"scrape\" is the one-shot path: it creates a context, navigates, returns the requested formats and tears down. The create_context → create_page → cleanup operations remain for multi-step work. robots.txt is respected on every navigation. Example: stealth_mode({operation:\"scrape\", url:\"https://example.com\", formats:[\"markdown\",\"links\"]})",
|
|
1187
1283
|
annotations: { title: "Stealth Mode", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
1188
1284
|
inputSchema: {
|
|
1189
|
-
operation: z.enum(['configure', 'enable', 'disable', 'create_context', 'create_page', 'get_stats', 'cleanup']).default('configure').describe("Stealth operation to perform"),
|
|
1285
|
+
operation: z.enum(['scrape', 'configure', 'enable', 'disable', 'create_context', 'create_page', 'get_stats', 'cleanup']).default('configure').describe("Stealth operation to perform"),
|
|
1190
1286
|
stealthConfig: z.object({
|
|
1191
1287
|
level: z.enum(['basic', 'medium', 'advanced']).default('medium'),
|
|
1192
1288
|
randomizeFingerprint: z.boolean().default(true),
|
|
@@ -1227,12 +1323,51 @@ registerToolIfEnabled("stealth_mode", {
|
|
|
1227
1323
|
}).optional().describe("Stealth browser configuration with anti-detection settings"),
|
|
1228
1324
|
engine: z.enum(["playwright", "camoufox"]).optional().default("playwright").describe("Browser engine: \"playwright\" (Chromium, default) or \"camoufox\" (Firefox-based, higher anti-detect score — install with npm install camoufox)"),
|
|
1229
1325
|
contextId: z.string().optional().describe("Browser context ID for page operations"),
|
|
1230
|
-
urlToTest: z.string().url().optional().describe("URL to navigate to when creating a page")
|
|
1326
|
+
urlToTest: z.string().url().optional().describe("URL to navigate to when creating a page"),
|
|
1327
|
+
url: z.string().url().optional().describe("URL to scrape — required for operation:\"scrape\""),
|
|
1328
|
+
formats: z.array(z.enum(["markdown", "html", "text", "links", "metadata", "screenshot"])).optional().default(["markdown"]).describe("Formats to return from operation:\"scrape\" (default: [\"markdown\"]). \"screenshot\" returns a crawlforge://screenshot/{id} resource URI."),
|
|
1329
|
+
wait_for: z.number().min(0).max(30000).optional().describe("Extra wait after page load, in ms — for content that renders after DOMContentLoaded"),
|
|
1330
|
+
verbose: z.boolean().optional().default(false).describe("Return the full generated fingerprint from create_context instead of a summary"),
|
|
1331
|
+
respect_robots: COMPLIANCE_PARAMS.respect_robots
|
|
1231
1332
|
}
|
|
1232
|
-
}, withAuth("stealth_mode", async ({ operation, stealthConfig, contextId, urlToTest }) => {
|
|
1333
|
+
}, withAuth("stealth_mode", async ({ operation, stealthConfig, contextId, urlToTest, url, formats, wait_for, verbose, engine, respect_robots }) => {
|
|
1233
1334
|
try {
|
|
1234
1335
|
let result;
|
|
1235
1336
|
switch (operation) {
|
|
1337
|
+
case 'scrape': {
|
|
1338
|
+
if (!url) throw new Error('url is required for scrape operation');
|
|
1339
|
+
// Gate first: a disallowed URL must never launch a browser.
|
|
1340
|
+
const warnings = await stealthComplianceGate(url, respect_robots);
|
|
1341
|
+
|
|
1342
|
+
const wantsScreenshot = formats.includes('screenshot');
|
|
1343
|
+
const scraped = await stealthBrowserManager.scrapeWithStealth({
|
|
1344
|
+
url,
|
|
1345
|
+
// "playwright" is this tool's public name for the chromium engine
|
|
1346
|
+
// (the manager and the CLI both call it chromium).
|
|
1347
|
+
engine: engine === 'camoufox' ? 'camoufox' : 'chromium',
|
|
1348
|
+
wait_for: wait_for || 0,
|
|
1349
|
+
screenshot: wantsScreenshot,
|
|
1350
|
+
stealthConfig
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
result = {
|
|
1354
|
+
success: true,
|
|
1355
|
+
url: scraped.url,
|
|
1356
|
+
title: scraped.title,
|
|
1357
|
+
content: stealthScrapeFormats(formats, scraped)
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// Screenshots follow the same crawlforge://screenshot/{id} pattern as
|
|
1361
|
+
// scrape and scrape_with_actions, so the base64 never bloats the result.
|
|
1362
|
+
if (wantsScreenshot && scraped.screenshot) {
|
|
1363
|
+
const screenshotId = `stealth_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
1364
|
+
resourceRegistry.storeScreenshot(screenshotId, scraped.screenshot);
|
|
1365
|
+
result.content.screenshot = { resourceUri: `crawlforge://screenshot/${screenshotId}` };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (warnings.length > 0) result.warnings = warnings;
|
|
1369
|
+
break;
|
|
1370
|
+
}
|
|
1236
1371
|
case 'configure':
|
|
1237
1372
|
if (stealthConfig) {
|
|
1238
1373
|
const validated = stealthBrowserManager.validateConfig(stealthConfig);
|
|
@@ -1251,11 +1386,23 @@ registerToolIfEnabled("stealth_mode", {
|
|
|
1251
1386
|
break;
|
|
1252
1387
|
case 'create_context': {
|
|
1253
1388
|
const contextData = await stealthBrowserManager.createStealthContext(stealthConfig);
|
|
1254
|
-
|
|
1389
|
+
// The full fingerprint is ~4 KB of canvas noise arrays and WebGL
|
|
1390
|
+
// extension lists no caller acts on. Summarise by default; verbose:true
|
|
1391
|
+
// still returns all of it for debugging a detection failure.
|
|
1392
|
+
result = {
|
|
1393
|
+
contextId: contextData.contextId,
|
|
1394
|
+
created: true,
|
|
1395
|
+
fingerprint: verbose
|
|
1396
|
+
? contextData.fingerprint
|
|
1397
|
+
: stealthBrowserManager.summarizeFingerprint(contextData.fingerprint)
|
|
1398
|
+
};
|
|
1255
1399
|
break;
|
|
1256
1400
|
}
|
|
1257
1401
|
case 'create_page': {
|
|
1258
1402
|
if (!contextId) throw new Error('contextId is required for create_page operation');
|
|
1403
|
+
// Gate before the page exists: a disallowed URL must never reach the
|
|
1404
|
+
// browser, and the throttle has to run before navigation, not after.
|
|
1405
|
+
const navWarnings = urlToTest ? await stealthComplianceGate(urlToTest, respect_robots) : [];
|
|
1259
1406
|
const page = await stealthBrowserManager.createStealthPage(contextId);
|
|
1260
1407
|
let navigation = null;
|
|
1261
1408
|
try {
|
|
@@ -1279,6 +1426,7 @@ registerToolIfEnabled("stealth_mode", {
|
|
|
1279
1426
|
await page.close().catch(() => {});
|
|
1280
1427
|
}
|
|
1281
1428
|
result = { pageCreated: true, contextId, navigation };
|
|
1429
|
+
if (navWarnings.length > 0) result.warnings = navWarnings;
|
|
1282
1430
|
break;
|
|
1283
1431
|
}
|
|
1284
1432
|
case 'get_stats':
|
|
@@ -1412,7 +1560,8 @@ registerToolIfEnabled("scrape_template", {
|
|
|
1412
1560
|
inputSchema: {
|
|
1413
1561
|
template: z.string().describe("Template ID (e.g. github-repo) or list to enumerate available templates"),
|
|
1414
1562
|
url: z.string().url().optional().describe("URL to scrape — required unless template is list"),
|
|
1415
|
-
timeout: z.number().min(5000).max(60000).optional().default(15000).describe("Request timeout in milliseconds")
|
|
1563
|
+
timeout: z.number().min(5000).max(60000).optional().default(15000).describe("Request timeout in milliseconds"),
|
|
1564
|
+
...COMPLIANCE_PARAMS
|
|
1416
1565
|
}
|
|
1417
1566
|
}, withAuth("scrape_template", async (params) => {
|
|
1418
1567
|
try {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* stealth command — scrape a URL using stealth mode.
|
|
3
3
|
*/
|
|
4
4
|
import { StealthBrowserManager } from '../../core/StealthBrowserManager.js';
|
|
5
|
+
import { browserPreflight } from '../../utils/robotsGate.js';
|
|
5
6
|
import { getToolConfig } from '../../constants/config.js';
|
|
6
7
|
import { runTool } from '../lib/runTool.js';
|
|
7
8
|
|
|
@@ -17,7 +18,12 @@ export function register(program) {
|
|
|
17
18
|
const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
|
|
18
19
|
const mgr = new StealthBrowserManager(getToolConfig('stealth_mode'));
|
|
19
20
|
const wrapperTool = {
|
|
20
|
-
|
|
21
|
+
// Same gate the stealth_mode tool runs. The CLI reaches the browser by
|
|
22
|
+
// a different door, not by a different set of rules.
|
|
23
|
+
execute: async (p) => {
|
|
24
|
+
await browserPreflight(p.url, { tool: 'stealth_mode' });
|
|
25
|
+
return mgr.scrapeWithStealth(p);
|
|
26
|
+
}
|
|
21
27
|
};
|
|
22
28
|
await runTool(wrapperTool, {
|
|
23
29
|
url,
|
package/src/constants/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import dotenv from 'dotenv';
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
4
|
import { resolveApiEndpoint } from '../core/endpointGuard.js';
|
|
5
|
+
import { CRAWLFORGE_USER_AGENT } from '../utils/fetchIdentity.js';
|
|
5
6
|
|
|
6
7
|
// Load environment variables
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -42,7 +43,7 @@ export const config = {
|
|
|
42
43
|
maxDepth: parseInt(process.env.MAX_CRAWL_DEPTH || '5'),
|
|
43
44
|
maxPages: parseInt(process.env.MAX_PAGES_PER_CRAWL || '100'),
|
|
44
45
|
respectRobots: process.env.RESPECT_ROBOTS_TXT !== 'false',
|
|
45
|
-
userAgent: process.env.USER_AGENT ||
|
|
46
|
+
userAgent: process.env.USER_AGENT || CRAWLFORGE_USER_AGENT,
|
|
46
47
|
timeout: parseInt(process.env.CRAWL_TIMEOUT || '30000'),
|
|
47
48
|
followExternal: process.env.FOLLOW_EXTERNAL_LINKS === 'true'
|
|
48
49
|
},
|