crawlforge-mcp-server 3.0.10 → 3.0.12
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 +1 -1
- package/server.js +15 -2
- package/setup.js +60 -23
- package/src/core/AuthManager.js +18 -8
- package/src/tools/search/searchWeb.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-mcp-server",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.12",
|
|
4
4
|
"description": "CrawlForge MCP Server - Professional Model Context Protocol server with 19 comprehensive web scraping, crawling, and content processing tools.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
package/server.js
CHANGED
|
@@ -8,20 +8,33 @@ import dotenv from 'dotenv';
|
|
|
8
8
|
// Load .env file early to check for creator secret
|
|
9
9
|
dotenv.config({ path: '.env', quiet: true });
|
|
10
10
|
|
|
11
|
+
// SECURITY: Clear any externally-set creator mode env var to prevent bypass
|
|
12
|
+
delete process.env.CRAWLFORGE_CREATOR_MODE;
|
|
13
|
+
|
|
11
14
|
const CREATOR_SECRET_HASH = 'cfef62e5068d48e7dd6a39c9e16f0be2615510c6b68274fc8abe3156feb5050b';
|
|
12
15
|
|
|
16
|
+
// Module-scoped flag - cannot be set externally
|
|
17
|
+
let _creatorModeVerified = false;
|
|
18
|
+
|
|
13
19
|
if (process.env.CRAWLFORGE_CREATOR_SECRET) {
|
|
14
20
|
const providedHash = crypto
|
|
15
21
|
.createHash('sha256')
|
|
16
22
|
.update(process.env.CRAWLFORGE_CREATOR_SECRET)
|
|
17
23
|
.digest('hex');
|
|
18
24
|
|
|
19
|
-
if (providedHash
|
|
20
|
-
|
|
25
|
+
if (crypto.timingSafeEqual(Buffer.from(providedHash, 'hex'), Buffer.from(CREATOR_SECRET_HASH, 'hex'))) {
|
|
26
|
+
_creatorModeVerified = true;
|
|
21
27
|
console.log('🔓 Creator Mode Enabled - Unlimited Access');
|
|
22
28
|
} else {
|
|
23
29
|
console.warn('⚠️ Invalid creator secret provided');
|
|
24
30
|
}
|
|
31
|
+
// Clean up the secret from environment
|
|
32
|
+
delete process.env.CRAWLFORGE_CREATOR_SECRET;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Export getter for AuthManager to use
|
|
36
|
+
export function isCreatorModeVerified() {
|
|
37
|
+
return _creatorModeVerified;
|
|
25
38
|
}
|
|
26
39
|
|
|
27
40
|
// Now import everything else
|
package/setup.js
CHANGED
|
@@ -15,9 +15,10 @@ import AuthManager from './src/core/AuthManager.js';
|
|
|
15
15
|
* Add CrawlForge to an MCP client configuration file
|
|
16
16
|
* @param {string} configPath - Path to the config file
|
|
17
17
|
* @param {string} clientName - Name of the client (for messages)
|
|
18
|
+
* @param {string} apiKey - The CrawlForge API key to include in env
|
|
18
19
|
* @returns {object} Result with success status and message
|
|
19
20
|
*/
|
|
20
|
-
function addToMcpConfig(configPath, clientName) {
|
|
21
|
+
function addToMcpConfig(configPath, clientName, apiKey) {
|
|
21
22
|
// Check if config exists
|
|
22
23
|
if (!fs.existsSync(configPath)) {
|
|
23
24
|
return {
|
|
@@ -37,8 +38,9 @@ function addToMcpConfig(configPath, clientName) {
|
|
|
37
38
|
config.mcpServers = {};
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
// Check if crawlforge is already configured
|
|
41
|
-
|
|
41
|
+
// Check if crawlforge is already configured with the correct API key
|
|
42
|
+
const existingConfig = config.mcpServers.crawlforge;
|
|
43
|
+
if (existingConfig && existingConfig.env?.CRAWLFORGE_API_KEY === apiKey) {
|
|
42
44
|
return {
|
|
43
45
|
success: true,
|
|
44
46
|
message: `CrawlForge already configured in ${clientName}`,
|
|
@@ -46,12 +48,14 @@ function addToMcpConfig(configPath, clientName) {
|
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
// Add crawlforge MCP server configuration
|
|
51
|
+
// Add or update crawlforge MCP server configuration with API key
|
|
50
52
|
config.mcpServers.crawlforge = {
|
|
51
53
|
type: "stdio",
|
|
52
54
|
command: "crawlforge",
|
|
53
55
|
args: [],
|
|
54
|
-
env: {
|
|
56
|
+
env: {
|
|
57
|
+
CRAWLFORGE_API_KEY: apiKey
|
|
58
|
+
}
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
// Write updated config
|
|
@@ -59,7 +63,9 @@ function addToMcpConfig(configPath, clientName) {
|
|
|
59
63
|
|
|
60
64
|
return {
|
|
61
65
|
success: true,
|
|
62
|
-
message:
|
|
66
|
+
message: existingConfig
|
|
67
|
+
? `CrawlForge API key updated in ${clientName} MCP config`
|
|
68
|
+
: `CrawlForge added to ${clientName} MCP config`
|
|
63
69
|
};
|
|
64
70
|
} catch (error) {
|
|
65
71
|
return {
|
|
@@ -71,9 +77,10 @@ function addToMcpConfig(configPath, clientName) {
|
|
|
71
77
|
|
|
72
78
|
/**
|
|
73
79
|
* Configure all detected MCP clients
|
|
80
|
+
* @param {string} apiKey - The CrawlForge API key to include in env
|
|
74
81
|
* @returns {object} Results for each client
|
|
75
82
|
*/
|
|
76
|
-
function configureMcpClients() {
|
|
83
|
+
function configureMcpClients(apiKey) {
|
|
77
84
|
const results = {
|
|
78
85
|
claudeCode: null,
|
|
79
86
|
cursor: null
|
|
@@ -81,7 +88,7 @@ function configureMcpClients() {
|
|
|
81
88
|
|
|
82
89
|
// Claude Code config path
|
|
83
90
|
const claudeConfigPath = path.join(os.homedir(), '.claude.json');
|
|
84
|
-
results.claudeCode = addToMcpConfig(claudeConfigPath, 'Claude Code');
|
|
91
|
+
results.claudeCode = addToMcpConfig(claudeConfigPath, 'Claude Code', apiKey);
|
|
85
92
|
|
|
86
93
|
// Cursor config path (macOS)
|
|
87
94
|
const cursorConfigPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
@@ -95,20 +102,47 @@ function configureMcpClients() {
|
|
|
95
102
|
// Ignore creation errors
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
|
-
results.cursor = addToMcpConfig(cursorConfigPath, 'Cursor');
|
|
105
|
+
results.cursor = addToMcpConfig(cursorConfigPath, 'Cursor', apiKey);
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
return results;
|
|
102
109
|
}
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
let rl = null;
|
|
112
|
+
|
|
113
|
+
function getReadline() {
|
|
114
|
+
if (!rl) {
|
|
115
|
+
rl = readline.createInterface({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
output: process.stdout
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return rl;
|
|
121
|
+
}
|
|
108
122
|
|
|
109
|
-
const question = (query) => new Promise((resolve) =>
|
|
123
|
+
const question = (query) => new Promise((resolve) => getReadline().question(query, resolve));
|
|
124
|
+
|
|
125
|
+
function closeReadline() {
|
|
126
|
+
if (rl) {
|
|
127
|
+
rl.close();
|
|
128
|
+
rl = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
110
131
|
|
|
111
132
|
async function main() {
|
|
133
|
+
// Check if running interactively
|
|
134
|
+
const isInteractive = process.stdin.isTTY;
|
|
135
|
+
|
|
136
|
+
if (!isInteractive) {
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log('❌ Setup requires an interactive terminal.');
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log('Please run this command directly in your terminal:');
|
|
141
|
+
console.log(' npx crawlforge-setup');
|
|
142
|
+
console.log('');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
112
146
|
console.log('');
|
|
113
147
|
console.log('╔═══════════════════════════════════════════════════════╗');
|
|
114
148
|
console.log('║ CrawlForge MCP Server Setup Wizard ║');
|
|
@@ -138,7 +172,7 @@ async function main() {
|
|
|
138
172
|
|
|
139
173
|
if (overwrite.toLowerCase() !== 'y') {
|
|
140
174
|
console.log('Setup cancelled.');
|
|
141
|
-
|
|
175
|
+
closeReadline();
|
|
142
176
|
process.exit(0);
|
|
143
177
|
}
|
|
144
178
|
console.log('');
|
|
@@ -151,7 +185,7 @@ async function main() {
|
|
|
151
185
|
console.log('');
|
|
152
186
|
console.log('❌ API key is required');
|
|
153
187
|
console.log('Get your free API key at: https://www.crawlforge.dev/signup');
|
|
154
|
-
|
|
188
|
+
closeReadline();
|
|
155
189
|
process.exit(1);
|
|
156
190
|
}
|
|
157
191
|
|
|
@@ -168,9 +202,9 @@ async function main() {
|
|
|
168
202
|
console.log('🎉 Setup complete! You can now use CrawlForge MCP Server.');
|
|
169
203
|
console.log('');
|
|
170
204
|
|
|
171
|
-
// Auto-configure MCP clients (Claude Code, Cursor)
|
|
205
|
+
// Auto-configure MCP clients (Claude Code, Cursor) with API key
|
|
172
206
|
console.log('🔧 Configuring MCP client integrations...');
|
|
173
|
-
const clientResults = configureMcpClients();
|
|
207
|
+
const clientResults = configureMcpClients(apiKey.trim());
|
|
174
208
|
let anyConfigured = false;
|
|
175
209
|
let needsRestart = false;
|
|
176
210
|
|
|
@@ -219,7 +253,10 @@ async function main() {
|
|
|
219
253
|
console.log(' "mcpServers": {');
|
|
220
254
|
console.log(' "crawlforge": {');
|
|
221
255
|
console.log(' "type": "stdio",');
|
|
222
|
-
console.log(' "command": "crawlforge"');
|
|
256
|
+
console.log(' "command": "crawlforge",');
|
|
257
|
+
console.log(' "env": {');
|
|
258
|
+
console.log(` "CRAWLFORGE_API_KEY": "${apiKey.trim()}"`);
|
|
259
|
+
console.log(' }');
|
|
223
260
|
console.log(' }');
|
|
224
261
|
console.log(' }');
|
|
225
262
|
console.log(' }');
|
|
@@ -242,11 +279,11 @@ async function main() {
|
|
|
242
279
|
console.log(' • Documentation: https://www.crawlforge.dev/docs');
|
|
243
280
|
console.log(' • Support: support@crawlforge.dev');
|
|
244
281
|
console.log('');
|
|
245
|
-
|
|
282
|
+
closeReadline();
|
|
246
283
|
process.exit(1);
|
|
247
284
|
}
|
|
248
285
|
|
|
249
|
-
|
|
286
|
+
closeReadline();
|
|
250
287
|
}
|
|
251
288
|
|
|
252
289
|
// Handle errors
|
|
@@ -254,13 +291,13 @@ process.on('unhandledRejection', (error) => {
|
|
|
254
291
|
console.error('');
|
|
255
292
|
console.error('❌ Setup error:', error.message);
|
|
256
293
|
console.error('');
|
|
257
|
-
|
|
294
|
+
closeReadline();
|
|
258
295
|
process.exit(1);
|
|
259
296
|
});
|
|
260
297
|
|
|
261
298
|
// Run setup
|
|
262
299
|
main().catch((error) => {
|
|
263
300
|
console.error('❌ Fatal error:', error);
|
|
264
|
-
|
|
301
|
+
closeReadline();
|
|
265
302
|
process.exit(1);
|
|
266
303
|
});
|
package/src/core/AuthManager.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Using native fetch (Node.js 18+)
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import { isCreatorModeVerified } from '../../server.js';
|
|
9
10
|
|
|
10
11
|
class AuthManager {
|
|
11
12
|
constructor() {
|
|
@@ -21,10 +22,10 @@ class AuthManager {
|
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Check if running in creator mode (unlimited access, no API required)
|
|
24
|
-
*
|
|
25
|
+
* Uses module-scoped verified flag from server.js - cannot be bypassed via env vars
|
|
25
26
|
*/
|
|
26
27
|
isCreatorMode() {
|
|
27
|
-
return
|
|
28
|
+
return isCreatorModeVerified();
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -83,8 +84,8 @@ class AuthManager {
|
|
|
83
84
|
const configDir = path.dirname(this.configPath);
|
|
84
85
|
await fs.mkdir(configDir, { recursive: true });
|
|
85
86
|
|
|
86
|
-
// Save config
|
|
87
|
-
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2));
|
|
87
|
+
// Save config with owner-only permissions (contains API key)
|
|
88
|
+
await fs.writeFile(this.configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
88
89
|
this.config = config;
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -183,7 +184,8 @@ class AuthManager {
|
|
|
183
184
|
const response = await fetch(`${this.apiEndpoint}/api/v1/credits`, {
|
|
184
185
|
headers: {
|
|
185
186
|
'X-API-Key': this.config.apiKey
|
|
186
|
-
}
|
|
187
|
+
},
|
|
188
|
+
signal: AbortSignal.timeout(5000)
|
|
187
189
|
});
|
|
188
190
|
|
|
189
191
|
if (response.ok) {
|
|
@@ -193,11 +195,19 @@ class AuthManager {
|
|
|
193
195
|
return data.creditsRemaining >= estimatedCredits;
|
|
194
196
|
}
|
|
195
197
|
} catch (error) {
|
|
196
|
-
// If can't check, allow operation but log error
|
|
197
198
|
console.error('Failed to check credits:', error.message);
|
|
198
|
-
}
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
// Grace period: allow stale cached credits during transient network failures
|
|
201
|
+
// This prevents outages from blocking authenticated users while still
|
|
202
|
+
// failing closed when there's no cached data (no free usage bypass)
|
|
203
|
+
const cached = this.creditCache.get(this.config.userId);
|
|
204
|
+
if (cached !== undefined && cached >= estimatedCredits) {
|
|
205
|
+
console.warn('Using cached credits due to network error — will re-verify on next call');
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
throw new Error('Unable to verify credits. Please check your connection and try again.');
|
|
210
|
+
}
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
/**
|
|
@@ -5,6 +5,7 @@ import { QueryExpander } from './queryExpander.js';
|
|
|
5
5
|
import { ResultRanker } from './ranking/ResultRanker.js';
|
|
6
6
|
import { ResultDeduplicator } from './ranking/ResultDeduplicator.js';
|
|
7
7
|
import LocalizationManager from '../../core/LocalizationManager.js';
|
|
8
|
+
import { isCreatorModeVerified } from '../../../server.js';
|
|
8
9
|
|
|
9
10
|
const SearchWebSchema = z.object({
|
|
10
11
|
query: z.string().min(1),
|
|
@@ -73,7 +74,7 @@ export class SearchWebTool {
|
|
|
73
74
|
} = options;
|
|
74
75
|
|
|
75
76
|
// Check for Creator Mode - allows search without API key for development/testing
|
|
76
|
-
const isCreatorMode =
|
|
77
|
+
const isCreatorMode = isCreatorModeVerified();
|
|
77
78
|
|
|
78
79
|
if (!apiKey && !isCreatorMode) {
|
|
79
80
|
throw new Error('CrawlForge API key is required for search functionality');
|