crawlforge-mcp-server 5.0.1 → 5.0.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 27 web scraping, crawling, and content processing tools (5 inline + 22 advanced).
64
64
 
65
- **Current Version:** 5.0.1
65
+ **Current Version:** 5.0.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.0.1",
3
+ "version": "5.0.2",
4
4
  "mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
5
5
  "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 27 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
@@ -99,7 +99,7 @@ const taskStore = createTaskStore({ logger });
99
99
  // Create the server
100
100
  const server = new McpServer({
101
101
  name: "crawlforge",
102
- version: "5.0.1",
102
+ version: "5.0.2",
103
103
  description: "Production-ready MCP server with 27 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, change tracking, local-LLM extraction via Ollama, unified multi-format scrape, and autonomous agent tool.",
104
104
  homepage: "https://www.crawlforge.dev",
105
105
  icon: "https://www.crawlforge.dev/icon.png",
@@ -228,7 +228,9 @@ export class LLMsTxtAnalyzer {
228
228
  $('a[href*="api"], a[href*="developer"], a[href*="docs"]').each((_, element) => {
229
229
  const href = $(element).attr('href');
230
230
  const text = $(element).text().toLowerCase();
231
- if (href && (text.includes('api') || text.includes('developer'))) {
231
+ // Word-boundary match: substring checks flagged "Sapiens"/"rapid"
232
+ // style words as API links.
233
+ if (href && /\b(api|developer)s?\b/.test(text)) {
232
234
  apis.push({
233
235
  url: new URL(href, baseUrl).toString(),
234
236
  type: 'documentation',
@@ -546,7 +546,9 @@ export class ContentAnalyzer {
546
546
  const people = doc.people().out('array');
547
547
  const places = doc.places().out('array');
548
548
  const organizations = doc.organizations().out('array');
549
- const dates = doc.dates().out('array');
549
+ // .dates() needs the compromise-dates plugin (not installed) and threw,
550
+ // aborting ALL entity extraction; #Date+ tag matching is core compromise.
551
+ const dates = doc.match('#Date+').out('array');
550
552
  const money = doc.money().out('array');
551
553
  let other = doc.topics().out('array').slice(0, 10);
552
554
 
@@ -28,7 +28,8 @@ const SEMANTIC_FIELD_SELECTORS = {
28
28
  summary: ['article p', 'main p', 'p'],
29
29
  author: ['[rel="author"]', '.author', '.byline'],
30
30
  date: ['time', '.date'],
31
- published: ['time', '.published', '.date']
31
+ published: ['time', '.published', '.date'],
32
+ price: ['[itemprop="price"]', '[class*="price"]']
32
33
  };
33
34
 
34
35
  const ExtractStructuredSchema = z.object({
@@ -225,6 +225,14 @@ export class GenerateLLMsTxtTool {
225
225
  emitSection('Tools', flatten('tools'));
226
226
  emitSection('Navigation', flatten('navigation'));
227
227
 
228
+ // Fallback: if no categorized section produced output, list the raw
229
+ // sitemap so llms.txt always carries a URL inventory. Must run BEFORE the
230
+ // APIs section — an APIs entry alone used to set hasBody and suppress it.
231
+ const hasBody = lines.some((l) => l.startsWith('## '));
232
+ if (!hasBody) {
233
+ emitSection('Pages', analysis.structure?.sitemap || []);
234
+ }
235
+
228
236
  // APIs as their own section.
229
237
  if (Array.isArray(analysis.apis) && analysis.apis.length > 0) {
230
238
  lines.push('## APIs');
@@ -236,12 +244,6 @@ export class GenerateLLMsTxtTool {
236
244
  lines.push('');
237
245
  }
238
246
 
239
- // Fallback: if no categorized sections produced output, list the raw sitemap.
240
- const hasBody = lines.some((l) => l.startsWith('## '));
241
- if (!hasBody) {
242
- emitSection('Pages', analysis.structure?.sitemap || []);
243
- }
244
-
245
247
  return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
246
248
  }
247
249
 
@@ -87,9 +87,13 @@ const TEMPLATES = [
87
87
  description: attr($, 'meta[property="og:description"]', 'content') || text($, 'p.f4.my-3'),
88
88
  stars: text($, '#repo-stars-counter-star') || text($, '[aria-label*="stargazers"]'),
89
89
  forks: text($, '#repo-network-counter') || text($, '[aria-label*="forks"]'),
90
- watchers: text($, '[aria-label*="watchers"]'),
90
+ // React (logged-out) layout has no watchers aria-label; the count is
91
+ // the <strong> right after the single octicon-eye. Language is a
92
+ // client-side skeleton on that layout — unrecoverable from static
93
+ // HTML, so it stays null there (itemprop still works on classic).
94
+ watchers: text($, '.octicon-eye + strong') || text($, '[aria-label*="watchers"]'),
91
95
  language: text($, 'span[itemprop="programmingLanguage"]') || text($, '.d-inline-flex[class*="language"]'),
92
- topics: list($, 'a.topic-tag'),
96
+ topics: list($, 'a.topic-tag, a[href^="/topics/"]'),
93
97
  license: text($, 'a[href*="blob/"][href*="LICENSE"]') || text($, '.octicon-law ~ span'),
94
98
  last_updated: attr($, 'relative-time', 'datetime'),
95
99
  homepage: attr($, 'a[href][rel="noopener noreferrer"]', 'href'),