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.
- package/CLAUDE.md +13 -1
- package/README.md +9 -9
- package/package.json +2 -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 +407 -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 +517 -13
- 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 +1 -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/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
|
@@ -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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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(
|
|
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
|
|