crawlforge-mcp-server 3.5.1 → 4.2.1

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.
Files changed (56) hide show
  1. package/package.json +6 -4
  2. package/server.js +138 -26
  3. package/src/cli/commands/actions.js +36 -0
  4. package/src/cli/commands/analyze.js +19 -0
  5. package/src/cli/commands/batch.js +45 -0
  6. package/src/cli/commands/crawl.js +30 -0
  7. package/src/cli/commands/extract.js +45 -0
  8. package/src/cli/commands/install-skills.js +46 -0
  9. package/src/cli/commands/llmstxt.js +24 -0
  10. package/src/cli/commands/localize.js +29 -0
  11. package/src/cli/commands/map.js +26 -0
  12. package/src/cli/commands/monitor.js +29 -0
  13. package/src/cli/commands/research.js +26 -0
  14. package/src/cli/commands/scrape.js +37 -0
  15. package/src/cli/commands/search.js +28 -0
  16. package/src/cli/commands/stealth.js +29 -0
  17. package/src/cli/commands/template.js +26 -0
  18. package/src/cli/commands/track.js +24 -0
  19. package/src/cli/commands/uninstall-skills.js +35 -0
  20. package/src/cli/formatter.js +57 -0
  21. package/src/cli/index.js +94 -0
  22. package/src/cli/lib/runTool.js +40 -0
  23. package/src/core/ActionExecutor.js +8 -6
  24. package/src/core/AuthManager.js +103 -3
  25. package/src/core/ChangeTracker.js +34 -0
  26. package/src/core/ElicitationHelper.js +112 -0
  27. package/src/core/JobManager.js +36 -2
  28. package/src/core/LocalizationManager.js +19 -5
  29. package/src/core/PerformanceManager.js +53 -17
  30. package/src/core/ResearchOrchestrator.js +40 -5
  31. package/src/core/SamplingClient.js +191 -0
  32. package/src/core/StealthBrowserManager.js +248 -2
  33. package/src/core/WebhookDispatcher.js +18 -10
  34. package/src/prompts/PromptRegistry.js +199 -0
  35. package/src/resources/ResourceRegistry.js +273 -0
  36. package/src/server/withAuth.js +25 -0
  37. package/src/skills/crawlforge-cli.md +157 -0
  38. package/src/skills/crawlforge-mcp.md +80 -0
  39. package/src/skills/crawlforge-research.md +104 -0
  40. package/src/skills/crawlforge-stealth.md +98 -0
  41. package/src/skills/installer.js +141 -0
  42. package/src/tools/advanced/batchScrape/index.js +30 -0
  43. package/src/tools/advanced/batchScrape/schema.js +1 -1
  44. package/src/tools/basic/extractText.js +19 -8
  45. package/src/tools/crawl/crawlDeep.js +27 -0
  46. package/src/tools/extract/extractContent.js +5 -17
  47. package/src/tools/extract/extractStructured.js +8 -0
  48. package/src/tools/extract/extractWithLlm.js +25 -5
  49. package/src/tools/extract/processDocument.js +7 -1
  50. package/src/tools/extract/summarizeContent.js +17 -0
  51. package/src/tools/research/deepResearch.js +34 -0
  52. package/src/tools/templates/ScrapeTemplateTool.js +68 -0
  53. package/src/tools/templates/TemplateRegistry.js +311 -0
  54. package/src/utils/Logger.js +15 -0
  55. package/src/utils/htmlToMarkdown.js +54 -0
  56. package/src/utils/secretMask.js +86 -0
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "3.5.1",
4
- "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 22 web scraping, crawling, and content processing tools. Defaults to local Ollama for LLM extraction (no API key needed); OpenAI/Anthropic available as opt-in.",
3
+ "version": "4.2.1",
4
+ "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 23 web scraping, crawling, and content processing tools. Defaults to local Ollama for LLM extraction (no API key needed); OpenAI/Anthropic available as opt-in. v4.0 adds Markdown-first output, pre-built site templates, Camoufox stealth engine, and cost transparency.",
5
5
  "main": "server.js",
6
6
  "bin": {
7
- "crawlforge": "server.js",
7
+ "crawlforge": "src/cli/index.js",
8
8
  "crawlforge-setup": "setup.js"
9
9
  },
10
10
  "scripts": {
@@ -19,7 +19,7 @@
19
19
  "test:tools": "node test-tools.js",
20
20
  "test:real-world": "node test-real-world.js",
21
21
  "test:all": "bash run-all-tests.sh",
22
- "postinstall": "echo '\n🎉 CrawlForge MCP Server installed!\n\nRun \"npx crawlforge-setup\" to configure your API key and get started.\n'",
22
+ "postinstall": "echo '\n\ud83c\udf89 CrawlForge MCP Server installed!\n\nRun \"npx crawlforge-setup\" to configure your API key and get started.\n'",
23
23
  "docker:build": "docker build -t crawlforge .",
24
24
  "docker:dev": "docker-compose up crawlforge-dev",
25
25
  "docker:prod": "docker-compose up crawlforge-prod"
@@ -96,6 +96,7 @@
96
96
  "@modelcontextprotocol/sdk": "^1.29.0",
97
97
  "@mozilla/readability": "^0.6.0",
98
98
  "cheerio": "^1.1.2",
99
+ "commander": "^12.1.0",
99
100
  "compromise": "^14.14.4",
100
101
  "diff": "^8.0.2",
101
102
  "dotenv": "^17.2.1",
@@ -109,6 +110,7 @@
109
110
  "pdf-parse": "^1.1.1",
110
111
  "playwright": "^1.54.2",
111
112
  "robots-parser": "^3.0.1",
113
+ "turndown": "^7.2.4",
112
114
  "winston": "^3.11.0",
113
115
  "zod": "^3.23.8"
114
116
  },
package/server.js CHANGED
@@ -5,7 +5,7 @@
5
5
  export { isCreatorModeVerified } from './src/core/creatorMode.js';
6
6
 
7
7
  // Import everything else
8
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { z } from "zod";
10
10
  import { logger } from "./src/utils/Logger.js";
11
11
  import { SearchWebTool } from "./src/tools/search/searchWeb.js";
@@ -23,6 +23,7 @@ import { ScrapeWithActionsTool } from "./src/tools/advanced/ScrapeWithActionsToo
23
23
  import { DeepResearchTool } from "./src/tools/research/deepResearch.js";
24
24
  import { TrackChangesTool } from "./src/tools/tracking/trackChanges/index.js";
25
25
  import { GenerateLLMsTxtTool } from "./src/tools/llmstxt/generateLLMsTxt.js";
26
+ import { ScrapeTemplateTool } from "./src/tools/templates/ScrapeTemplateTool.js"; // D3.3
26
27
  import { StealthBrowserManager } from "./src/core/StealthBrowserManager.js";
27
28
  import { LocalizationManager } from "./src/core/LocalizationManager.js";
28
29
  import { memoryMonitor } from "./src/utils/MemoryMonitor.js";
@@ -43,6 +44,10 @@ import { extractTextHandler } from "./src/tools/basic/extractText.js";
43
44
  import { extractLinksHandler } from "./src/tools/basic/extractLinks.js";
44
45
  import { extractMetadataHandler } from "./src/tools/basic/extractMetadata.js";
45
46
  import { scrapeStructuredHandler } from "./src/tools/basic/scrapeStructured.js";
47
+ // D1.1 Resources + D1.2 Prompts + D1.4 Elicitation
48
+ import { ResourceRegistry } from "./src/resources/ResourceRegistry.js";
49
+ import { PROMPTS, getPromptMessages } from "./src/prompts/PromptRegistry.js";
50
+ import { ElicitationHelper } from "./src/core/ElicitationHelper.js";
46
51
 
47
52
  // Initialize Authentication Manager
48
53
  await AuthManager.initialize();
@@ -90,8 +95,8 @@ if (configErrors.length > 0 && config.server.nodeEnv === 'production') {
90
95
  // Create the server
91
96
  const server = new McpServer({
92
97
  name: "crawlforge",
93
- version: "3.5.1",
94
- description: "Production-ready MCP server with 21 web scraping, crawling, and content processing tools. Features stealth browsing, deep research, structured extraction, change tracking, and local-LLM extraction via Ollama.",
98
+ version: "4.2.1",
99
+ description: "Production-ready MCP server with 23 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, change tracking, and local-LLM extraction via Ollama.",
95
100
  homepage: "https://www.crawlforge.dev",
96
101
  icon: "https://www.crawlforge.dev/icon.png"
97
102
  });
@@ -154,14 +159,99 @@ const scrapeWithActionsTool = new ScrapeWithActionsTool();
154
159
  const deepResearchTool = new DeepResearchTool();
155
160
  const trackChangesTool = new TrackChangesTool();
156
161
  const generateLLMsTxtTool = new GenerateLLMsTxtTool();
162
+ const scrapeTemplateTool = new ScrapeTemplateTool(); // D3.3
157
163
  const stealthBrowserManager = new StealthBrowserManager();
158
164
  const localizationManager = new LocalizationManager();
159
165
 
166
+ // D1.1: Resource Registry (wired to existing singletons)
167
+ const resourceRegistry = new ResourceRegistry({
168
+ researchOrchestrator: deepResearchTool, // exposes activeSessions
169
+ snapshotManager: null, // SnapshotManager not directly instantiated in server.js
170
+ jobManager: batchScrapeTool.jobManager,
171
+ mapSiteTool,
172
+ scrapeWithActionsTool,
173
+ });
174
+
175
+ // D1.4: Elicitation helper (client may not support — fails open)
176
+ const elicitation = new ElicitationHelper({ mcpServer: server, logger });
177
+
178
+ // D1.4: Wire elicitation into tools and AuthManager
179
+ deepResearchTool.setMcpServer(server);
180
+ batchScrapeTool.setMcpServer(server);
181
+ crawlDeepTool.setMcpServer(server);
182
+ extractStructuredTool.setMcpServer(server);
183
+ AuthManager.setElicitation(elicitation);
184
+
185
+ // ─── D1.1 Resource Templates (MCP Resources) ─────────────────────────────────
186
+ // Resources use the MCP ResourceTemplate URI pattern for dynamic crawlforge:// URIs.
187
+ // The registry is populated at runtime as tools produce artifacts.
188
+
189
+ // Research sessions: crawlforge://research/{sessionId}
190
+ server.resource(
191
+ "crawlforge-research",
192
+ new ResourceTemplate("crawlforge://research/{sessionId}", {
193
+ list: async () => ({
194
+ resources: resourceRegistry.listResources().filter(r => r.uri.startsWith("crawlforge://research/"))
195
+ })
196
+ }),
197
+ { description: "Completed deep_research report stored in the server session" },
198
+ async (uri) => resourceRegistry.readResource(uri)
199
+ );
200
+
201
+ // Job results: crawlforge://job/{jobId}
202
+ server.resource(
203
+ "crawlforge-job",
204
+ new ResourceTemplate("crawlforge://job/{jobId}", {
205
+ list: async () => ({
206
+ resources: resourceRegistry.listResources().filter(r => r.uri.startsWith("crawlforge://job/"))
207
+ })
208
+ }),
209
+ { description: "Completed batch_scrape job result" },
210
+ async (uri) => resourceRegistry.readResource(uri)
211
+ );
212
+
213
+ // Crawl sitemaps: crawlforge://crawl/{sessionId}/sitemap
214
+ server.resource(
215
+ "crawlforge-crawl-sitemap",
216
+ new ResourceTemplate("crawlforge://crawl/{sessionId}/sitemap", {
217
+ list: async () => ({
218
+ resources: resourceRegistry.listResources().filter(r => r.uri.startsWith("crawlforge://crawl/"))
219
+ })
220
+ }),
221
+ { description: "map_site output stored for a crawl session" },
222
+ async (uri) => resourceRegistry.readResource(uri)
223
+ );
224
+
225
+ // Screenshots: crawlforge://screenshot/{actionId}
226
+ server.resource(
227
+ "crawlforge-screenshot",
228
+ new ResourceTemplate("crawlforge://screenshot/{actionId}", {
229
+ list: async () => ({
230
+ resources: resourceRegistry.listResources().filter(r => r.uri.startsWith("crawlforge://screenshot/"))
231
+ })
232
+ }),
233
+ { description: "Screenshot from scrape_with_actions" },
234
+ async (uri) => resourceRegistry.readResource(uri)
235
+ );
236
+
237
+ // ─── D1.2 Prompts (workflow templates) ────────────────────────────────────────
238
+ // Register the 5 CrawlForge workflow prompts from PromptRegistry.
239
+
240
+ for (const p of PROMPTS) {
241
+ const argsShape = {};
242
+ for (const arg of p.arguments) {
243
+ argsShape[arg.name] = z.string().optional().describe(arg.description);
244
+ }
245
+ server.registerPrompt(p.name, { description: p.description, argsSchema: argsShape }, async (args) => {
246
+ return getPromptMessages(p.name, args || {});
247
+ });
248
+ }
249
+
160
250
  // ─── Tool registrations ────────────────────────────────────────────────────────
161
251
 
162
252
  // Tool: fetch_url
163
253
  server.registerTool("fetch_url", {
164
- description: "Fetch content from a URL with optional headers and timeout",
254
+ description: "Use this when you need raw HTTP content from a URL HTML, JSON, XML, or plain text. Ideal as the first step before extract_text or extract_content. Supports custom headers (e.g. auth tokens) and configurable timeout. Example: fetch_url({url: \"https://example.com\", timeout: 15000})",
165
255
  annotations: { title: "Fetch URL", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
166
256
  inputSchema: {
167
257
  url: z.string().url().describe("The URL to fetch content from"),
@@ -172,18 +262,19 @@ server.registerTool("fetch_url", {
172
262
 
173
263
  // Tool: extract_text
174
264
  server.registerTool("extract_text", {
175
- description: "Extract clean text content from a webpage",
265
+ description: "Use this when you need a page's human-readable text or markdown stripped of HTML tags, scripts, and styles — e.g. for keyword search, summarization, RAG ingestion, or NLP. Use output_format:\"markdown\" for RAG workflows. Faster than extract_content but returns unstructured content. Example: extract_text({url: \"https://example.com/article\", output_format:\"markdown\"})",
176
266
  annotations: { title: "Extract Text", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
177
267
  inputSchema: {
178
268
  url: z.string().url().describe("The URL to extract text from"),
179
269
  remove_scripts: z.boolean().optional().default(true).describe("Remove script tags before extraction"),
180
- remove_styles: z.boolean().optional().default(true).describe("Remove style tags before extraction")
270
+ remove_styles: z.boolean().optional().default(true).describe("Remove style tags before extraction"),
271
+ output_format: z.enum(["text", "markdown"]).optional().default("text").describe("Output format: \"text\" (default) or \"markdown\" — use markdown for RAG workflows")
181
272
  }
182
273
  }, withAuth("extract_text", extractTextHandler));
183
274
 
184
275
  // Tool: extract_links
185
276
  server.registerTool("extract_links", {
186
- description: "Extract all links from a webpage with optional filtering",
277
+ description: "Use this when you need to discover all hyperlinks on a page e.g. to build a crawl seed list, audit broken links, or find related resources. Use filter_external:true to get only outbound links. Example: extract_links({url: \"https://example.com\", filter_external: true})",
187
278
  annotations: { title: "Extract Links", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
188
279
  inputSchema: {
189
280
  url: z.string().url().describe("The URL to extract links from"),
@@ -194,7 +285,7 @@ server.registerTool("extract_links", {
194
285
 
195
286
  // Tool: extract_metadata
196
287
  server.registerTool("extract_metadata", {
197
- description: "Extract metadata from a webpage (title, description, keywords, etc.)",
288
+ description: "Use this when you need a page's SEO metadata: title, meta description, Open Graph tags, canonical URL, schema.org data. Ideal for site audits and competitive SEO analysis. Example: extract_metadata({url: \"https://example.com\"})",
198
289
  annotations: { title: "Extract Metadata", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
199
290
  inputSchema: {
200
291
  url: z.string().url().describe("The URL to extract metadata from")
@@ -203,7 +294,7 @@ server.registerTool("extract_metadata", {
203
294
 
204
295
  // Tool: scrape_structured
205
296
  server.registerTool("scrape_structured", {
206
- description: "Extract structured data from a webpage using CSS selectors",
297
+ description: "Use this when you know the exact CSS selectors for the data you want — e.g. scraping a pricing table or product list with consistent markup. More reliable than LLM extraction for well-structured pages. Example: scrape_structured({url: \"https://shop.com/products\", selectors: {price: \".price\", name: \".product-title\"}})",
207
298
  annotations: { title: "Scrape Structured Data", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
208
299
  inputSchema: {
209
300
  url: z.string().url().describe("The URL to scrape"),
@@ -213,7 +304,7 @@ server.registerTool("scrape_structured", {
213
304
 
214
305
  // Tool: search_web
215
306
  server.registerTool("search_web", {
216
- description: "Search the web using Google Search API (proxied through CrawlForge)",
307
+ description: "Use this when you need web search results for a query — returns titles, URLs, snippets, and optional metadata. Supports language, date range, and site filters. Start research workflows here before using fetch_url or deep_research. Example: search_web({query: \"best MCP servers 2025\", limit: 10, time_range: \"month\"})",
217
308
  annotations: { title: "Search the Web", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
218
309
  inputSchema: {
219
310
  query: z.string().describe("Search query string"),
@@ -239,7 +330,7 @@ server.registerTool("search_web", {
239
330
 
240
331
  // Tool: crawl_deep
241
332
  server.registerTool("crawl_deep", {
242
- description: "Crawl websites deeply using breadth-first search",
333
+ description: "Use this when you need to discover and optionally extract content from many pages within a site — e.g. building a knowledge base, indexing docs, or auditing all pages. Use map_site first to estimate scope, then crawl_deep for content. Example: crawl_deep({url: \"https://docs.example.com\", max_depth: 3, max_pages: 200, extract_content: true})",
243
334
  annotations: { title: "Deep Crawl", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
244
335
  inputSchema: {
245
336
  url: z.string().url().describe("Starting URL for the crawl"),
@@ -266,7 +357,7 @@ server.registerTool("crawl_deep", {
266
357
 
267
358
  // Tool: map_site
268
359
  server.registerTool("map_site", {
269
- description: "Discover and map website structure",
360
+ description: "Use this when you need to know all URLs on a domain without fetching full page content — e.g. before a crawl_deep, for a site audit, or to find specific section URLs. Reads sitemap.xml when available. Example: map_site({url: \"https://example.com\", include_sitemap: true, max_urls: 500})",
270
361
  annotations: { title: "Map Website", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
271
362
  inputSchema: {
272
363
  url: z.string().url().describe("The website URL to map"),
@@ -289,7 +380,7 @@ server.registerTool("map_site", {
289
380
 
290
381
  // Tool: extract_content
291
382
  server.registerTool("extract_content", {
292
- description: "Extract and analyze main content from web pages with enhanced readability detection",
383
+ description: "Use this when you need a clean, readable version of a web article or page — removes ads, nav, footers, and boilerplate. Ideal for RAG ingestion, summarization, or LLM context. Prefer this over extract_text for article-style pages. Example: extract_content({url: \"https://blog.example.com/post-title\"})",
293
384
  annotations: { title: "Extract Content", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
294
385
  inputSchema: {
295
386
  url: z.string().url().describe("The URL to extract content from"),
@@ -309,7 +400,7 @@ server.registerTool("extract_content", {
309
400
 
310
401
  // Tool: process_document
311
402
  server.registerTool("process_document", {
312
- description: "Process documents from multiple sources and formats including PDFs and web pages",
403
+ description: "Use this when you need to extract text from a PDF URL or file e.g. research papers, contracts, reports. Also handles HTML URLs. Returns structured sections, metadata, and word count. Example: process_document({source: \"https://example.com/report.pdf\", sourceType: \"pdf_url\"})",
313
404
  annotations: { title: "Process Document", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
314
405
  inputSchema: {
315
406
  source: z.string().describe("Document source - URL or file path"),
@@ -330,7 +421,7 @@ server.registerTool("process_document", {
330
421
 
331
422
  // Tool: summarize_content
332
423
  server.registerTool("summarize_content", {
333
- description: "Generate intelligent summaries of text content with configurable options",
424
+ description: "Use this when you have text content (from extract_text or extract_content) and need a condensed version — e.g. for briefings, comparison tables, or LLM context reduction. Supports extractive (sentence selection) and abstractive (rewrite via Ollama/sampling) modes. Example: summarize_content({text: \"..long article..\", options: {summaryLength: \"short\", summaryType: \"abstractive\"}})",
334
425
  annotations: { title: "Summarize Content", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
335
426
  inputSchema: {
336
427
  text: z.string().describe("The text content to summarize"),
@@ -350,7 +441,7 @@ server.registerTool("summarize_content", {
350
441
 
351
442
  // Tool: analyze_content
352
443
  server.registerTool("analyze_content", {
353
- description: "Perform comprehensive content analysis including language detection and topic extraction",
444
+ description: "Use this when you need NLP metrics for text — language detection, sentiment, topic extraction, entity recognition, readability score. Good for content auditing and classification. Example: analyze_content({text: \"..article text..\", options: {extractTopics: true, includeSentiment: true}})",
354
445
  annotations: { title: "Analyze Content", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
355
446
  inputSchema: {
356
447
  text: z.string().describe("The text content to analyze"),
@@ -370,7 +461,7 @@ server.registerTool("analyze_content", {
370
461
 
371
462
  // Tool: extract_structured
372
463
  server.registerTool("extract_structured", {
373
- description: "Extract structured data from a webpage using LLM-powered analysis and a JSON Schema. Falls back to CSS selector extraction when no LLM provider is configured.",
464
+ description: "Use this when you need a specific data shape extracted from a page using a JSON schema e.g. product details, job listings, event data. Uses LLM by default; falls back to CSS selectors when no LLM is configured. Example: extract_structured({url: \"https://jobs.example.com/post/123\", schema: {properties: {title: {type:\"string\"}, salary: {type:\"string\"}}, required:[\"title\"]}})",
374
465
  annotations: { title: "Extract Structured Data", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
375
466
  inputSchema: {
376
467
  url: z.string().url().describe("The URL to extract structured data from"),
@@ -437,7 +528,7 @@ server.registerTool("list_ollama_models", {
437
528
 
438
529
  // Tool: batch_scrape
439
530
  server.registerTool("batch_scrape", {
440
- description: "Process multiple URLs simultaneously with support for async job management and webhook notifications",
531
+ description: "Use this when you need to scrape 2–50 URLs in parallel — e.g. batch-collecting product pages, news articles, or competitor pages. Use mode:\"async\" with a webhook for large batches; mode:\"sync\" for up to ~25 URLs when you need results immediately. Example: batch_scrape({urls: [\"https://a.com\",\"https://b.com\"], formats: [\"json\"], maxConcurrency: 5})",
441
532
  annotations: { title: "Batch Scrape", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
442
533
  inputSchema: {
443
534
  urls: z.array(z.union([
@@ -482,7 +573,7 @@ server.registerTool("batch_scrape", {
482
573
 
483
574
  // Tool: scrape_with_actions
484
575
  server.registerTool("scrape_with_actions", {
485
- description: "Execute browser action chains before scraping content, with form auto-fill and intermediate state capture",
576
+ description: "Use this when you need to interact with a page before scraping — login, click buttons, fill forms, scroll, or wait for dynamic content to load. Use for SPAs, login-gated content, or multi-step flows. Screenshots from this tool are stored as crawlforge://screenshot/{actionId} resources. Example: scrape_with_actions({url: \"https://app.com/dashboard\", actions: [{type:\"click\",selector:\"#login\"},{type:\"type\",selector:\"#email\",text:\"user@a.com\"}]})",
486
577
  annotations: { title: "Scrape with Browser Actions", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
487
578
  inputSchema: {
488
579
  url: z.string().url().describe("The URL to scrape"),
@@ -538,7 +629,7 @@ server.registerTool("scrape_with_actions", {
538
629
 
539
630
  // Tool: deep_research
540
631
  server.registerTool("deep_research", {
541
- description: "Conduct comprehensive multi-stage research with intelligent query expansion, source verification, and conflict detection",
632
+ description: "Use this when you need exhaustive multi-source research on a topic — it searches the web, fetches and analyses sources, detects conflicts, and (when LLM keys or Ollama are configured) synthesizes a report. Best for complex questions needing 10+ sources. Will request confirmation (elicitation) if maxUrls > 50. Results are stored as crawlforge://research/{sessionId} resources. Example: deep_research({topic: \"quantum computing NISQ devices 2025\", maxUrls: 30, researchApproach: \"academic\"})",
542
633
  annotations: { title: "Deep Research", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
543
634
  inputSchema: {
544
635
  topic: z.string().min(3).max(500).describe("Research topic or question"),
@@ -594,7 +685,7 @@ server.registerTool("deep_research", {
594
685
 
595
686
  // Tool: track_changes
596
687
  server.registerTool("track_changes", {
597
- description: "Enhanced content change tracking with baseline capture, comparison, scheduled monitoring, advanced comparison engine, alert system, and historical analysis",
688
+ description: "Use this when you need to monitor a URL for content changes over time e.g. competitor pricing, regulation updates, product availability. Start with operation:\"create_baseline\", then periodically use operation:\"compare\" to diff. Supports webhooks and scheduled monitoring. Example: track_changes({url: \"https://example.com/pricing\", operation: \"create_baseline\"})",
598
689
  annotations: { title: "Track Changes", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
599
690
  inputSchema: {
600
691
  url: z.string().url().describe("The URL to track changes for"),
@@ -699,7 +790,7 @@ server.registerTool("track_changes", {
699
790
 
700
791
  // Tool: generate_llms_txt
701
792
  server.registerTool("generate_llms_txt", {
702
- description: "Analyze websites and generate standard-compliant LLMs.txt and LLMs-full.txt files defining AI model interaction guidelines",
793
+ description: "Use this when you need to generate an llms.txt file for a website — the standard that tells AI models how to interact with a site's content. Useful for site owners preparing for AI discoverability, or for understanding a site's AI access policy. Example: generate_llms_txt({url: \"https://example.com\"})",
703
794
  annotations: { title: "Generate llms.txt", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
704
795
  inputSchema: {
705
796
  url: z.string().url().describe("The website URL to generate llms.txt for"),
@@ -733,7 +824,7 @@ server.registerTool("generate_llms_txt", {
733
824
 
734
825
  // Tool: stealth_mode
735
826
  server.registerTool("stealth_mode", {
736
- description: "Advanced anti-detection browser management with stealth features, fingerprint randomization, and human behavior simulation",
827
+ description: "Use this when a site blocks normal scraping — Cloudflare, Datadome, or other bot-detection systems. Manages a Playwright browser with randomized fingerprints, human behavior simulation, WebRTC/canvas spoofing. Start with operation:\"create_context\" then use the contextId. Example: stealth_mode({operation:\"create_context\", stealthConfig:{level:\"advanced\", simulateHumanBehavior:true}})",
737
828
  annotations: { title: "Stealth Mode", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
738
829
  inputSchema: {
739
830
  operation: z.enum(['configure', 'enable', 'disable', 'create_context', 'create_page', 'get_stats', 'cleanup']).default('configure').describe("Stealth operation to perform"),
@@ -775,6 +866,7 @@ server.registerTool("stealth_mode", {
775
866
  hardwareSpoofing: z.boolean().default(true)
776
867
  }).optional()
777
868
  }).optional().describe("Stealth browser configuration with anti-detection settings"),
869
+ engine: z.enum(["playwright", "camoufox"]).optional().default("playwright").describe("Browser engine: \"playwright\" (Chromium, default) or \"camoufox\" (Firefox-based, higher anti-detect score — install with npm install camoufox)"),
778
870
  contextId: z.string().optional().describe("Browser context ID for page operations"),
779
871
  urlToTest: z.string().url().optional().describe("URL to navigate to when creating a page")
780
872
  }
@@ -827,7 +919,7 @@ server.registerTool("stealth_mode", {
827
919
 
828
920
  // Tool: localization
829
921
  server.registerTool("localization", {
830
- description: "Multi-language and geo-location management with country-specific settings, browser locale emulation, timezone spoofing, and geo-blocked content handling",
922
+ description: "Use this when you need to scrape geo-restricted content or emulate a specific locale/timezone e.g. seeing region-specific pricing, bypassing geo-blocks, or searching in another language. Use operation:\"configure_country\" to set country context. Example: localization({operation:\"configure_country\", countryCode:\"DE\", language:\"de\"})",
831
923
  annotations: { title: "Localization", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
832
924
  inputSchema: {
833
925
  operation: z.enum(['configure_country', 'localize_search', 'localize_browser', 'generate_timezone_spoof', 'handle_geo_blocking', 'auto_detect', 'get_stats', 'get_supported_countries']).default('configure_country').describe("Localization operation to perform"),
@@ -931,6 +1023,25 @@ server.registerTool("localization", {
931
1023
  }
932
1024
  }));
933
1025
 
1026
+
1027
+ // Tool: scrape_template (D3.3 — pre-built site templates)
1028
+ server.registerTool("scrape_template", {
1029
+ description: "Use this when you want structured data from a well-known site without writing custom selectors. Pass template:\"list\" to see all available templates. Supports: amazon-product, linkedin-profile, github-repo, youtube-video, tweet, reddit-thread, hacker-news-front-page, producthunt-launch, stackoverflow-question, npm-package. Example: scrape_template({template:\"github-repo\", url:\"https://github.com/user/repo\"})",
1030
+ annotations: { title: "Scrape Template", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1031
+ inputSchema: {
1032
+ template: z.string().describe("Template ID (e.g. github-repo) or list to enumerate available templates"),
1033
+ url: z.string().url().optional().describe("URL to scrape — required unless template is list"),
1034
+ timeout: z.number().min(5000).max(60000).optional().default(15000).describe("Request timeout in milliseconds")
1035
+ }
1036
+ }, withAuth("scrape_template", async (params) => {
1037
+ try {
1038
+ const result = await scrapeTemplateTool.execute(params);
1039
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1040
+ } catch (error) {
1041
+ return { content: [{ type: "text", text: `Template scrape failed: ${error.message}` }], isError: true };
1042
+ }
1043
+ }));
1044
+
934
1045
  // ─── Transport + startup ───────────────────────────────────────────────────────
935
1046
 
936
1047
  const useHttp = process.argv.includes('--http') || process.env.MCP_HTTP === 'true';
@@ -980,9 +1091,10 @@ async function runServer() {
980
1091
  "extract_content", "process_document", "summarize_content", "analyze_content",
981
1092
  "batch_scrape", "scrape_with_actions",
982
1093
  "deep_research", "track_changes", "generate_llms_txt",
983
- "stealth_mode", "localization", "extract_structured", "extract_with_llm"
1094
+ "stealth_mode", "localization", "extract_structured", "extract_with_llm",
1095
+ "scrape_template" // D3.3
984
1096
  ];
985
- console.error(`Tools available: ${allTools.join(', ')}`);
1097
+ console.error(`Tools available (23): ${allTools.join(", ")}`);
986
1098
 
987
1099
  // Start memory monitoring in development
988
1100
  if (config.server.nodeEnv === "development") {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * actions command — run browser automation actions from a script file.
3
+ */
4
+ import { ScrapeWithActionsTool } from '../../tools/advanced/ScrapeWithActionsTool.js';
5
+ import { getToolConfig } from '../../constants/config.js';
6
+ import { runTool } from '../lib/runTool.js';
7
+ import { readFileSync } from 'node:fs';
8
+
9
+ export function register(program) {
10
+ program
11
+ .command('actions <url>')
12
+ .description('Run browser automation actions against a URL')
13
+ .requiredOption('--script <file>', 'JSON file containing action script')
14
+ .option('--screenshot', 'Capture screenshot after actions')
15
+ .option('--wait <ms>', 'Wait time between actions in milliseconds', '500')
16
+ .action(async (url, opts, cmd) => {
17
+ const globals = cmd.parent.opts();
18
+ const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
19
+
20
+ let actions;
21
+ try {
22
+ actions = JSON.parse(readFileSync(opts.script, 'utf8'));
23
+ } catch (e) {
24
+ process.stderr.write(`Error reading script file: ${e.message}\n`);
25
+ process.exit(1);
26
+ }
27
+
28
+ const tool = new ScrapeWithActionsTool(getToolConfig('scrape_with_actions'));
29
+ await runTool(tool, {
30
+ url,
31
+ actions,
32
+ screenshot: !!opts.screenshot,
33
+ wait_between_actions: parseInt(opts.wait, 10)
34
+ }, cliFlags);
35
+ });
36
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * analyze command — analyze content of a URL.
3
+ */
4
+ import { AnalyzeContentTool } from '../../tools/extract/analyzeContent.js';
5
+ import { getToolConfig } from '../../constants/config.js';
6
+ import { runTool } from '../lib/runTool.js';
7
+
8
+ export function register(program) {
9
+ program
10
+ .command('analyze <url>')
11
+ .description('Analyze content of a URL (sentiment, entities, readability)')
12
+ .option('--depth <level>', 'Analysis depth: basic or full', 'basic')
13
+ .action(async (url, opts, cmd) => {
14
+ const globals = cmd.parent.opts();
15
+ const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
16
+ const tool = new AnalyzeContentTool(getToolConfig('analyze_content'));
17
+ await runTool(tool, { url, analysis_depth: opts.depth }, cliFlags);
18
+ });
19
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * batch command — scrape multiple URLs from a file.
3
+ * Reads newline-delimited URLs from the specified file.
4
+ */
5
+ import { BatchScrapeTool } from '../../tools/advanced/BatchScrapeTool.js';
6
+ import { getToolConfig } from '../../constants/config.js';
7
+ import { runTool } from '../lib/runTool.js';
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ export function register(program) {
11
+ program
12
+ .command('batch <urls-file>')
13
+ .description('Scrape multiple URLs from a newline-delimited file')
14
+ .option('--format <fmt>', 'Output format: text, markdown, html', 'markdown')
15
+ .option('--concurrency <n>', 'Concurrent requests', '5')
16
+ .option('--max-retries <n>', 'Maximum retries per URL', '2')
17
+ .action(async (urlsFile, opts, cmd) => {
18
+ const globals = cmd.parent.opts();
19
+ const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
20
+
21
+ let urls;
22
+ try {
23
+ urls = readFileSync(urlsFile, 'utf8')
24
+ .split('\n')
25
+ .map(l => l.trim())
26
+ .filter(l => l && !l.startsWith('#'));
27
+ } catch (e) {
28
+ process.stderr.write(`Error reading URLs file: ${e.message}\n`);
29
+ process.exit(1);
30
+ }
31
+
32
+ if (urls.length === 0) {
33
+ process.stderr.write('Error: No URLs found in file\n');
34
+ process.exit(1);
35
+ }
36
+
37
+ const tool = new BatchScrapeTool(getToolConfig('batch_scrape'));
38
+ await runTool(tool, {
39
+ urls,
40
+ formats: [opts.format],
41
+ maxConcurrency: parseInt(opts.concurrency, 10),
42
+ jobOptions: { maxRetries: parseInt(opts.maxRetries, 10) }
43
+ }, cliFlags);
44
+ });
45
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * crawl command — deep crawl a website using crawl_deep tool.
3
+ */
4
+ import { CrawlDeepTool } from '../../tools/crawl/crawlDeep.js';
5
+ import { getToolConfig } from '../../constants/config.js';
6
+ import { runTool } from '../lib/runTool.js';
7
+
8
+ export function register(program) {
9
+ program
10
+ .command('crawl <url>')
11
+ .description('Deep crawl a website and extract its content')
12
+ .option('--depth <n>', 'Maximum crawl depth (1-5)', '3')
13
+ .option('--max-pages <n>', 'Maximum pages to crawl', '100')
14
+ .option('--no-robots', 'Ignore robots.txt')
15
+ .option('--follow-external', 'Follow external links')
16
+ .option('--concurrency <n>', 'Concurrent requests (1-20)', '10')
17
+ .action(async (url, opts, cmd) => {
18
+ const globals = cmd.parent.opts();
19
+ const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
20
+ const tool = new CrawlDeepTool(getToolConfig('crawl_deep'));
21
+ await runTool(tool, {
22
+ url,
23
+ max_depth: parseInt(opts.depth, 10),
24
+ max_pages: parseInt(opts.maxPages, 10),
25
+ respect_robots: opts.robots !== false,
26
+ follow_external: !!opts.followExternal,
27
+ concurrency: parseInt(opts.concurrency, 10)
28
+ }, cliFlags);
29
+ });
30
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * extract command — extract structured data or LLM-guided extraction.
3
+ * With --schema: uses extract_structured (JSON schema-based).
4
+ * With --prompt: uses extract_with_llm (natural language).
5
+ */
6
+ import { ExtractStructuredTool } from '../../tools/extract/extractStructured.js';
7
+ import { ExtractWithLlm } from '../../tools/extract/extractWithLlm.js';
8
+ import { getToolConfig } from '../../constants/config.js';
9
+ import { runTool } from '../lib/runTool.js';
10
+ import { readFileSync } from 'node:fs';
11
+
12
+ export function register(program) {
13
+ program
14
+ .command('extract <url>')
15
+ .description('Extract structured data from a URL')
16
+ .option('--schema <file>', 'JSON schema file for structured extraction')
17
+ .option('--prompt <text>', 'Natural language prompt for LLM-guided extraction')
18
+ .option('--model <model>', 'LLM model to use (ollama model name or openai/anthropic)')
19
+ .action(async (url, opts, cmd) => {
20
+ const globals = cmd.parent.opts();
21
+ const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
22
+
23
+ if (opts.schema) {
24
+ let schema;
25
+ try {
26
+ schema = JSON.parse(readFileSync(opts.schema, 'utf8'));
27
+ } catch (e) {
28
+ process.stderr.write(`Error reading schema file: ${e.message}\n`);
29
+ process.exit(1);
30
+ }
31
+ const tool = new ExtractStructuredTool(getToolConfig('extract_structured'));
32
+ await runTool(tool, { url, schema }, cliFlags);
33
+ } else if (opts.prompt) {
34
+ const tool = new ExtractWithLlm(getToolConfig('extract_with_llm'));
35
+ await runTool(tool, {
36
+ url,
37
+ prompt: opts.prompt,
38
+ model: opts.model
39
+ }, cliFlags);
40
+ } else {
41
+ process.stderr.write('Error: extract requires --schema <file> or --prompt <text>\n');
42
+ process.exit(1);
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * install-skills command -- install CrawlForge skill files into AI coding tools.
3
+ */
4
+ import { install } from '../../skills/installer.js';
5
+
6
+ export function register(program) {
7
+ program
8
+ .command('install-skills')
9
+ .description('Install CrawlForge skill files into Claude Code, Cursor, or VS Code')
10
+ .option('--target <target>', 'Target: claude-code, cursor, vscode, or all', 'all')
11
+ .option('--force', 'Overwrite existing skill files')
12
+ .option('--dry-run', 'Show what would be installed without writing files')
13
+ .action(async (opts) => {
14
+ try {
15
+ const results = await install({
16
+ target: opts.target,
17
+ force: Boolean(opts.force),
18
+ dryRun: Boolean(opts.dryRun),
19
+ cwd: process.cwd()
20
+ });
21
+
22
+ if (opts.dryRun) {
23
+ process.stdout.write('Dry run -- would install to:\n');
24
+ results.paths.forEach(p => process.stdout.write(' ' + p + '\n'));
25
+ process.exit(0);
26
+ return;
27
+ }
28
+
29
+ if (results.installed.length > 0) {
30
+ process.stdout.write('Installed:\n');
31
+ results.installed.forEach(p => process.stdout.write(' ' + p + '\n'));
32
+ }
33
+ if (results.skipped.length > 0) {
34
+ process.stdout.write('Skipped (already installed; use --force to overwrite):\n');
35
+ results.skipped.forEach(p => process.stdout.write(' ' + p + '\n'));
36
+ }
37
+ if (results.installed.length === 0 && results.skipped.length === 0) {
38
+ process.stdout.write('Nothing to install.\n');
39
+ }
40
+ process.exit(0);
41
+ } catch (err) {
42
+ process.stderr.write('Error: ' + err.message + '\n');
43
+ process.exit(1);
44
+ }
45
+ });
46
+ }