crawlforge-mcp-server 5.2.1 → 5.2.2

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 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.2.1
65
+ **Current Version:** 5.2.2
66
66
 
67
67
  ## Development Commands
68
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.2.1",
3
+ "version": "5.2.2",
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",
package/server.js CHANGED
@@ -100,7 +100,7 @@ const taskStore = createTaskStore({ logger });
100
100
  // Create the server
101
101
  const server = new McpServer({
102
102
  name: "crawlforge",
103
- version: "5.2.1",
103
+ version: "5.2.2",
104
104
  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
105
  homepage: "https://www.crawlforge.dev",
106
106
  icon: "https://www.crawlforge.dev/icon.png",
@@ -106,24 +106,25 @@ const LANGUAGE_NAMES = {
106
106
  'rus': 'Russian',
107
107
  'jpn': 'Japanese',
108
108
  'kor': 'Korean',
109
- 'chi': 'Chinese',
110
- 'ara': 'Arabic',
109
+ 'cmn': 'Chinese',
110
+ 'arb': 'Arabic',
111
111
  'hin': 'Hindi',
112
112
  'nld': 'Dutch',
113
113
  'swe': 'Swedish',
114
- 'nor': 'Norwegian',
114
+ 'nob': 'Norwegian',
115
115
  'dan': 'Danish',
116
116
  'fin': 'Finnish',
117
117
  'pol': 'Polish',
118
118
  'ces': 'Czech',
119
119
  'hun': 'Hungarian',
120
120
  'tur': 'Turkish',
121
- 'gre': 'Greek',
121
+ 'ell': 'Greek',
122
122
  'heb': 'Hebrew',
123
123
  'tha': 'Thai',
124
124
  'vie': 'Vietnamese',
125
125
  'ind': 'Indonesian',
126
- 'msa': 'Malay',
126
+ 'zlm': 'Malay',
127
+ 'zsm': 'Malay',
127
128
  'tgl': 'Tagalog',
128
129
  'ukr': 'Ukrainian',
129
130
  'bul': 'Bulgarian',
@@ -282,6 +283,28 @@ export class ContentAnalyzer {
282
283
  */
283
284
  async detectLanguage(text, options = {}) {
284
285
  try {
286
+ // franc scores the single most common script, so a Chinese, Japanese or
287
+ // Korean page carrying the usual run of English product names and code
288
+ // samples is detected as English. Those scripts never appear in
289
+ // Latin-script prose, so a meaningful share of them settles the question
290
+ // before trigram scoring gets a say.
291
+ const letters = (text.match(/\p{L}/gu) || []).length;
292
+ if (letters > 0) {
293
+ const han = (text.match(/\p{Script=Han}/gu) || []).length;
294
+ const kana = (text.match(/[\p{Script=Hiragana}\p{Script=Katakana}]/gu) || []).length;
295
+ const hangul = (text.match(/\p{Script=Hangul}/gu) || []).length;
296
+ if ((han + kana + hangul) / letters >= 0.1) {
297
+ const code = kana > 0 ? 'jpn' : hangul > han ? 'kor' : 'cmn';
298
+ return {
299
+ code,
300
+ name: LANGUAGE_NAMES[code],
301
+ confidence: 0.9,
302
+ alternative: [],
303
+ detectionMethod: 'script'
304
+ };
305
+ }
306
+ }
307
+
285
308
  // Use franc for language detection
286
309
  const detected = franc(text, {
287
310
  minLength: 10,
@@ -20,6 +20,38 @@ import { stripHiddenHtml } from './hiddenContent.js';
20
20
 
21
21
  let _td = null;
22
22
 
23
+ // Mirrors turndown-plugin-gfm's own heading-row test, which is what decides
24
+ // whether it converts a table or keeps it as raw HTML.
25
+ function isHeadingRow(tr) {
26
+ const parent = tr.parentNode;
27
+ if (!parent) return false;
28
+ if (parent.nodeName === 'THEAD') return true;
29
+ const firstTbody =
30
+ parent.nodeName === 'TBODY' &&
31
+ (!parent.previousSibling ||
32
+ (parent.previousSibling.nodeName === 'THEAD' &&
33
+ /^\s*$/.test(parent.previousSibling.textContent)));
34
+ return (
35
+ parent.firstChild === tr &&
36
+ (parent.nodeName === 'TABLE' || firstTbody) &&
37
+ Array.prototype.every.call(tr.childNodes, n => n.nodeName === 'TH')
38
+ );
39
+ }
40
+
41
+ function isLayoutTable(node) {
42
+ return (
43
+ node.nodeName === 'TABLE' &&
44
+ !(node.rows && node.rows[0] && isHeadingRow(node.rows[0]))
45
+ );
46
+ }
47
+
48
+ function isInLayoutTable(node) {
49
+ for (let p = node.parentNode; p; p = p.parentNode) {
50
+ if (p.nodeName === 'TABLE') return isLayoutTable(p);
51
+ }
52
+ return false;
53
+ }
54
+
23
55
  function getTurndown() {
24
56
  if (_td === null) {
25
57
  _td = new TurndownService({
@@ -35,6 +67,26 @@ function getTurndown() {
35
67
  // Enable GFM extensions (tables, strikethrough, task lists)
36
68
  _td.use(gfm);
37
69
 
70
+ // turndown-plugin-gfm only converts a table whose first row is all <th>;
71
+ // every other table is passed through its `keep` filter as raw HTML. Pages
72
+ // we scrape are full of layout tables (Hacker News, older sites), so that
73
+ // leaks <table> markup into a field the caller asked for as markdown.
74
+ // Rules added here are matched before keep filters, so these reclaim the
75
+ // tables the plugin skipped and flatten them to their cell content, while
76
+ // real data tables still reach the plugin and render as pipe tables.
77
+ _td.addRule('layoutTable', {
78
+ filter: isLayoutTable,
79
+ replacement: content => '\n\n' + content.replace(/\n{3,}/g, '\n\n').trim() + '\n\n'
80
+ });
81
+ _td.addRule('layoutTableCell', {
82
+ filter: node => (node.nodeName === 'TH' || node.nodeName === 'TD') && isInLayoutTable(node),
83
+ replacement: content => (content.trim() ? content.trim() + ' ' : '')
84
+ });
85
+ _td.addRule('layoutTableRow', {
86
+ filter: node => node.nodeName === 'TR' && isInLayoutTable(node),
87
+ replacement: content => (content.trim() ? content.trim() + '\n' : '')
88
+ });
89
+
38
90
  // Remove boilerplate elements before converting
39
91
  _td.remove(['script', 'style', 'nav', 'footer', 'aside', 'noscript']);
40
92
  }