scorchcrawl-mcp 2.0.0 → 2.1.0

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/dist/index.js CHANGED
@@ -4,7 +4,6 @@ import { FastMCP } from './lib/fastmcp/index.js';
4
4
  import { z } from 'zod';
5
5
  import ScorchClient from './lib/scorch-client/index.js';
6
6
  import { localScrape, isLocalProxyEnabled, getCleanApiUrl } from './local-scraper.js';
7
- import { getCopilotClient } from './copilot-client.js';
8
7
  import { mapError, safeExecute, processResponse, processResponseSync, } from './response-utils.js';
9
8
  dotenv.config({ debug: false, quiet: true });
10
9
  function extractApiKey(headers) {
@@ -20,22 +19,6 @@ function extractApiKey(headers) {
20
19
  }
21
20
  return undefined;
22
21
  }
23
- /**
24
- * Extract a GitHub / Copilot token from request headers.
25
- * Clients set one of:
26
- * x-copilot-token: <token>
27
- * x-github-token: <token>
28
- * If neither header is present the server-wide env var
29
- * GITHUB_TOKEN is used as a fallback.
30
- */
31
- function extractCopilotToken(headers) {
32
- const copilotHeader = (headers['x-copilot-token'] ||
33
- headers['x-github-token']);
34
- if (copilotHeader) {
35
- return Array.isArray(copilotHeader) ? copilotHeader[0] : copilotHeader;
36
- }
37
- return process.env.GITHUB_TOKEN || undefined;
38
- }
39
22
  function removeEmptyTopLevel(obj) {
40
23
  const out = {};
41
24
  for (const [k, v] of Object.entries(obj)) {
@@ -90,14 +73,12 @@ const server = new FastMCP({
90
73
  logger: new ConsoleLogger(),
91
74
  roots: { enabled: false },
92
75
  authenticate: async (request) => {
93
- // Extract per-user Copilot token (falls back to env var)
94
- const copilotToken = extractCopilotToken(request.headers);
95
76
  if (process.env.CLOUD_SERVICE === 'true') {
96
77
  const apiKey = extractApiKey(request.headers);
97
78
  if (!apiKey) {
98
79
  throw new Error('API key is required');
99
80
  }
100
- return { scraperApiKey: apiKey, copilotToken };
81
+ return { scraperApiKey: apiKey };
101
82
  }
102
83
  else {
103
84
  // For self-hosted instances, API key is optional if SCORCHCRAWL_API_URL is provided
@@ -105,7 +86,7 @@ const server = new FastMCP({
105
86
  console.error('Either SCORCHCRAWL_API_KEY or SCORCHCRAWL_API_URL must be provided');
106
87
  process.exit(1);
107
88
  }
108
- return { scraperApiKey: process.env.SCORCHCRAWL_API_KEY, copilotToken };
89
+ return { scraperApiKey: process.env.SCORCHCRAWL_API_KEY };
109
90
  }
110
91
  },
111
92
  // Lightweight health endpoint for LB checks
@@ -348,8 +329,6 @@ ${SAFE_MODE
348
329
  });
349
330
  return await processResponse(res, {
350
331
  url: String(url),
351
- getCopilotClientFn: getCopilotClient,
352
- copilotToken: session?.copilotToken,
353
332
  });
354
333
  }
355
334
  // SPA detected: the local fetch returned a JS-only shell.
@@ -365,8 +344,6 @@ ${SAFE_MODE
365
344
  });
366
345
  return await processResponse(res, {
367
346
  url: String(url),
368
- getCopilotClientFn: getCopilotClient,
369
- copilotToken: session?.copilotToken,
370
347
  });
371
348
  }
372
349
  // Errors from local scraper
@@ -381,8 +358,6 @@ ${SAFE_MODE
381
358
  }
382
359
  return await processResponse(localResult, {
383
360
  url: String(url),
384
- getCopilotClientFn: getCopilotClient,
385
- copilotToken: session?.copilotToken,
386
361
  });
387
362
  }
388
363
  // --- Normal mode: use remote scraping API ---
@@ -394,8 +369,6 @@ ${SAFE_MODE
394
369
  });
395
370
  return await processResponse(res, {
396
371
  url: String(url),
397
- getCopilotClientFn: getCopilotClient,
398
- copilotToken: session?.copilotToken,
399
372
  });
400
373
  }, { tool: 'scorch_scrape', url: String(url) });
401
374
  },
@@ -1,24 +1,17 @@
1
1
  /**
2
2
  * Response Processing Utilities
3
3
  *
4
- * Three layers of intelligent response handling for MCP tool results:
4
+ * Two layers of intelligent response handling for MCP tool results:
5
5
  * 1. Error Mapping — classifies errors and returns actionable guidance
6
6
  * 2. Content Truncation — smart paragraph-boundary truncation with notices
7
- * 3. AI Summarization — Copilot SDK-powered content compression (optional)
8
7
  *
9
8
  * No competitor MCP scraper implements any of these. This is a ScorchCrawl
10
9
  * differentiator.
11
10
  */
12
- import { createHash } from 'node:crypto';
13
11
  // ---------------------------------------------------------------------------
14
12
  // Configuration (from environment)
15
13
  // ---------------------------------------------------------------------------
16
14
  const MAX_CONTENT_CHARS = parseInt(process.env.SCORCHCRAWL_MAX_CONTENT_CHARS || '25000', 10);
17
- const SUMMARIZE_AFTER_WORDS = parseInt(process.env.SCORCHCRAWL_SUMMARIZE_AFTER_WORDS || '5000', 10);
18
- const SUMMARIZE_MODEL = process.env.SCORCHCRAWL_SUMMARIZE_MODEL || 'gpt-4o';
19
- const SUMMARIZE_CACHE_SIZE = parseInt(process.env.SCORCHCRAWL_SUMMARIZE_CACHE_SIZE || '100', 10);
20
- const SUMMARIZE_MAX_PER_MINUTE = parseInt(process.env.SCORCHCRAWL_SUMMARIZE_MAX_PER_MINUTE || '10', 10);
21
- const SUMMARIZE_TIMEOUT_MS = parseInt(process.env.SCORCHCRAWL_SUMMARIZE_TIMEOUT_MS || '300000', 10);
22
15
  /**
23
16
  * Classify a raw error (from fetch, SDK, or engine) into a structured
24
17
  * MappedError with actionable guidance for the LLM.
@@ -335,205 +328,6 @@ function buildTruncationNotice(shownChars, originalChars) {
335
328
  return `\n\n---\n[Content truncated. Showing ~${Math.round(shownChars / 1000)}k of ~${Math.round(originalChars / 1000)}k characters.\nTo get specific information, try:\n- Use JSON format with a schema to extract only the data you need\n- Add onlyMainContent: true to exclude navigation/footers\n- Use scorch_map to find a more specific page URL]`;
336
329
  }
337
330
  // ---------------------------------------------------------------------------
338
- // Feature 3: AI Summarization
339
- // ---------------------------------------------------------------------------
340
- /**
341
- * LRU cache for summarization results.
342
- * Key: sha256(url + first 1000 chars of markdown)
343
- * Value: { summary, timestamp }
344
- */
345
- const summaryCache = new Map();
346
- const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
347
- /** Sliding window for summarization rate limiting */
348
- const summarizationTimestamps = [];
349
- /** Word count helper */
350
- export function wordCount(text) {
351
- return text.split(/\s+/).filter(Boolean).length;
352
- }
353
- /** Generate a cache key for summarization */
354
- function summaryKey(url, markdown) {
355
- const fingerprint = url + markdown.slice(0, 1000);
356
- return createHash('sha256').update(fingerprint).digest('hex');
357
- }
358
- /** Check if we can make another summarization call (rate limit) */
359
- function canSummarize() {
360
- if (SUMMARIZE_MAX_PER_MINUTE <= 0)
361
- return false;
362
- const now = Date.now();
363
- const windowStart = now - 60_000;
364
- // Purge old entries
365
- while (summarizationTimestamps.length > 0 && summarizationTimestamps[0] < windowStart) {
366
- summarizationTimestamps.shift();
367
- }
368
- return summarizationTimestamps.length < SUMMARIZE_MAX_PER_MINUTE;
369
- }
370
- /** Record a summarization call for rate limiting */
371
- function recordSummarization() {
372
- summarizationTimestamps.push(Date.now());
373
- }
374
- /** Evict oldest entries if cache exceeds max size */
375
- function evictCache() {
376
- if (summaryCache.size <= SUMMARIZE_CACHE_SIZE)
377
- return;
378
- // Find and delete the oldest entries
379
- const entries = [...summaryCache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp);
380
- const toDelete = entries.slice(0, entries.length - SUMMARIZE_CACHE_SIZE);
381
- for (const [key] of toDelete) {
382
- summaryCache.delete(key);
383
- }
384
- }
385
- /**
386
- * Summarize markdown content using the Copilot SDK if available.
387
- * Falls back to truncation on failure or when no LLM is available.
388
- *
389
- * @param markdown - The full markdown content to potentially summarize
390
- * @param url - Source URL (used for cache key)
391
- * @param getCopilotClientFn - Function to get a CopilotClient instance
392
- * @param copilotToken - Optional per-user Copilot token
393
- * @returns Processed markdown with summarization metadata
394
- */
395
- export async function summarizeIfNeeded(markdown, url, getCopilotClientFn, copilotToken) {
396
- // Check if summarization is disabled
397
- if (SUMMARIZE_AFTER_WORDS <= 0) {
398
- return { markdown, summarized: false };
399
- }
400
- const words = wordCount(markdown);
401
- if (words <= SUMMARIZE_AFTER_WORDS) {
402
- return { markdown, summarized: false };
403
- }
404
- // Check cache
405
- const key = summaryKey(url, markdown);
406
- const cached = summaryCache.get(key);
407
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
408
- return {
409
- markdown: cached.summary,
410
- summarized: true,
411
- meta: {
412
- _summarized: true,
413
- _originalWordCount: words,
414
- _summaryWordCount: cached.wordCount,
415
- _summarizedBy: SUMMARIZE_MODEL,
416
- _cachedResult: true,
417
- },
418
- };
419
- }
420
- // Check if Copilot client is available
421
- if (!getCopilotClientFn) {
422
- return { markdown, summarized: false };
423
- }
424
- // Check rate limit
425
- if (!canSummarize()) {
426
- return { markdown, summarized: false };
427
- }
428
- try {
429
- const client = await getCopilotClientFn(copilotToken);
430
- if (!client) {
431
- return { markdown, summarized: false };
432
- }
433
- const targetWords = Math.min(SUMMARIZE_AFTER_WORDS, Math.floor(words * 0.3));
434
- const sessionConfig = {
435
- model: SUMMARIZE_MODEL,
436
- systemMessage: {
437
- mode: 'replace',
438
- content: buildSummarizationPrompt(words, targetWords),
439
- },
440
- };
441
- const session = await client.createSession(sessionConfig);
442
- recordSummarization();
443
- try {
444
- const response = await session.sendAndWait({ prompt: markdown }, SUMMARIZE_TIMEOUT_MS);
445
- // Extract content from response — handle multiple possible shapes
446
- let summary = '';
447
- if (response && typeof response === 'object') {
448
- const r = response;
449
- // Shape 1: { data: { content: "..." } } (most common)
450
- if (r.data && typeof r.data === 'object') {
451
- const d = r.data;
452
- if (typeof d.content === 'string')
453
- summary = d.content;
454
- else if (typeof d.text === 'string')
455
- summary = d.text;
456
- }
457
- // Shape 2: { content: "..." }
458
- if (!summary && typeof r.content === 'string')
459
- summary = r.content;
460
- // Shape 3: { text: "..." }
461
- if (!summary && typeof r.text === 'string')
462
- summary = r.text;
463
- // Shape 4: { message: { content: "..." } }
464
- if (!summary && r.message && typeof r.message === 'object') {
465
- const m = r.message;
466
- if (typeof m.content === 'string')
467
- summary = m.content;
468
- }
469
- }
470
- else if (typeof response === 'string') {
471
- summary = response;
472
- }
473
- if (!summary || summary.length < 50) {
474
- // Summarization returned garbage — fall back
475
- console.error(`[Summarization] Response too short or empty (${summary.length} chars), response keys: ${response ? Object.keys(response).join(', ') : 'null'}`);
476
- return { markdown, summarized: false };
477
- }
478
- const summaryWords = wordCount(summary);
479
- // Cache the result
480
- summaryCache.set(key, {
481
- summary,
482
- timestamp: Date.now(),
483
- wordCount: summaryWords,
484
- });
485
- evictCache();
486
- return {
487
- markdown: summary,
488
- summarized: true,
489
- meta: {
490
- _summarized: true,
491
- _originalWordCount: words,
492
- _summaryWordCount: summaryWords,
493
- _summarizedBy: SUMMARIZE_MODEL,
494
- },
495
- };
496
- }
497
- finally {
498
- try {
499
- await session.destroy();
500
- }
501
- catch { /* ignore cleanup */ }
502
- }
503
- }
504
- catch (err) {
505
- // Summarization failed — fall back silently but log for debugging
506
- const errMsg = err instanceof Error ? err.message : String(err);
507
- const errStack = err instanceof Error ? err.stack : '';
508
- console.error(`[Summarization] Failed for ${url}: ${errMsg}`);
509
- if (errStack)
510
- console.error(`[Summarization] Stack: ${errStack}`);
511
- return {
512
- markdown,
513
- summarized: false,
514
- meta: { _summarizationFailed: true, _summarizationError: errMsg },
515
- };
516
- }
517
- }
518
- /** Build the system prompt for content summarization */
519
- function buildSummarizationPrompt(originalWords, targetWords) {
520
- return `You are a content summarizer for a web scraping tool.
521
- Summarize the following web page content, preserving:
522
- - ALL factual data (numbers, dates, names, prices, specifications)
523
- - Key arguments, conclusions, and findings
524
- - Structural organization (use headings, lists)
525
- - Any code snippets or technical details
526
-
527
- Do NOT:
528
- - Add opinions or analysis
529
- - Omit specific data points (numbers, names, URLs)
530
- - Change the meaning or emphasis of the content
531
- - Add any preamble like "Here is a summary" — just output the summary directly
532
-
533
- Original content word count: ${originalWords}
534
- Target summary: ~${targetWords} words`;
535
- }
536
- // ---------------------------------------------------------------------------
537
331
  // Combined Processing Pipeline
538
332
  // ---------------------------------------------------------------------------
539
333
  /**
@@ -559,85 +353,23 @@ export async function safeExecute(fn, context) {
559
353
  }
560
354
  }
561
355
  /**
562
- * Process a successful response: truncate content if needed and
563
- * optionally summarize via Copilot SDK.
356
+ * Process a successful response: truncate content if needed.
564
357
  *
565
358
  * Replaces the old `asText()` function.
566
359
  */
567
360
  export async function processResponse(data, options) {
568
- // Step 1: Try AI summarization on markdown content
569
- if (!options?.skipSummarization && options?.getCopilotClientFn) {
570
- const md = extractMarkdown(data);
571
- const wc = md ? wordCount(md) : 0;
572
- console.error(`[Summarization] Check: markdown=${md ? md.length : 0} chars, ${wc} words, threshold=${SUMMARIZE_AFTER_WORDS}`);
573
- if (md && wc > SUMMARIZE_AFTER_WORDS && SUMMARIZE_AFTER_WORDS > 0) {
574
- console.error(`[Summarization] Attempting summarization for ${options.url || 'unknown url'} (${wc} words)`);
575
- const result = await summarizeIfNeeded(md, options.url || '', options.getCopilotClientFn, options.copilotToken);
576
- if (result.summarized) {
577
- // Replace the markdown in the data with the summary
578
- const updated = injectMarkdown(data, result.markdown, result.meta || {});
579
- return JSON.stringify(updated, null, 2);
580
- }
581
- // Summarization was attempted but failed — inject failure metadata and fall through to truncation
582
- if (result.meta) {
583
- console.error(`[Summarization] Failed, falling back to truncation. Meta:`, JSON.stringify(result.meta));
584
- }
585
- }
586
- }
587
- else {
588
- console.error(`[Summarization] Skipped: skipSummarization=${options?.skipSummarization}, hasCopilotFn=${!!options?.getCopilotClientFn}`);
589
- }
590
- // Step 2: Content truncation
591
361
  const { result, wasTruncated } = truncateContent(data);
592
362
  return JSON.stringify(result, null, 2);
593
363
  }
594
364
  /**
595
365
  * Simple `asText` replacement for backward compatibility.
596
- * Applies truncation but not summarization.
366
+ * Applies truncation only.
597
367
  */
598
368
  export function processResponseSync(data) {
599
369
  const { result } = truncateContent(data);
600
370
  return JSON.stringify(result, null, 2);
601
371
  }
602
372
  // ---------------------------------------------------------------------------
603
- // Helpers for extracting/injecting markdown from nested response objects
604
- // ---------------------------------------------------------------------------
605
- /** Extract markdown content from a scrape response, wherever it may be nested */
606
- function extractMarkdown(data) {
607
- if (!data || typeof data !== 'object')
608
- return null;
609
- const obj = data;
610
- // Direct: data.markdown
611
- if (typeof obj.markdown === 'string')
612
- return obj.markdown;
613
- // Nested: data.data.markdown
614
- if (obj.data && typeof obj.data === 'object') {
615
- const inner = obj.data;
616
- if (typeof inner.markdown === 'string')
617
- return inner.markdown;
618
- }
619
- return null;
620
- }
621
- /** Replace the markdown field in a response and merge metadata */
622
- function injectMarkdown(data, newMarkdown, meta) {
623
- const serialized = JSON.stringify(data);
624
- const cloned = JSON.parse(serialized);
625
- if (typeof cloned.markdown === 'string') {
626
- cloned.markdown = newMarkdown;
627
- Object.assign(cloned.metadata || cloned, meta);
628
- return cloned;
629
- }
630
- if (cloned.data && typeof cloned.data === 'object') {
631
- if (typeof cloned.data.markdown === 'string') {
632
- cloned.data.markdown = newMarkdown;
633
- Object.assign(cloned.data.metadata || cloned.data, meta);
634
- return cloned;
635
- }
636
- }
637
- return cloned;
638
- }
639
- // ---------------------------------------------------------------------------
640
373
  // Exports for testing
641
374
  // ---------------------------------------------------------------------------
642
- export { MAX_CONTENT_CHARS, SUMMARIZE_AFTER_WORDS, SUMMARIZE_MODEL };
643
- export { summaryCache, summarizationTimestamps };
375
+ export { MAX_CONTENT_CHARS };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scorchcrawl-mcp",
3
- "version": "2.0.0",
4
- "description": "Open-source Copilot SDK-compliant MCP server for web scraping with stealth bot-detection bypass.",
3
+ "version": "2.1.0",
4
+ "description": "Open-source MCP server for web scraping with stealth bot-detection bypass.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "scorchcrawl-mcp": "dist/index.js"
@@ -28,7 +28,6 @@
28
28
  },
29
29
  "license": "AGPL-3.0",
30
30
  "dependencies": {
31
- "@github/copilot-sdk": "^0.1.23",
32
31
  "@mendable/firecrawl-js": "^4.9.3",
33
32
  "cheerio": "^1.2.0",
34
33
  "dotenv": "^17.2.2",
@@ -45,8 +44,6 @@
45
44
  "mcp",
46
45
  "scorchcrawl",
47
46
  "web-scraping",
48
- "copilot",
49
- "copilot-sdk",
50
47
  "stealth",
51
48
  "bot-detection-bypass",
52
49
  "crawler",