crawlforge-mcp-server 5.2.8 → 5.3.0

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.
Files changed (71) hide show
  1. package/CLAUDE.md +13 -1
  2. package/README.md +9 -9
  3. package/package.json +2 -2
  4. package/server.js +175 -26
  5. package/src/cli/commands/stealth.js +7 -1
  6. package/src/constants/config.js +2 -1
  7. package/src/core/ActionExecutor.js +168 -16
  8. package/src/core/AlertNotificationSystem.js +2 -1
  9. package/src/core/AuthManager.js +19 -1
  10. package/src/core/ChangeTracker.js +34 -6
  11. package/src/core/LLMsTxtAnalyzer.js +94 -12
  12. package/src/core/LocalizationManager.js +2 -1
  13. package/src/core/ResearchOrchestrator.js +407 -86
  14. package/src/core/StealthBrowserManager.js +186 -105
  15. package/src/core/WebhookDispatcher.js +3 -4
  16. package/src/core/analysis/ContentAnalyzer.js +41 -15
  17. package/src/core/analysis/sentenceUtils.js +16 -5
  18. package/src/core/crawlers/BFSCrawler.js +44 -21
  19. package/src/core/llm/LLMManager.js +517 -13
  20. package/src/core/processing/BrowserProcessor.js +27 -0
  21. package/src/core/processing/ContentProcessor.js +11 -39
  22. package/src/core/processing/PDFProcessor.js +2 -3
  23. package/src/core/research/claimFilters.js +235 -0
  24. package/src/schemas/toolOutputSchemas.js +5 -1
  25. package/src/security/wave3-security.js +2 -1
  26. package/src/server/requestContext.js +23 -0
  27. package/src/server/withAuth.js +21 -5
  28. package/src/skills/agent-skills/crawlforge-deep-research/SKILL.md +1 -1
  29. package/src/tools/advanced/ScrapeWithActionsTool.js +49 -1
  30. package/src/tools/advanced/batchScrape/schema.js +4 -0
  31. package/src/tools/advanced/batchScrape/worker.js +19 -10
  32. package/src/tools/basic/_fetch.js +19 -15
  33. package/src/tools/basic/extractLinks.js +8 -3
  34. package/src/tools/basic/extractMetadata.js +7 -3
  35. package/src/tools/basic/extractText.js +8 -3
  36. package/src/tools/basic/fetchUrl.js +7 -3
  37. package/src/tools/basic/scrapeStructured.js +76 -3
  38. package/src/tools/crawl/_sessionContext.js +10 -2
  39. package/src/tools/crawl/crawlDeep.js +29 -12
  40. package/src/tools/crawl/mapSite.js +39 -14
  41. package/src/tools/extract/_fetchAndParse.js +23 -8
  42. package/src/tools/extract/analyzeContent.js +5 -3
  43. package/src/tools/extract/extractContent.js +18 -4
  44. package/src/tools/extract/extractStructured.js +66 -12
  45. package/src/tools/extract/extractWithLlm.js +51 -4
  46. package/src/tools/extract/processDocument.js +45 -78
  47. package/src/tools/extract/summarizeContent.js +35 -1
  48. package/src/tools/llmstxt/generateLLMsTxt.js +19 -4
  49. package/src/tools/research/deepResearch.js +2 -1
  50. package/src/tools/scrape/_brandingExtractor.js +42 -3
  51. package/src/tools/scrape/_mainContent.js +105 -0
  52. package/src/tools/scrape/unifiedScrape.js +21 -14
  53. package/src/tools/search/adapters/redditOfficialApi.js +7 -6
  54. package/src/tools/search/redditSearch.js +6 -3
  55. package/src/tools/search/searchWeb.js +26 -3
  56. package/src/tools/templates/ScrapeTemplateTool.js +17 -6
  57. package/src/tools/tracking/trackChanges/differ.js +26 -3
  58. package/src/tools/tracking/trackChanges/index.js +12 -5
  59. package/src/tools/tracking/trackChanges/notifier.js +3 -1
  60. package/src/tools/tracking/trackChanges/schema.js +3 -0
  61. package/src/utils/complianceAudit.js +72 -0
  62. package/src/utils/contentUtils.js +12 -1
  63. package/src/utils/domainFilter.js +38 -19
  64. package/src/utils/fetchIdentity.js +62 -0
  65. package/src/utils/hostBlocklist.js +81 -0
  66. package/src/utils/hostRateLimiter.js +101 -2
  67. package/src/utils/robotsChecker.js +90 -43
  68. package/src/utils/robotsGate.js +206 -0
  69. package/src/utils/sitemapParser.js +33 -15
  70. package/src/utils/ssrfProtection.js +2 -1
  71. package/src/utils/webBotAuth.js +193 -0
@@ -2,12 +2,14 @@ import { load } from 'cheerio';
2
2
  import { QueueManager } from '../queue/QueueManager.js';
3
3
  import { CacheManager } from '../cache/CacheManager.js';
4
4
  import { RateLimiter } from '../../utils/rateLimiter.js';
5
- import { RobotsChecker } from '../../utils/robotsChecker.js';
6
5
  import { DomainFilter } from '../../utils/domainFilter.js';
7
6
  import { LinkAnalyzer } from '../analysis/LinkAnalyzer.js';
8
7
  import { normalizeUrl, extractLinks, isValidUrl } from '../../utils/urlNormalizer.js';
9
8
  import { Logger } from '../../utils/Logger.js';
10
9
  import { safeFetch } from '../../utils/ssrfGuard.js';
10
+ import { robotsPreflight } from '../../utils/robotsGate.js';
11
+ import { throttleHost } from '../../utils/hostRateLimiter.js';
12
+ import { CRAWLFORGE_USER_AGENT, identityHeaders } from '../../utils/fetchIdentity.js';
11
13
 
12
14
  const logger = new Logger('BFSCrawler');
13
15
 
@@ -18,7 +20,7 @@ export class BFSCrawler {
18
20
  maxPages = 100,
19
21
  followExternal = false,
20
22
  respectRobots = true,
21
- userAgent = 'MCP-WebScraper/1.0',
23
+ userAgent = CRAWLFORGE_USER_AGENT,
22
24
  timeout = 30000,
23
25
  concurrency = 10,
24
26
  domainFilter = null,
@@ -59,7 +61,6 @@ export class BFSCrawler {
59
61
  // effectiveRateLimit hasn't changed, rather than recreating it on every URL.
60
62
  this.rateLimiter = new RateLimiter({ requestsPerSecond: 10 });
61
63
  this._domainRateLimiters = new Map();
62
- this.robotsChecker = respectRobots ? new RobotsChecker(userAgent) : null;
63
64
 
64
65
  // Initialize domain filter (create new if not provided)
65
66
  this.domainFilter = domainFilter || new DomainFilter({
@@ -109,14 +110,18 @@ export class BFSCrawler {
109
110
  const normalizedStart = normalizeUrl(startUrl);
110
111
  this.baseUrl = new URL(normalizedStart);
111
112
 
112
- // Check if start URL is allowed
113
- const startUrlDecision = this.domainFilter.isAllowed(normalizedStart);
113
+ // Check if start URL is allowed. isSeed exempts it from the include-pattern gate:
114
+ // include patterns scope where the crawl may go next, not whether the URL the caller
115
+ // explicitly asked for may be fetched. Blacklist and exclude patterns still apply.
116
+ // The un-normalized startUrl is passed so a pattern written with a trailing slash
117
+ // ('/docs/') is tested against the form the caller wrote.
118
+ const startUrlDecision = this.domainFilter.isAllowed(startUrl, { isSeed: true });
114
119
  if (!startUrlDecision.allowed) {
115
120
  throw new Error(`Start URL blocked by domain filter: ${startUrlDecision.reason}`);
116
121
  }
117
122
 
118
123
  // Initialize queue with starting URL
119
- await this.queue.add(() => this.processUrl(normalizedStart, 0));
124
+ await this.queue.add(() => this.processUrl(startUrl, 0));
120
125
 
121
126
  // Wait for crawling to complete
122
127
  await this.queue.onIdle();
@@ -148,8 +153,11 @@ export class BFSCrawler {
148
153
  return;
149
154
  }
150
155
 
156
+ // Only the seed is queued at depth 0; children are always depth >= 1.
157
+ const isSeed = depth === 0;
158
+
151
159
  // Check domain filter (replaces old pattern checking)
152
- const filterDecision = this.domainFilter.isAllowed(normalizedUrl);
160
+ const filterDecision = this.domainFilter.isAllowed(url, { isSeed });
153
161
  this.filterDecisions.push({
154
162
  url: normalizedUrl,
155
163
  decision: filterDecision,
@@ -162,18 +170,22 @@ export class BFSCrawler {
162
170
  }
163
171
 
164
172
  // Backward compatibility: also check legacy patterns
165
- if (!this.shouldCrawlUrl(normalizedUrl)) {
173
+ if (!this.shouldCrawlUrl(normalizedUrl, url, isSeed)) {
166
174
  logger.debug(`Legacy pattern blocks: ${normalizedUrl}`);
167
175
  return;
168
176
  }
169
177
 
170
- // Check robots.txt
171
- if (this.respectRobots && this.robotsChecker) {
172
- const canFetch = await this.robotsChecker.canFetch(normalizedUrl);
173
- if (!canFetch) {
174
- logger.debug(`Robots.txt blocks: ${normalizedUrl}`);
175
- return;
176
- }
178
+ // Check robots.txt through the shared gate, so this crawl reuses the same
179
+ // cached robots.txt every other tool fetched (and honours the platform
180
+ // blocklist, which no per-request flag can switch off).
181
+ const gate = await robotsPreflight(normalizedUrl, {
182
+ respectRobots: this.respectRobots,
183
+ userAgent: this.userAgent,
184
+ tool: 'crawl_deep'
185
+ });
186
+ if (!gate.allowed) {
187
+ logger.debug(`Robots.txt blocks: ${normalizedUrl}`);
188
+ return;
177
189
  }
178
190
 
179
191
  // Mark as visited. Re-check the cap and dedupe first: the checks at the
@@ -209,6 +221,12 @@ export class BFSCrawler {
209
221
 
210
222
  await this._domainRateLimiters.get(domain).checkLimit(normalizedUrl);
211
223
 
224
+ // The host's own Crawl-delay, on top of our per-domain limit. Only
225
+ // when it asked for one — otherwise the domain limiter above stands.
226
+ if (gate.crawlDelayMs > 0) {
227
+ await throttleHost(normalizedUrl, { crawlDelayMs: gate.crawlDelayMs });
228
+ }
229
+
212
230
  // Fetch the page
213
231
  pageData = await this.fetchPage(normalizedUrl);
214
232
 
@@ -292,7 +310,7 @@ export class BFSCrawler {
292
310
  const domainRules = this.domainFilter.getDomainRules(urlObj.hostname);
293
311
 
294
312
  const defaultHeaders = {
295
- 'User-Agent': this.userAgent,
313
+ ...identityHeaders({ userAgent: this.userAgent }),
296
314
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
297
315
  'Accept-Language': 'en-US,en;q=0.5',
298
316
  'Accept-Encoding': 'gzip, deflate',
@@ -409,16 +427,21 @@ export class BFSCrawler {
409
427
  }
410
428
  }
411
429
 
412
- shouldCrawlUrl(url) {
413
- // Check include patterns
414
- if (this.includePatterns.length > 0) {
415
- const matches = this.includePatterns.some(pattern => pattern.test(url));
430
+ shouldCrawlUrl(url, rawUrl = url, isSeed = false) {
431
+ // Check include patterns. The seed is exempt for the same reason as in crawl():
432
+ // it is the URL the caller named, not a link the crawl chose to follow.
433
+ if (!isSeed && this.includePatterns.length > 0) {
434
+ const matches = this.includePatterns.some(
435
+ pattern => pattern.test(url) || pattern.test(rawUrl)
436
+ );
416
437
  if (!matches) return false;
417
438
  }
418
439
 
419
440
  // Check exclude patterns
420
441
  if (this.excludePatterns.length > 0) {
421
- const excluded = this.excludePatterns.some(pattern => pattern.test(url));
442
+ const excluded = this.excludePatterns.some(
443
+ pattern => pattern.test(url) || pattern.test(rawUrl)
444
+ );
422
445
  if (excluded) return false;
423
446
  }
424
447