crawlforge-mcp-server 5.5.6 → 5.5.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "5.5.6",
3
+ "version": "5.5.7",
4
4
  "mcpName": "io.github.mysleekdesigns/crawlforge-mcp-server",
5
5
  "description": "CrawlForge MCP Server - Professional Model Context Protocol server with 29 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
@@ -105,7 +105,7 @@ const taskStore = createTaskStore({ logger });
105
105
  // Create the server
106
106
  const server = new McpServer({
107
107
  name: "crawlforge",
108
- version: "5.5.6",
108
+ version: "5.5.7",
109
109
  description: "Production-ready MCP server with 29 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, embedded JavaScript state 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.",
110
110
  homepage: "https://www.crawlforge.dev",
111
111
  icon: "https://www.crawlforge.dev/icon.png",
@@ -14,6 +14,7 @@ import { htmlToMarkdown } from '../../utils/htmlToMarkdown.js'; // D3.1
14
14
  import { safeFetch } from '../../utils/ssrfGuard.js';
15
15
  import { preflightFetch } from '../../utils/robotsGate.js';
16
16
  import { noteRetryAfter } from '../../utils/hostRateLimiter.js';
17
+ import { isRemoteTransport } from '../../utils/remoteMode.js';
17
18
 
18
19
  const ProcessDocumentSchema = z.object({
19
20
  source: z.string().min(1),
@@ -148,6 +149,18 @@ export class ProcessDocumentTool {
148
149
  const validated = ProcessDocumentSchema.parse(params);
149
150
  const { source, sourceType, options, respect_robots, user_agent } = validated;
150
151
 
152
+ // `file` and `pdf_file` read a path straight off this machine's disk,
153
+ // which is the point of the tool when you are running it yourself. Served
154
+ // to a network, it is arbitrary file read on the host, so refuse it there
155
+ // rather than trusting whoever holds the key. Local use is unaffected.
156
+ if ((sourceType === 'file' || sourceType === 'pdf_file') && isRemoteTransport()) {
157
+ throw new Error(
158
+ `sourceType '${sourceType}' reads a local file and is only available when this server ` +
159
+ `runs on your own machine (stdio, or HTTP bound to loopback). Pass a URL with ` +
160
+ `sourceType 'url' or 'pdf_url' instead.`
161
+ );
162
+ }
163
+
151
164
  const result = {
152
165
  source,
153
166
  sourceType,
@@ -5,6 +5,10 @@
5
5
  */
6
6
 
7
7
  import { identityHeaders } from '../../../utils/fetchIdentity.js';
8
+ // Webhook and Slack targets are caller-supplied URLs, so they go out through the
9
+ // same guard as every other outbound request. WebhookDispatcher already did this;
10
+ // these two senders were the only ones left calling bare fetch.
11
+ import { safeFetch } from '../../../utils/ssrfGuard.js';
8
12
 
9
13
  /**
10
14
  * Send all enabled notifications for a detected change.
@@ -41,7 +45,7 @@ export async function sendWebhookNotification(url, changeResult, webhookConfig,
41
45
  details: webhookConfig.includeContent ? changeResult.details : undefined
42
46
  };
43
47
 
44
- const response = await fetch(webhookConfig.url, {
48
+ const response = await safeFetch(webhookConfig.url, {
45
49
  method: webhookConfig.method || 'POST',
46
50
  headers: {
47
51
  'Content-Type': 'application/json',
@@ -90,7 +94,7 @@ export async function sendSlackNotification(url, changeResult, slackConfig, emit
90
94
  username: slackConfig.username || 'Change Tracker'
91
95
  };
92
96
 
93
- const response = await fetch(slackConfig.webhookUrl, {
97
+ const response = await safeFetch(slackConfig.webhookUrl, {
94
98
  method: 'POST',
95
99
  headers: { 'Content-Type': 'application/json' },
96
100
  body: JSON.stringify(payload)
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Is this process serving a network that is not just the machine it runs on?
3
+ *
4
+ * The distinction matters for tools that touch the local filesystem. Over
5
+ * stdio, and over HTTP bound to loopback, the caller *is* the local user:
6
+ * reading a file they name is the feature, and no privilege boundary is
7
+ * crossed. Bound to a public interface it is a different act entirely — the
8
+ * caller is remote and the filesystem is the server's, so the same request
9
+ * reads files that were never theirs to read.
10
+ *
11
+ * The two expressions below are deliberately identical to the places that
12
+ * actually decide the transport and the bind address:
13
+ * - `server.js` (`useHttp`), and
14
+ * - `src/server/transports/streamableHttp.js` (`host` / `hostIsLoopback`).
15
+ * Keep them in step. Erring toward "remote" is the safe direction: it refuses
16
+ * a local-file read that would have been allowed, which is a visible,
17
+ * reversible inconvenience rather than a silent disclosure.
18
+ */
19
+ export function isRemoteTransport() {
20
+ const useHttp = process.argv.includes('--http') || process.env.MCP_HTTP === 'true';
21
+ if (!useHttp) return false; // stdio — the caller is the local user
22
+
23
+ const host = process.env.MCP_HTTP_HOST ?? (process.env.RENDER ? '0.0.0.0' : '127.0.0.1');
24
+ return !(host === '127.0.0.1' || host === '::1' || host === 'localhost');
25
+ }