crawlforge-mcp-server 5.0.4 → 5.0.5

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.4
65
+ **Current Version:** 5.0.5
66
66
 
67
67
  ## Development Commands
68
68
 
@@ -181,7 +181,7 @@ fetch_url, extract_text, extract_links, extract_metadata, scrape_structured
181
181
  search_web, serp_rank, crawl_deep, map_site, extract_content, process_document, summarize_content, analyze_content, extract_structured, extract_with_llm, list_ollama_models, batch_scrape, scrape_with_actions, deep_research, track_changes, generate_llms_txt, stealth_mode, localization, scrape_template, scrape, agent
182
182
 
183
183
  **serp_rank (DataForSEO):**
184
- - `serp_rank` — reports where a target domain ranks in Google's REAL organic results for a keyword (the SERP position Google Custom Search / `search_web` cannot give). Backed by the DataForSEO Google Organic SERP API (Live Advanced, `POST /v3/serp/google/organic/live/advanced`, HTTP Basic auth). Credentials via `DATAFORSEO_LOGIN` / `DATAFORSEO_PASSWORD`, billed to the user's own DataForSEO account (~US$0.002/call), separate from CrawlForge credits. When unconfigured it returns `{ configured:false }` and charges **0** credits; when configured, **Cost: 5**. Never fabricates a rank.
184
+ - `serp_rank` — reports where a target domain ranks in Google's REAL organic results for a keyword (the SERP position Google Custom Search / `search_web` cannot give). Backed by the DataForSEO Google Organic SERP API (Live Advanced, `POST /v3/serp/google/organic/live/advanced`, HTTP Basic auth). Credentials via `DATAFORSEO_LOGIN` / `DATAFORSEO_PASSWORD`, billed to the user's own DataForSEO account (~US$0.002 per 10 results of `depth`, so $0.004 at the default `depth:20` and $0.02 at `depth:100`), separate from CrawlForge credits. When unconfigured it returns `{ configured:false }` and charges **0** credits; when configured, **Cost: 5**. Never fabricates a rank.
185
185
 
186
186
  **v4.6.0 additions (Phase D):**
187
187
  - `scrape` — single fetch + one cheerio load dispatching a `formats` array (markdown/html/rawHtml/text/links/metadata/screenshot/json-schema) + `onlyMainContent`; partial-success via per-format `warnings[]`. Cost: 2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.0.4",
3
+ "version": "5.0.5",
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",
@@ -127,7 +127,8 @@
127
127
  "turndown-plugin-gfm": "^1.0.2",
128
128
  "undici": "^7.24.0",
129
129
  "winston": "^3.11.0",
130
- "zod": "^3.23.8"
130
+ "zod": "^3.23.8",
131
+ "zod-to-json-schema": "^3.25.1"
131
132
  },
132
133
  "optionalDependencies": {
133
134
  "camoufox": "^0.1.19"
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.2",
102
+ version: "5.0.5",
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",
@@ -427,7 +427,7 @@ registerToolIfEnabled("serp_rank", {
427
427
  location_code: z.number().optional().describe("Numeric DataForSEO location code (overrides location_name)"),
428
428
  language_code: z.string().optional().describe("Language code (e.g. 'en')"),
429
429
  device: z.enum(["desktop", "mobile"]).optional().describe("Device to emulate"),
430
- depth: z.number().min(10).max(200).optional().describe("How many results to scan, 10-200 (100 = 1 page of cost)")
430
+ depth: z.number().min(10).max(200).optional().describe("How many results to scan, 10-200 (default 20; DataForSEO bills ~$0.002 per 10 and gets slower the deeper it goes)")
431
431
  },
432
432
  outputSchema: OUTPUT_SCHEMAS.serp_rank
433
433
  }, withAuth("serp_rank", async ({ keyword, target, location_name, location_code, language_code, device, depth }) => {
@@ -644,7 +644,7 @@ class AuthManager {
644
644
  case 'serp_rank':
645
645
  note = projected === 0
646
646
  ? 'DataForSEO not configured — no-op, no credits charged. Set DATAFORSEO_LOGIN/PASSWORD to enable.'
647
- : 'DataForSEO SERP API (~US$0.002/call) billed to your own DataForSEO account, separate from the credit cost.';
647
+ : 'DataForSEO SERP API (~US$0.002 per 10 results of depth — $0.004 at the default depth 20) billed to your own DataForSEO account, separate from the credit cost.';
648
648
  break;
649
649
  case 'scrape': {
650
650
  projected = base;
@@ -30,10 +30,50 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
30
30
  import { createServer } from 'node:http';
31
31
  import { randomUUID } from 'node:crypto';
32
32
  import { readFileSync } from 'node:fs';
33
+ import { z } from 'zod';
34
+ import { zodToJsonSchema } from 'zod-to-json-schema';
33
35
 
34
36
  const pkg = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
35
37
  const SERVER_VERSION = pkg.version;
36
38
 
39
+ /**
40
+ * Build the `tools` array for the Smithery static server card, straight from
41
+ * the live tool registry.
42
+ *
43
+ * Smithery scans a published server to populate its listing, but our /mcp
44
+ * endpoint 401s without a key, so the scan cannot enumerate anything. Their
45
+ * documented fallback is a static server card carrying the metadata — and a
46
+ * card with no `tools` leaves the listing showing whatever was typed in by
47
+ * hand at publish time, which is how it goes stale.
48
+ *
49
+ * Deriving it here means the card tracks the registry on every release instead
50
+ * of drifting. Tool `inputSchema`s are registered as ZodRawShapes, so wrap
51
+ * before converting; a tool whose schema will not convert is still listed,
52
+ * with an open object schema, rather than dropped.
53
+ */
54
+ function buildToolCards(server) {
55
+ const registered = server?._registeredTools ?? {};
56
+ return Object.entries(registered)
57
+ .filter(([, tool]) => tool?.enabled !== false)
58
+ .map(([name, tool]) => {
59
+ let inputSchema = { type: 'object', properties: {} };
60
+ try {
61
+ const shape = tool?.inputSchema;
62
+ if (shape) {
63
+ const zodObject = typeof shape?.safeParse === 'function' ? shape : z.object(shape);
64
+ const converted = zodToJsonSchema(zodObject, { $refStrategy: 'none' });
65
+ delete converted.$schema;
66
+ inputSchema = converted;
67
+ }
68
+ } catch {
69
+ // Keep the tool visible with an open schema rather than hiding it.
70
+ }
71
+ const card = { name, description: tool?.description ?? tool?.annotations?.title ?? '', inputSchema };
72
+ if (tool?.annotations) card.annotations = tool.annotations;
73
+ return card;
74
+ });
75
+ }
76
+
37
77
  /**
38
78
  * The MCP SDK's Protocol.connect() allows at most one active transport per
39
79
  * Server/McpServer instance (it throws 'Already connected to a transport'
@@ -163,6 +203,10 @@ export async function connectStreamableHttp(server, authManager, logger, options
163
203
  icon: 'https://www.crawlforge.dev/icon.png'
164
204
  },
165
205
  transport: { type: 'streamable-http', url: '/mcp' },
206
+ authentication: { required: true, schemes: ['apiKey'] },
207
+ tools: buildToolCards(server),
208
+ resources: [],
209
+ prompts: [],
166
210
  configSchema: {
167
211
  type: 'object',
168
212
  properties: {
@@ -11,9 +11,13 @@
11
11
  * Endpoint (Live Advanced, synchronous — one request, one response):
12
12
  * POST https://api.dataforseo.com/v3/serp/google/organic/live/advanced
13
13
  *
14
- * Cost: ~US$0.002 per 100 results (depth) on Live Advanced. For high-volume
15
- * scheduled tracking, DataForSEO's task-based "Standard" queue (task_post
16
- * tasks_ready task_get) is cheaper; swap the endpoint + poll if cost matters.
14
+ * Cost: US$0.002 per 10 results of `depth` on Live Advanced measured live,
15
+ * not estimated: depth 10 bills $0.002, the depth 20 default bills $0.004, and
16
+ * depth 100 bills $0.02. Deeper scans are also slower (see the timeout note
17
+ * below), so raise `depth` only when a rank below the default is worth paying
18
+ * for. For high-volume scheduled tracking, DataForSEO's task-based "Standard"
19
+ * queue (task_post → tasks_ready → task_get) is cheaper; swap the endpoint +
20
+ * poll if cost matters.
17
21
  */
18
22
 
19
23
  export class DataForSEOSearchAdapter {
@@ -25,9 +29,16 @@ export class DataForSEOSearchAdapter {
25
29
  this.login = login;
26
30
  this.password = password;
27
31
  this.apiBaseUrl = options.apiBaseUrl || 'https://api.dataforseo.com';
28
- // Live Advanced is synchronous and usually answers in a few seconds; cap it
29
- // so a hung connection can't wedge the tool. Overridable for tests/self-host.
30
- this.timeoutMs = options.timeoutMs ?? 30000;
32
+ // Live Advanced is synchronous and runs a real-time Google scrape, so its
33
+ // latency swings widely with their capacity AND with `depth`. Measured on
34
+ // one account in a single session: ~13-15s at depth 10, and 30s / 45s /
35
+ // over 60s (twice) for the SAME depth-100 request. A killed request is
36
+ // still billed — DataForSEO has already run the scrape — so the cap exists
37
+ // only to stop a hung connection wedging the tool, and sits well above the
38
+ // slow end rather than through the middle of it.
39
+ // Overridable via DATAFORSEO_TIMEOUT_MS, or directly for tests/self-host.
40
+ this.timeoutMs =
41
+ options.timeoutMs ?? (Number(process.env.DATAFORSEO_TIMEOUT_MS) || 120000);
31
42
  // HTTP Basic auth header, computed once.
32
43
  this.authHeader = 'Basic ' + Buffer.from(`${login}:${password}`).toString('base64');
33
44
  }
@@ -40,7 +51,7 @@ export class DataForSEOSearchAdapter {
40
51
  * @param {number} [params.locationCode] - Numeric DataForSEO location code (overrides locationName)
41
52
  * @param {string} [params.languageCode='en'] - Language code
42
53
  * @param {('desktop'|'mobile')} [params.device='desktop'] - Device to emulate
43
- * @param {number} [params.depth=100] - How many results to scan (100 = one page of cost)
54
+ * @param {number} [params.depth=20] - How many results to scan (billed per 10)
44
55
  * @returns {Promise<{items: Array<Object>, meta: Object}>} Normalized organic results + metadata
45
56
  */
46
57
  async search(params) {
@@ -50,7 +61,7 @@ export class DataForSEOSearchAdapter {
50
61
  locationCode,
51
62
  languageCode = 'en',
52
63
  device = 'desktop',
53
- depth = 100,
64
+ depth = 20,
54
65
  } = params;
55
66
 
56
67
  if (!keyword) {
@@ -24,7 +24,10 @@ const SerpRankSchema = z.object({
24
24
  location_code: z.number().int().optional(),
25
25
  language_code: z.string().optional().default('en'),
26
26
  device: z.enum(['desktop', 'mobile']).optional().default('desktop'),
27
- depth: z.number().int().min(10).max(200).optional().default(100), // DataForSEO caps depth at 200
27
+ // DataForSEO bills per 10 results scanned and gets slower the deeper it goes,
28
+ // so the default stays shallow: 20 covers Google's first two pages for $0.004.
29
+ // Raise it (max 200, their cap) when a deeper position is worth the spend.
30
+ depth: z.number().int().min(10).max(200).optional().default(20),
28
31
  });
29
32
 
30
33
  /** Reduce a domain or URL to a bare, comparable host: "https://www.Example.com/x" → "example.com". */