crawlforge-mcp-server 5.5.6 → 5.5.8
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.
|
|
3
|
+
"version": "5.5.8",
|
|
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.
|
|
108
|
+
version: "5.5.8",
|
|
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,22 @@
|
|
|
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';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How long to wait on a notification endpoint before giving up.
|
|
15
|
+
*
|
|
16
|
+
* fetch/undici has no `timeout` RequestInit option, so without an explicit
|
|
17
|
+
* signal a webhook target that accepts the connection and then never answers
|
|
18
|
+
* holds the request open forever. These sends are awaited together in
|
|
19
|
+
* `sendNotifications`, so one unresponsive endpoint stalls every notification
|
|
20
|
+
* for that change, not just its own. `WebhookDispatcher` hit exactly this and
|
|
21
|
+
* documents it; 30s matches the default it settled on.
|
|
22
|
+
*/
|
|
23
|
+
const NOTIFY_TIMEOUT_MS = Number(process.env.TRACK_CHANGES_NOTIFY_TIMEOUT_MS) || 30000;
|
|
8
24
|
|
|
9
25
|
/**
|
|
10
26
|
* Send all enabled notifications for a detected change.
|
|
@@ -41,14 +57,15 @@ export async function sendWebhookNotification(url, changeResult, webhookConfig,
|
|
|
41
57
|
details: webhookConfig.includeContent ? changeResult.details : undefined
|
|
42
58
|
};
|
|
43
59
|
|
|
44
|
-
const response = await
|
|
60
|
+
const response = await safeFetch(webhookConfig.url, {
|
|
45
61
|
method: webhookConfig.method || 'POST',
|
|
46
62
|
headers: {
|
|
47
63
|
'Content-Type': 'application/json',
|
|
48
64
|
...identityHeaders({ role: 'webhook' }),
|
|
49
65
|
...webhookConfig.headers
|
|
50
66
|
},
|
|
51
|
-
body: JSON.stringify(payload)
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
signal: AbortSignal.timeout(NOTIFY_TIMEOUT_MS)
|
|
52
69
|
});
|
|
53
70
|
|
|
54
71
|
if (!response.ok) {
|
|
@@ -90,10 +107,11 @@ export async function sendSlackNotification(url, changeResult, slackConfig, emit
|
|
|
90
107
|
username: slackConfig.username || 'Change Tracker'
|
|
91
108
|
};
|
|
92
109
|
|
|
93
|
-
const response = await
|
|
110
|
+
const response = await safeFetch(slackConfig.webhookUrl, {
|
|
94
111
|
method: 'POST',
|
|
95
112
|
headers: { 'Content-Type': 'application/json' },
|
|
96
|
-
body: JSON.stringify(payload)
|
|
113
|
+
body: JSON.stringify(payload),
|
|
114
|
+
signal: AbortSignal.timeout(NOTIFY_TIMEOUT_MS)
|
|
97
115
|
});
|
|
98
116
|
|
|
99
117
|
if (!response.ok) {
|
|
@@ -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
|
+
}
|