crawlforge-mcp-server 5.5.2 → 5.5.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.5.2",
3
+ "version": "5.5.3",
4
4
  "mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
5
5
  "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 29 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
@@ -105,7 +105,7 @@ const taskStore = createTaskStore({ logger });
105
105
  // Create the server
106
106
  const server = new McpServer({
107
107
  name: "crawlforge",
108
- version: "5.5.2",
108
+ version: "5.5.3",
109
109
  description: "Production-ready MCP server with 29 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, 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.",
110
110
  homepage: "https://www.crawlforge.dev",
111
111
  icon: "https://www.crawlforge.dev/icon.png",
@@ -1412,7 +1412,13 @@ registerToolIfEnabled("stealth_mode", {
1412
1412
  result = { disabled: true };
1413
1413
  break;
1414
1414
  case 'create_context': {
1415
- const contextData = await stealthBrowserManager.createStealthContext(stealthConfig);
1415
+ // Forward the tool-level engine the same way operation:"scrape" does —
1416
+ // without it, create_context always ran on chromium whatever the caller
1417
+ // asked for ("playwright" is this tool's public name for chromium).
1418
+ const contextData = await stealthBrowserManager.createStealthContext({
1419
+ ...(stealthConfig || {}),
1420
+ engine: engine === 'camoufox' ? 'camoufox' : 'chromium'
1421
+ });
1416
1422
  // The full fingerprint is ~4 KB of canvas noise arrays and WebGL
1417
1423
  // extension lists no caller acts on. Summarise by default; verbose:true
1418
1424
  // still returns all of it for debugging a detection failure.
@@ -434,9 +434,11 @@ export class StealthBrowserManager {
434
434
  async createStealthContext(config = {}) {
435
435
  const validatedConfig = StealthConfigSchema.parse({ ...this.defaultConfig, ...config });
436
436
 
437
- if (!this.browser) {
438
- await this.launchStealthBrowser(validatedConfig);
439
- }
437
+ // Always go through launchStealthBrowser: it returns the running browser
438
+ // when the engine matches, and closes + relaunches on an engine mismatch.
439
+ // Guarding on `!this.browser` here skipped that mismatch check, so a
440
+ // camoufox request silently reused an already-running chromium browser.
441
+ await this.launchStealthBrowser(validatedConfig);
440
442
 
441
443
  // Generate fingerprint for this context
442
444
  const fingerprint = this.generateAdvancedFingerprint(validatedConfig);
@@ -192,6 +192,14 @@ export class ExtractStructuredTool {
192
192
  // the model was shown would null every correct price.
193
193
  const fullSource = `${html}\n${textContent}`;
194
194
 
195
+ // A model that saw no answer sometimes fills a required field with a
196
+ // placeholder ("N/A", "unknown") instead of null. That carries no digits,
197
+ // so the provenance guard has nothing to check and the retry below never
198
+ // fired — git-scm.com is the case that found this: Readability drops the
199
+ // version box, the model answered "N/A", and the page plainly says 2.55.0.
200
+ const isPlaceholder = (v) => v == null ||
201
+ (typeof v === 'string' && (v.trim() === '' || /^(n\/?a|none|null|unknown|not (available|found|specified))$/i.test(v.trim())));
202
+
195
203
  /** Run the guard over one extraction and describe what it did. */
196
204
  const applyGuard = (result) => {
197
205
  const checked = verifyNumericProvenance(result.data || {}, fullSource);
@@ -201,9 +209,14 @@ export class ExtractStructuredTool {
201
209
  const nulledRequired = checked.unverified
202
210
  .map((entry) => entry.path)
203
211
  .filter((path) => (schema.required || []).includes(path));
212
+ // Retry currency: required fields with no usable answer, whether the
213
+ // guard nulled them or the model handed back a placeholder.
214
+ const missingRequired = (schema.required || [])
215
+ .filter((path) => nulledRequired.includes(path) || isPlaceholder(checked.data?.[path]));
204
216
  return {
205
217
  checked,
206
218
  nulledRequired,
219
+ missingRequired,
207
220
  result: {
208
221
  ...result,
209
222
  data: checked.data,
@@ -234,7 +247,7 @@ export class ExtractStructuredTool {
234
247
  // which is exactly what the guard will accept. Only a strictly better
235
248
  // result is kept, so a retry can never make the answer worse.
236
249
  const shownText = mainContentText($, html, url) || textContent;
237
- if (guarded.nulledRequired.length > 0 && shownText !== textContent && textContent) {
250
+ if (guarded.missingRequired.length > 0 && shownText !== textContent && textContent) {
238
251
  try {
239
252
  const llm = this._ensureLLMManager(llmConfig || {});
240
253
  const retry = await llm.extractStructured(textContent, schema, {
@@ -243,7 +256,7 @@ export class ExtractStructuredTool {
243
256
  });
244
257
  if (retry?.method === 'llm') {
245
258
  const retryGuarded = applyGuard(retry);
246
- if (retryGuarded.nulledRequired.length < guarded.nulledRequired.length) {
259
+ if (retryGuarded.missingRequired.length < guarded.missingRequired.length) {
247
260
  guarded = retryGuarded;
248
261
  }
249
262
  }