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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crawlforge-mcp-server",
3
- "version": "3.0.10",
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 === CREATOR_SECRET_HASH) {
20
- process.env.CRAWLFORGE_CREATOR_MODE = 'true';
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
- if (config.mcpServers.crawlforge) {
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: `CrawlForge added to ${clientName} MCP config`
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
- const rl = readline.createInterface({
105
- input: process.stdin,
106
- output: process.stdout
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) => rl.question(query, 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
- rl.close();
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
- rl.close();
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
- rl.close();
282
+ closeReadline();
246
283
  process.exit(1);
247
284
  }
248
285
 
249
- rl.close();
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
- rl.close();
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
- rl.close();
301
+ closeReadline();
265
302
  process.exit(1);
266
303
  });
@@ -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
- * Reads from environment variable dynamically to ensure proper initialization order
25
+ * Uses module-scoped verified flag from server.js - cannot be bypassed via env vars
25
26
  */
26
27
  isCreatorMode() {
27
- return process.env.CRAWLFORGE_CREATOR_MODE === 'true';
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
- return true; // Allow operation if can't verify
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 = process.env.CRAWLFORGE_CREATOR_MODE === 'true';
77
+ const isCreatorMode = isCreatorModeVerified();
77
78
 
78
79
  if (!apiKey && !isCreatorMode) {
79
80
  throw new Error('CrawlForge API key is required for search functionality');