crawlforge-mcp-server 6.7.0 → 6.9.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 +19 -2
- package/README.md +21 -3
- package/package.json +4 -2
- package/server.js +51 -27
- package/src/constants/config.js +22 -0
- package/src/core/AgentOrchestrator.js +123 -25
- package/src/core/AuthManager.js +10 -1
- package/src/core/ResearchOrchestrator.js +72 -2
- package/src/core/StealthBrowserManager.js +852 -250
- package/src/core/creatorMode.js +1 -1
- package/src/core/processing/BrowserProcessor.js +30 -4
- package/src/skills/agent-skills/crawlforge-deep-research/SKILL.md +1 -1
- package/src/skills/agent-skills/crawlforge-stealth-browsing/SKILL.md +35 -11
- package/src/skills/agent-skills/crawlforge-stealth-browsing/references/engine-selection.md +64 -19
- package/src/skills/agent-skills/crawlforge-web-scraping/SKILL.md +12 -6
- package/src/tools/advanced/BrowserSessionTool.js +38 -3
- package/src/tools/advanced/ScrapeWithActionsTool.js +30 -2
- package/src/tools/agent/agent.js +29 -9
- package/src/tools/agent/escalation.js +40 -0
- package/src/tools/scrape/escalation.js +6 -1
- package/src/tools/scrape/unifiedScrape.js +2 -1
- package/src/utils/challengeDetection.js +86 -3
- package/src/utils/stealthVerdict.js +36 -2
package/src/core/AuthManager.js
CHANGED
|
@@ -14,6 +14,7 @@ import { logger } from '../utils/Logger.js';
|
|
|
14
14
|
import { maskSecrets } from '../utils/secretMask.js';
|
|
15
15
|
import { scrapeFormatSurcharge } from '../tools/scrape/formats.js';
|
|
16
16
|
import { scrapeEscalationSurcharge } from '../tools/scrape/escalation.js';
|
|
17
|
+
import { agentEscalationSurcharge } from '../tools/agent/escalation.js';
|
|
17
18
|
import { searchQueryCount } from '../tools/search/batchSearch.js';
|
|
18
19
|
import { redactionSurcharge, REDACT_PII_MODEL_CREDITS } from '../server/redaction.js';
|
|
19
20
|
// Stamped on every usage report so support can tell which client version
|
|
@@ -686,6 +687,14 @@ class AuthManager {
|
|
|
686
687
|
return costs.search_web * searchQueryCount(params?.queries) + redaction;
|
|
687
688
|
}
|
|
688
689
|
|
|
690
|
+
// agent retries a walled page in the stealth browser automatically, at
|
|
691
|
+
// most AGENT_MAX_ESCALATIONS times a run, 5 each. The projection is that
|
|
692
|
+
// ceiling; the tool reports what actually ran (setActualCost), so a run
|
|
693
|
+
// with no retry still costs 8. The rule lives in tools/agent/escalation.js.
|
|
694
|
+
if (tool === 'agent') {
|
|
695
|
+
return costs.agent + agentEscalationSurcharge(params) + redaction;
|
|
696
|
+
}
|
|
697
|
+
|
|
689
698
|
return (costs[tool] ?? 1) + redaction;
|
|
690
699
|
}
|
|
691
700
|
|
|
@@ -761,7 +770,7 @@ class AuthManager {
|
|
|
761
770
|
const agentUrls = params?.maxUrls || 10;
|
|
762
771
|
const isPro = params?.model === 'pro';
|
|
763
772
|
projected = Math.max(base, base + Math.ceil(agentUrls / 5) + (isPro ? 5 : 0));
|
|
764
|
-
note = `Lower-bound estimate. Scales with maxUrls (${agentUrls}).${isPro ? ' pro model adds deep-research cost.' : ''} External LLM billed separately.`;
|
|
773
|
+
note = `Lower-bound estimate. Scales with maxUrls (${agentUrls}).${isPro ? ' pro model adds deep-research cost.' : ' Includes the ceiling for automatic stealth retries of walled pages (5 each, at most 2 a run); the actual charge drops by 5 for each retry that did not run or did not get the page.'} External LLM billed separately.`;
|
|
765
774
|
break;
|
|
766
775
|
}
|
|
767
776
|
default:
|
|
@@ -9,6 +9,7 @@ import { CacheManager } from './cache/CacheManager.js';
|
|
|
9
9
|
import { Logger } from '../utils/Logger.js';
|
|
10
10
|
import { LLMManager } from './llm/LLMManager.js';
|
|
11
11
|
import { safeFetch, safeGoto } from '../utils/ssrfGuard.js';
|
|
12
|
+
import { serverStealthProxies } from '../constants/config.js';
|
|
12
13
|
import { preflightFetch, browserPreflight } from '../utils/robotsGate.js';
|
|
13
14
|
import { guardFirefoxPageErrors } from '../utils/firefoxPageErrorGuard.js';
|
|
14
15
|
import { noteRetryAfter } from '../utils/hostRateLimiter.js';
|
|
@@ -143,6 +144,12 @@ export class ResearchOrchestrator extends EventEmitter {
|
|
|
143
144
|
stealthEngine = process.env.RESEARCH_STEALTH_ENGINE || 'auto',
|
|
144
145
|
stealthLevel = 'medium',
|
|
145
146
|
stealthTimeoutMs = 20000,
|
|
147
|
+
// Proxy list for the stealth fallback, in proxyRotation's URL format. The
|
|
148
|
+
// caller's list always wins; with none, the server-level
|
|
149
|
+
// CRAWLFORGE_STEALTH_PROXIES applies — this path launches Camoufox itself
|
|
150
|
+
// rather than through StealthBrowserManager, so it does not inherit that
|
|
151
|
+
// manager's proxy fallback and has to resolve one here.
|
|
152
|
+
stealthProxies = serverStealthProxies(),
|
|
146
153
|
searchConfig = {},
|
|
147
154
|
crawlConfig = {},
|
|
148
155
|
extractConfig = {},
|
|
@@ -164,6 +171,7 @@ export class ResearchOrchestrator extends EventEmitter {
|
|
|
164
171
|
this.stealthEngine = stealthEngine;
|
|
165
172
|
this.stealthLevel = stealthLevel;
|
|
166
173
|
this.stealthTimeoutMs = stealthTimeoutMs;
|
|
174
|
+
this.stealthProxies = Array.isArray(stealthProxies) ? stealthProxies : [];
|
|
167
175
|
this._stealthManager = null; // Chromium StealthBrowserManager (fallback engine)
|
|
168
176
|
this._stealthBrowser = null; // Camoufox browser handle (preferred engine)
|
|
169
177
|
this._stealthEngineActive = null;
|
|
@@ -952,6 +960,11 @@ export class ResearchOrchestrator extends EventEmitter {
|
|
|
952
960
|
if (!this._stealthInit) {
|
|
953
961
|
this._stealthInit = (async () => {
|
|
954
962
|
if (this.stealthEngine === 'camoufox' || this.stealthEngine === 'auto') {
|
|
963
|
+
// Resolved outside the try: a malformed proxy must fail loudly, not be
|
|
964
|
+
// swallowed by the 'auto' fallback into a proxyless Chromium run —
|
|
965
|
+
// "the caller believes their traffic is proxied and it is not" is the
|
|
966
|
+
// failure mode this plumbing exists to prevent.
|
|
967
|
+
const proxy = this._resolveStealthProxy();
|
|
955
968
|
try {
|
|
956
969
|
const { createRequire } = await import('module');
|
|
957
970
|
const require = createRequire(import.meta.url);
|
|
@@ -960,15 +973,35 @@ export class ResearchOrchestrator extends EventEmitter {
|
|
|
960
973
|
// A page whose own JavaScript throws must not take the process with
|
|
961
974
|
// it — see src/utils/firefoxPageErrorGuard.js.
|
|
962
975
|
guardFirefoxPageErrors();
|
|
963
|
-
|
|
976
|
+
const launchOptions = { headless: true };
|
|
977
|
+
if (proxy) {
|
|
978
|
+
// camoufox takes the URL string and splits the credentials out
|
|
979
|
+
// itself (getProxyUrl → Playwright's {server,username,password}).
|
|
980
|
+
launchOptions.proxy = proxy.url;
|
|
981
|
+
// Same rule as StealthBrowserManager (`geoip: !!proxy`): behind a
|
|
982
|
+
// proxy, geoip derives timezone, locale and geolocation from the
|
|
983
|
+
// exit IP, which is the only thing that makes a proxied browser
|
|
984
|
+
// coherent. Without a proxy it would look up this machine's own
|
|
985
|
+
// public IP — a network call, and an external service learning
|
|
986
|
+
// our address, to confirm a location the browser is already in.
|
|
987
|
+
// No locale is passed either way here, so geoip's derived one
|
|
988
|
+
// has nothing to contradict.
|
|
989
|
+
launchOptions.geoip = true;
|
|
990
|
+
}
|
|
991
|
+
this._stealthBrowser = await camoufox.Camoufox(launchOptions);
|
|
964
992
|
this._stealthEngineActive = 'camoufox';
|
|
965
|
-
this.logger.info('Stealth fallback using Camoufox (Firefox) engine'
|
|
993
|
+
this.logger.info('Stealth fallback using Camoufox (Firefox) engine', {
|
|
994
|
+
proxy: proxy ? proxy.redacted : null
|
|
995
|
+
});
|
|
966
996
|
return;
|
|
967
997
|
} catch (e) {
|
|
968
998
|
if (this.stealthEngine === 'camoufox') throw e; // explicit request → surface
|
|
969
999
|
this.logger.warn('Camoufox unavailable, falling back to Chromium stealth', { error: e.message });
|
|
970
1000
|
}
|
|
971
1001
|
}
|
|
1002
|
+
// No proxy wiring on this branch: StealthBrowserManager resolves its own
|
|
1003
|
+
// (resolveProxy), server-level list included, so passing one here would
|
|
1004
|
+
// be a second path to the same setting.
|
|
972
1005
|
const { StealthBrowserManager } = await import('./StealthBrowserManager.js');
|
|
973
1006
|
this._stealthManager = new StealthBrowserManager();
|
|
974
1007
|
await this._stealthManager.launchStealthBrowser({ level: this.stealthLevel });
|
|
@@ -978,6 +1011,43 @@ export class ResearchOrchestrator extends EventEmitter {
|
|
|
978
1011
|
await this._stealthInit;
|
|
979
1012
|
}
|
|
980
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* The proxy this stealth fallback launches behind: the caller's list when they
|
|
1016
|
+
* configured one, otherwise the server-level CRAWLFORGE_STEALTH_PROXIES.
|
|
1017
|
+
* Returns null when neither is set, and `{ url, redacted }` otherwise — only
|
|
1018
|
+
* the redacted form (scheme + host, no credentials) is ever logged.
|
|
1019
|
+
*
|
|
1020
|
+
* Resolved once per browser, taking the first entry: camoufox fixes its
|
|
1021
|
+
* fingerprint — and with geoip its timezone, locale and geolocation — at
|
|
1022
|
+
* launch, so rotating underneath it would leave it reporting the first
|
|
1023
|
+
* proxy's city behind the second proxy's exit IP, which is a worse signal
|
|
1024
|
+
* than not rotating at all (see StealthBrowserManager._doLaunchStealthBrowser).
|
|
1025
|
+
*/
|
|
1026
|
+
_resolveStealthProxy() {
|
|
1027
|
+
const raw = (this.stealthProxies[0] || '').trim();
|
|
1028
|
+
if (!raw) return null;
|
|
1029
|
+
|
|
1030
|
+
// `host:port` with no scheme parses as protocol "host:" and an empty host,
|
|
1031
|
+
// so the bare form gets the http:// every proxy list assumes.
|
|
1032
|
+
const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw : `http://${raw}`;
|
|
1033
|
+
let parsed;
|
|
1034
|
+
try {
|
|
1035
|
+
parsed = new URL(url);
|
|
1036
|
+
} catch {
|
|
1037
|
+
// Never echo the entry: it carries the password, and camoufox's own
|
|
1038
|
+
// `new URL(proxy)` would quote the whole string in its TypeError.
|
|
1039
|
+
throw new Error('Invalid stealth proxy: expected host:port or scheme://user:pass@host:port');
|
|
1040
|
+
}
|
|
1041
|
+
const scheme = parsed.protocol.replace(':', '').toLowerCase();
|
|
1042
|
+
if (!['http', 'https', 'socks4', 'socks5'].includes(scheme)) {
|
|
1043
|
+
throw new Error(`Invalid stealth proxy scheme "${scheme}": expected http, https, socks4 or socks5`);
|
|
1044
|
+
}
|
|
1045
|
+
if (!parsed.hostname) {
|
|
1046
|
+
throw new Error('Invalid stealth proxy: no host');
|
|
1047
|
+
}
|
|
1048
|
+
return { url, redacted: `${parsed.protocol}//${parsed.host}` };
|
|
1049
|
+
}
|
|
1050
|
+
|
|
981
1051
|
/**
|
|
982
1052
|
* macOS packaging fix for camoufox-js: it expects properties.json in
|
|
983
1053
|
* Camoufox.app/Contents/MacOS/, but the .app bundle ships it under
|