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 +1 -1
- package/package.json +1 -1
- package/server.js +1 -1
- package/src/core/analysis/ContentAnalyzer.js +28 -5
- package/src/utils/htmlToMarkdown.js +52 -0
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.
|
|
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.
|
|
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.
|
|
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
|
-
'
|
|
110
|
-
'
|
|
109
|
+
'cmn': 'Chinese',
|
|
110
|
+
'arb': 'Arabic',
|
|
111
111
|
'hin': 'Hindi',
|
|
112
112
|
'nld': 'Dutch',
|
|
113
113
|
'swe': 'Swedish',
|
|
114
|
-
'
|
|
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
|
-
'
|
|
121
|
+
'ell': 'Greek',
|
|
122
122
|
'heb': 'Hebrew',
|
|
123
123
|
'tha': 'Thai',
|
|
124
124
|
'vie': 'Vietnamese',
|
|
125
125
|
'ind': 'Indonesian',
|
|
126
|
-
'
|
|
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
|
}
|