crawlforge-mcp-server 4.2.3 → 4.2.5
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/CLAUDE.md +7 -4
- package/README.md +5 -3
- package/package.json +4 -2
- package/server.js +3 -2
- package/setup.js +17 -7
- package/src/cli/commands/analyze.js +26 -1
- package/src/cli/commands/localize.js +45 -10
- package/src/cli/commands/monitor.js +2 -1
- package/src/cli/commands/template.js +8 -3
- package/src/cli/index.js +49 -0
- package/src/cli/lib/runTool.js +11 -2
- package/src/core/ActionExecutor.js +2 -1
- package/src/core/AuthManager.js +3 -2
- package/src/core/PerformanceManager.js +3 -0
- package/src/core/creatorMode.js +2 -1
- package/src/tools/advanced/batchScrape/index.js +2 -1
- package/src/tools/search/adapters/searchProviderFactory.js +2 -1
- package/src/utils/Logger.js +3 -0
package/CLAUDE.md
CHANGED
|
@@ -62,7 +62,7 @@ These guidelines are working if: fewer unnecessary changes in diffs, fewer rewri
|
|
|
62
62
|
|
|
63
63
|
CrawlForge MCP Server - A professional MCP (Model Context Protocol) server providing 23 web scraping, crawling, and content processing tools (5 inline + 18 advanced).
|
|
64
64
|
|
|
65
|
-
**Current Version:** 4.2.
|
|
65
|
+
**Current Version:** 4.2.4
|
|
66
66
|
|
|
67
67
|
## Development Commands
|
|
68
68
|
|
|
@@ -92,8 +92,11 @@ npm run dev
|
|
|
92
92
|
# Test MCP protocol compliance
|
|
93
93
|
npm test
|
|
94
94
|
|
|
95
|
-
# Unit tests (
|
|
95
|
+
# Unit tests (262 tests, no live network)
|
|
96
96
|
npm run test:unit
|
|
97
|
+
# Note: add --test-force-exit if the run appears to hang at the end — importing
|
|
98
|
+
# StealthBrowserManager (d2-reliability.test.js) leaves a Playwright handle that
|
|
99
|
+
# otherwise delays process exit ~100s. Tests themselves pass either way.
|
|
97
100
|
|
|
98
101
|
# Integration tests
|
|
99
102
|
npm run test:integration
|
|
@@ -120,7 +123,7 @@ npm run docker:prod # Run production container
|
|
|
120
123
|
|
|
121
124
|
### Debugging Tips
|
|
122
125
|
|
|
123
|
-
-
|
|
126
|
+
- All diagnostic output (Winston logs + status banners) goes to stderr; stdout is reserved for the MCP JSON-RPC stream and CLI `--json` output (enforced in v4.2.4)
|
|
124
127
|
- Set `NODE_ENV=development` for verbose logging
|
|
125
128
|
- Use `--expose-gc` flag for memory profiling: `node --expose-gc server.js`
|
|
126
129
|
- Check `cache/` directory for cached responses
|
|
@@ -282,4 +285,4 @@ try {
|
|
|
282
285
|
- Each sub agent must work on their strengths; when done they report to the project manager who updates `docs/PRODUCTION_READINESS.md`
|
|
283
286
|
- Whenever a phase is completed, push all changes to GitHub
|
|
284
287
|
- Put all documentation md files into the `docs/` folder
|
|
285
|
-
- Every time you finish a phase run `npm run
|
|
288
|
+
- Every time you finish a phase run the test suites (`npm run test:unit` and `npm test`) and fix all failures before pushing
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Add to `claude_desktop_config.json`:
|
|
|
48
48
|
"mcpServers": {
|
|
49
49
|
"crawlforge": {
|
|
50
50
|
"command": "npx",
|
|
51
|
-
"args": ["crawlforge-mcp-server"]
|
|
51
|
+
"args": ["-y", "crawlforge-mcp-server"]
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -71,7 +71,7 @@ The setup wizard automatically configures Claude Code by adding to `~/.claude.js
|
|
|
71
71
|
"mcpServers": {
|
|
72
72
|
"crawlforge": {
|
|
73
73
|
"type": "stdio",
|
|
74
|
-
"command": "crawlforge"
|
|
74
|
+
"command": "crawlforge-mcp"
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -89,7 +89,7 @@ The setup wizard automatically configures Cursor by adding to `~/.cursor/mcp.jso
|
|
|
89
89
|
"mcpServers": {
|
|
90
90
|
"crawlforge": {
|
|
91
91
|
"type": "stdio",
|
|
92
|
-
"command": "crawlforge"
|
|
92
|
+
"command": "crawlforge-mcp"
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -98,6 +98,8 @@ The setup wizard automatically configures Cursor by adding to `~/.cursor/mcp.jso
|
|
|
98
98
|
Restart Cursor to activate.
|
|
99
99
|
</details>
|
|
100
100
|
|
|
101
|
+
> **Which launch command?** `npx -y crawlforge-mcp-server` needs no global install and always runs the published version (recommended for Claude Desktop). For a global install (`npm i -g crawlforge-mcp-server`), use the dedicated `crawlforge-mcp` bin — it resolves on your `PATH`, so it survives Node/nvm version switches. The bare `crawlforge` command still launches the server when an MCP client spawns it over stdio (backward compatibility for configs created before v4.2.5); interactively it's the CLI — run `crawlforge mcp` to start the server by hand.
|
|
102
|
+
|
|
101
103
|
## 📊 Available Tools
|
|
102
104
|
|
|
103
105
|
### Basic Tools (1 credit each)
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-mcp-server",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.5",
|
|
4
4
|
"description": "CrawlForge MCP Server - Professional Model Context Protocol server with 23 web scraping, crawling, and content processing tools. Defaults to local Ollama for LLM extraction (no API key needed); OpenAI/Anthropic available as opt-in. v4.0 adds Markdown-first output, pre-built site templates, Camoufox stealth engine, and cost transparency.",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"crawlforge": "src/cli/index.js",
|
|
8
|
-
"crawlforge-setup": "setup.js"
|
|
8
|
+
"crawlforge-setup": "setup.js",
|
|
9
|
+
"crawlforge-mcp-server": "server.js",
|
|
10
|
+
"crawlforge-mcp": "server.js"
|
|
9
11
|
},
|
|
10
12
|
"scripts": {
|
|
11
13
|
"start": "node server.js",
|
package/server.js
CHANGED
|
@@ -57,7 +57,8 @@ if (!AuthManager.isAuthenticated() && !AuthManager.isCreatorMode()) {
|
|
|
57
57
|
const apiKey = process.env.CRAWLFORGE_API_KEY;
|
|
58
58
|
if (apiKey) {
|
|
59
59
|
// Auto-setup if API key is provided via environment
|
|
60
|
-
|
|
60
|
+
// Status → stderr; stdout is reserved for the MCP JSON-RPC stream.
|
|
61
|
+
console.error('🔧 Auto-configuring CrawlForge with provided API key...');
|
|
61
62
|
const success = await AuthManager.runSetup(apiKey);
|
|
62
63
|
if (!success) {
|
|
63
64
|
console.error('❌ Failed to authenticate with provided API key');
|
|
@@ -95,7 +96,7 @@ if (configErrors.length > 0 && config.server.nodeEnv === 'production') {
|
|
|
95
96
|
// Create the server
|
|
96
97
|
const server = new McpServer({
|
|
97
98
|
name: "crawlforge",
|
|
98
|
-
version: "4.2.
|
|
99
|
+
version: "4.2.5",
|
|
99
100
|
description: "Production-ready MCP server with 23 web scraping, crawling, and content processing tools. Features MCP Resources (crawlforge://), Prompts, Sampling fallback, Elicitation, stealth browsing, deep research, structured extraction, change tracking, and local-LLM extraction via Ollama.",
|
|
100
101
|
homepage: "https://www.crawlforge.dev",
|
|
101
102
|
icon: "https://www.crawlforge.dev/icon.png"
|
package/setup.js
CHANGED
|
@@ -38,9 +38,16 @@ function addToMcpConfig(configPath, clientName, apiKey) {
|
|
|
38
38
|
config.mcpServers = {};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Check if crawlforge is already configured with the correct API key
|
|
41
|
+
// Check if crawlforge is already configured with the correct API key AND the
|
|
42
|
+
// current launch command. The `crawlforge-mcp` bin (added in v4.2.5) is the
|
|
43
|
+
// dedicated MCP server launcher; older configs used the bare `crawlforge` bin,
|
|
44
|
+
// which became the CLI in v4.1.0 — re-running setup migrates them.
|
|
42
45
|
const existingConfig = config.mcpServers.crawlforge;
|
|
43
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
existingConfig &&
|
|
48
|
+
existingConfig.env?.CRAWLFORGE_API_KEY === apiKey &&
|
|
49
|
+
existingConfig.command === 'crawlforge-mcp'
|
|
50
|
+
) {
|
|
44
51
|
return {
|
|
45
52
|
success: true,
|
|
46
53
|
message: `CrawlForge already configured in ${clientName}`,
|
|
@@ -48,10 +55,13 @@ function addToMcpConfig(configPath, clientName, apiKey) {
|
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
// Add or update crawlforge MCP server configuration with API key
|
|
58
|
+
// Add or update crawlforge MCP server configuration with API key.
|
|
59
|
+
// `crawlforge-mcp` is the dedicated stdio MCP-server bin (PATH-resolved, so it
|
|
60
|
+
// survives Node/nvm version switches). The bare `crawlforge` bin also still
|
|
61
|
+
// launches the server when spawned over stdio, for backward compatibility.
|
|
52
62
|
config.mcpServers.crawlforge = {
|
|
53
63
|
type: "stdio",
|
|
54
|
-
command: "crawlforge",
|
|
64
|
+
command: "crawlforge-mcp",
|
|
55
65
|
args: [],
|
|
56
66
|
env: {
|
|
57
67
|
CRAWLFORGE_API_KEY: apiKey
|
|
@@ -253,7 +263,7 @@ async function main() {
|
|
|
253
263
|
console.log(' "mcpServers": {');
|
|
254
264
|
console.log(' "crawlforge": {');
|
|
255
265
|
console.log(' "type": "stdio",');
|
|
256
|
-
console.log(' "command": "crawlforge",');
|
|
266
|
+
console.log(' "command": "crawlforge-mcp",');
|
|
257
267
|
console.log(' "env": {');
|
|
258
268
|
console.log(` "CRAWLFORGE_API_KEY": "${apiKey.trim()}"`);
|
|
259
269
|
console.log(' }');
|
|
@@ -266,8 +276,8 @@ async function main() {
|
|
|
266
276
|
console.log('────────────────────────────────────────────────────────');
|
|
267
277
|
console.log('');
|
|
268
278
|
console.log('Quick start:');
|
|
269
|
-
console.log(' crawlforge
|
|
270
|
-
console.log('
|
|
279
|
+
console.log(' crawlforge-mcp # Start the MCP server (stdio)');
|
|
280
|
+
console.log(' crawlforge --help # Explore the CLI commands');
|
|
271
281
|
console.log('');
|
|
272
282
|
console.log('Need help? Visit: https://www.crawlforge.dev/docs');
|
|
273
283
|
console.log('');
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* analyze command — analyze content of a URL.
|
|
3
|
+
* Fetches and cleans the page content first (extract_content), then runs
|
|
4
|
+
* NLP analysis (analyze_content) on the extracted text.
|
|
3
5
|
*/
|
|
6
|
+
import { ExtractContentTool } from '../../tools/extract/extractContent.js';
|
|
4
7
|
import { AnalyzeContentTool } from '../../tools/extract/analyzeContent.js';
|
|
5
8
|
import { getToolConfig } from '../../constants/config.js';
|
|
6
9
|
import { runTool } from '../lib/runTool.js';
|
|
@@ -13,7 +16,29 @@ export function register(program) {
|
|
|
13
16
|
.action(async (url, opts, cmd) => {
|
|
14
17
|
const globals = cmd.parent.opts();
|
|
15
18
|
const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
|
|
19
|
+
|
|
20
|
+
// analyze_content operates on text, so fetch & clean the page first.
|
|
21
|
+
const extractor = new ExtractContentTool(getToolConfig('extract_content'));
|
|
22
|
+
let text;
|
|
23
|
+
try {
|
|
24
|
+
const extracted = await extractor.execute({ url });
|
|
25
|
+
text = extracted?.content?.text;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
process.stderr.write(`Error fetching content from ${url}: ${e.message}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!text || text.trim().length < 10) {
|
|
32
|
+
process.stderr.write(`Error: could not extract enough text from ${url} to analyze\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
const tool = new AnalyzeContentTool(getToolConfig('analyze_content'));
|
|
17
|
-
|
|
37
|
+
// All analyses (language, topics, entities, sentiment, readability) default to true;
|
|
38
|
+
// --depth full additionally enables advanced metrics.
|
|
39
|
+
await runTool(tool, {
|
|
40
|
+
text,
|
|
41
|
+
options: { includeAdvancedMetrics: opts.depth === 'full' }
|
|
42
|
+
}, cliFlags);
|
|
18
43
|
});
|
|
19
44
|
}
|
|
@@ -1,29 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* localize command — fetch
|
|
2
|
+
* localize command — fetch a URL with locale/geo-aware request headers.
|
|
3
|
+
* Builds a localization config (Accept-Language, User-Agent) for the target
|
|
4
|
+
* country via LocalizationManager, then fetches the URL with those headers.
|
|
3
5
|
*/
|
|
4
6
|
import { LocalizationManager } from '../../core/LocalizationManager.js';
|
|
7
|
+
import { fetchUrlHandler } from '../../tools/basic/fetchUrl.js';
|
|
5
8
|
import { getToolConfig } from '../../constants/config.js';
|
|
6
9
|
import { runTool } from '../lib/runTool.js';
|
|
7
10
|
|
|
11
|
+
// Derive a 2-letter country code from a --country flag or an en-US style locale.
|
|
12
|
+
function resolveCountry(country, locale) {
|
|
13
|
+
if (country) return country.toUpperCase();
|
|
14
|
+
if (locale && locale.includes('-')) return locale.split('-')[1].toUpperCase();
|
|
15
|
+
return 'US';
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export function register(program) {
|
|
9
19
|
program
|
|
10
20
|
.command('localize <url>')
|
|
11
|
-
.description('Fetch URL with locale/geo-aware
|
|
21
|
+
.description('Fetch URL with locale/geo-aware request headers')
|
|
12
22
|
.option('--locale <locale>', 'Locale code (e.g. en-US, fr-FR)', 'en-US')
|
|
13
23
|
.option('--country <code>', 'Country code for geo-targeting (e.g. US, FR)')
|
|
14
24
|
.option('--currency <code>', 'Currency code (e.g. USD, EUR)')
|
|
15
25
|
.action(async (url, opts, cmd) => {
|
|
16
26
|
const globals = cmd.parent.opts();
|
|
17
27
|
const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
|
|
18
|
-
|
|
28
|
+
|
|
29
|
+
const countryCode = resolveCountry(opts.country, opts.locale);
|
|
30
|
+
const language = opts.locale ? opts.locale.split('-')[0] : undefined;
|
|
31
|
+
|
|
19
32
|
const wrapperTool = {
|
|
20
|
-
execute: (
|
|
33
|
+
execute: async () => {
|
|
34
|
+
const mgr = new LocalizationManager(getToolConfig('localization'));
|
|
35
|
+
await mgr.initialize();
|
|
36
|
+
const config = await mgr.configureCountry(countryCode, {
|
|
37
|
+
language,
|
|
38
|
+
currency: opts.currency
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const headers = {
|
|
42
|
+
'Accept-Language': config.acceptLanguage,
|
|
43
|
+
'User-Agent': mgr.generateUserAgent(countryCode),
|
|
44
|
+
...(config.customHeaders || {})
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const fetched = await fetchUrlHandler({ url, headers });
|
|
48
|
+
return {
|
|
49
|
+
localization: {
|
|
50
|
+
countryCode: config.countryCode,
|
|
51
|
+
language: config.language,
|
|
52
|
+
timezone: config.timezone,
|
|
53
|
+
currency: config.currency,
|
|
54
|
+
acceptLanguage: config.acceptLanguage
|
|
55
|
+
},
|
|
56
|
+
request_headers: headers,
|
|
57
|
+
response: fetched
|
|
58
|
+
};
|
|
59
|
+
}
|
|
21
60
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
locale: opts.locale,
|
|
25
|
-
country: opts.country,
|
|
26
|
-
currency: opts.currency
|
|
27
|
-
}, cliFlags);
|
|
61
|
+
|
|
62
|
+
await runTool(wrapperTool, {}, cliFlags);
|
|
28
63
|
});
|
|
29
64
|
}
|
|
@@ -17,6 +17,7 @@ export function register(program) {
|
|
|
17
17
|
const globals = cmd.parent.opts();
|
|
18
18
|
const cliFlags = { json: globals.json, pretty: globals.pretty, quiet: globals.quiet };
|
|
19
19
|
const tool = new TrackChangesTool(getToolConfig('track_changes'));
|
|
20
|
+
// monitor runs continuously — do not auto-exit after the first result.
|
|
20
21
|
await runTool(tool, {
|
|
21
22
|
url,
|
|
22
23
|
scheduled: true,
|
|
@@ -24,6 +25,6 @@ export function register(program) {
|
|
|
24
25
|
selector: opts.selector,
|
|
25
26
|
webhook_url: opts.webhook,
|
|
26
27
|
change_threshold: parseFloat(opts.threshold)
|
|
27
|
-
}, cliFlags);
|
|
28
|
+
}, cliFlags, { exitOnSuccess: false });
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -7,7 +7,7 @@ import { runTool } from '../lib/runTool.js';
|
|
|
7
7
|
|
|
8
8
|
export function register(program) {
|
|
9
9
|
program
|
|
10
|
-
.command('template
|
|
10
|
+
.command('template [id] [target]')
|
|
11
11
|
.description('Scrape using a pre-built site template (e.g. amazon-product, github-repo)')
|
|
12
12
|
.option('--list', 'List all available templates')
|
|
13
13
|
.action(async (id, target, opts, cmd) => {
|
|
@@ -16,11 +16,16 @@ export function register(program) {
|
|
|
16
16
|
const tool = new ScrapeTemplateTool(getToolConfig('scrape_template'));
|
|
17
17
|
|
|
18
18
|
if (opts.list) {
|
|
19
|
-
const wrapperTool = { execute: () => tool.
|
|
19
|
+
const wrapperTool = { execute: () => tool.execute({ template: 'list' }) };
|
|
20
20
|
await runTool(wrapperTool, {}, cliFlags);
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
if (!id || !target) {
|
|
25
|
+
process.stderr.write('Error: template requires <id> and <target>, or use --list\n');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await runTool(tool, { template: id, url: target }, cliFlags);
|
|
25
30
|
});
|
|
26
31
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -46,6 +46,25 @@ import { register as registerMonitor } from './commands/monitor.js';
|
|
|
46
46
|
import { register as registerInstallSkills } from './commands/install-skills.js';
|
|
47
47
|
import { register as registerUninstallSkills } from './commands/uninstall-skills.js';
|
|
48
48
|
|
|
49
|
+
// ─── MCP stdio server mode (backward compatibility) ──────────────────────────
|
|
50
|
+
// Before v4.1.0 the `crawlforge` bin WAS the MCP server. v4.1.0 turned it into
|
|
51
|
+
// this CLI, which silently broke MCP clients still configured with
|
|
52
|
+
// `command: "crawlforge"` — they received CLI help text instead of a JSON-RPC
|
|
53
|
+
// stream, surfacing as a -32000 connect error. Detect that case and hand off to
|
|
54
|
+
// the MCP server so existing configs keep working with no edits:
|
|
55
|
+
// • explicit: `crawlforge mcp` / `crawlforge serve` (registered below)
|
|
56
|
+
// • explicit: CRAWLFORGE_MCP_STDIO=true
|
|
57
|
+
// • implicit: no subcommand AND stdin is not a TTY (i.e. spawned by an MCP host)
|
|
58
|
+
// Escape hatch: CRAWLFORGE_FORCE_CLI=true forces CLI help even over a pipe.
|
|
59
|
+
const __mcpImplicit =
|
|
60
|
+
process.argv.slice(2).length === 0 &&
|
|
61
|
+
!process.stdin.isTTY &&
|
|
62
|
+
process.env.CRAWLFORGE_FORCE_CLI !== 'true';
|
|
63
|
+
|
|
64
|
+
if (process.env.CRAWLFORGE_MCP_STDIO === 'true' || __mcpImplicit) {
|
|
65
|
+
await import('../../server.js');
|
|
66
|
+
} else {
|
|
67
|
+
|
|
49
68
|
const program = new Command();
|
|
50
69
|
|
|
51
70
|
program
|
|
@@ -58,11 +77,28 @@ program
|
|
|
58
77
|
.option('--api-key <key>', 'CrawlForge API key (overrides CRAWLFORGE_API_KEY env var)')
|
|
59
78
|
.option('--timeout <ms>', 'Global request timeout in milliseconds', '30000');
|
|
60
79
|
|
|
80
|
+
// Resolve the API key from (in priority order): --api-key flag, CRAWLFORGE_API_KEY env,
|
|
81
|
+
// then the stored ~/.crawlforge/config.json written by `crawlforge-setup`.
|
|
82
|
+
function loadStoredApiKey() {
|
|
83
|
+
try {
|
|
84
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
85
|
+
if (!home) return undefined;
|
|
86
|
+
const cfgPath = join(home, '.crawlforge', 'config.json');
|
|
87
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
88
|
+
return cfg.apiKey || undefined;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
61
94
|
// Apply --api-key globally before commands run
|
|
62
95
|
program.hook('preAction', (thisCommand) => {
|
|
63
96
|
const opts = program.opts();
|
|
64
97
|
if (opts.apiKey) {
|
|
65
98
|
process.env.CRAWLFORGE_API_KEY = opts.apiKey;
|
|
99
|
+
} else if (!process.env.CRAWLFORGE_API_KEY) {
|
|
100
|
+
const stored = loadStoredApiKey();
|
|
101
|
+
if (stored) process.env.CRAWLFORGE_API_KEY = stored;
|
|
66
102
|
}
|
|
67
103
|
if (opts.timeout) {
|
|
68
104
|
process.env.CRAWLFORGE_CLI_TIMEOUT = opts.timeout;
|
|
@@ -88,7 +124,20 @@ registerMonitor(program);
|
|
|
88
124
|
registerInstallSkills(program);
|
|
89
125
|
registerUninstallSkills(program);
|
|
90
126
|
|
|
127
|
+
// `crawlforge mcp` / `crawlforge serve` — explicitly start the MCP server over
|
|
128
|
+
// stdio. Extra args (e.g. --http) are read directly by server.js from argv.
|
|
129
|
+
program
|
|
130
|
+
.command('mcp')
|
|
131
|
+
.alias('serve')
|
|
132
|
+
.description('Start the MCP server over stdio (for MCP clients like Claude Code, Claude Desktop, Cursor)')
|
|
133
|
+
.allowUnknownOption(true)
|
|
134
|
+
.action(async () => {
|
|
135
|
+
await import('../../server.js');
|
|
136
|
+
});
|
|
137
|
+
|
|
91
138
|
program.parseAsync(process.argv).catch((err) => {
|
|
92
139
|
process.stderr.write(`Fatal error: ${err.message}\n`);
|
|
93
140
|
process.exit(1);
|
|
94
141
|
});
|
|
142
|
+
|
|
143
|
+
}
|
package/src/cli/lib/runTool.js
CHANGED
|
@@ -16,9 +16,13 @@ import { formatResult, formatError } from '../formatter.js';
|
|
|
16
16
|
* @param {object} cliFlags — { json, pretty, quiet }
|
|
17
17
|
* @param {object} [options]
|
|
18
18
|
* @param {boolean} [options.exitOnError=true]
|
|
19
|
+
* @param {boolean} [options.exitOnSuccess=true] Exit the process after writing
|
|
20
|
+
* output. One-shot CLI commands need this because background timers
|
|
21
|
+
* (metrics, cache/connection cleanup, etc.) otherwise keep the event loop
|
|
22
|
+
* alive. Long-running commands (e.g. `monitor`) pass false.
|
|
19
23
|
*/
|
|
20
24
|
export async function runTool(tool, params, cliFlags, options = {}) {
|
|
21
|
-
const { exitOnError = true } = options;
|
|
25
|
+
const { exitOnError = true, exitOnSuccess = true } = options;
|
|
22
26
|
|
|
23
27
|
try {
|
|
24
28
|
const result = await tool.execute(params);
|
|
@@ -32,7 +36,12 @@ export async function runTool(tool, params, cliFlags, options = {}) {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const output = formatResult(result, cliFlags);
|
|
35
|
-
if (output)
|
|
39
|
+
if (output) {
|
|
40
|
+
// Wait for stdout to flush (pipes/files buffer) before exiting.
|
|
41
|
+
process.stdout.write(output + '\n', () => { if (exitOnSuccess) process.exit(0); });
|
|
42
|
+
} else if (exitOnSuccess) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
36
45
|
} catch (error) {
|
|
37
46
|
process.stderr.write(formatError(error, cliFlags) + '\n');
|
|
38
47
|
if (exitOnError) process.exit(1);
|
|
@@ -926,7 +926,8 @@ export class ActionExecutor extends EventEmitter {
|
|
|
926
926
|
*/
|
|
927
927
|
log(level, message) {
|
|
928
928
|
if (this.enableLogging) {
|
|
929
|
-
|
|
929
|
+
// → stderr so stdout stays clean for MCP JSON-RPC / CLI --json output.
|
|
930
|
+
console.error('[ActionExecutor:' + level.toUpperCase() + '] ' + message);
|
|
930
931
|
}
|
|
931
932
|
}
|
|
932
933
|
|
package/src/core/AuthManager.js
CHANGED
|
@@ -69,7 +69,8 @@ class AuthManager {
|
|
|
69
69
|
|
|
70
70
|
// Skip config loading in creator mode
|
|
71
71
|
if (this.isCreatorMode()) {
|
|
72
|
-
|
|
72
|
+
// Status → stderr; stdout is reserved for MCP JSON-RPC / CLI --json output.
|
|
73
|
+
console.error('🚀 Creator Mode Active - Unlimited Access Enabled');
|
|
73
74
|
this.initialized = true;
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
@@ -78,7 +79,7 @@ class AuthManager {
|
|
|
78
79
|
await this.loadConfig();
|
|
79
80
|
this.initialized = true;
|
|
80
81
|
} catch (error) {
|
|
81
|
-
console.
|
|
82
|
+
console.error('No existing CrawlForge configuration found. Run setup to configure.');
|
|
82
83
|
this.initialized = true;
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -771,6 +771,9 @@ export class PerformanceManager extends EventEmitter {
|
|
|
771
771
|
this.metricsTimer = setInterval(() => {
|
|
772
772
|
this.collectMetrics();
|
|
773
773
|
}, this.metricsInterval);
|
|
774
|
+
// Don't let the metrics interval keep a short-lived process (e.g. a one-shot
|
|
775
|
+
// CLI command) alive. The long-running server stays up via its stdio transport.
|
|
776
|
+
if (typeof this.metricsTimer.unref === 'function') this.metricsTimer.unref();
|
|
774
777
|
}
|
|
775
778
|
|
|
776
779
|
/**
|
package/src/core/creatorMode.js
CHANGED
|
@@ -29,7 +29,8 @@ if (process.env.CRAWLFORGE_CREATOR_SECRET) {
|
|
|
29
29
|
|
|
30
30
|
if (crypto.timingSafeEqual(Buffer.from(providedHash, 'hex'), Buffer.from(CREATOR_SECRET_HASH, 'hex'))) {
|
|
31
31
|
_creatorModeVerified = true;
|
|
32
|
-
|
|
32
|
+
// Status message → stderr so stdout stays clean (MCP JSON-RPC / CLI --json output).
|
|
33
|
+
console.error('Creator Mode Enabled - Unlimited Access');
|
|
33
34
|
} else {
|
|
34
35
|
console.warn('Invalid creator secret provided');
|
|
35
36
|
}
|
|
@@ -301,7 +301,8 @@ export class BatchScrapeTool extends EventEmitter {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
_log(level, message) {
|
|
304
|
-
|
|
304
|
+
// → stderr so stdout stays clean for MCP JSON-RPC / CLI --json output.
|
|
305
|
+
if (this.enableLogging) console.error(`[BatchScrapeTool:${level.toUpperCase()}] ${message}`);
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
_initializeJobExecutors() {
|
|
@@ -36,7 +36,8 @@ export class SearchProviderFactory {
|
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Status message → stderr so stdout stays clean (MCP JSON-RPC / CLI --json).
|
|
40
|
+
console.error('🔍 Creator Mode: Using Google Search API directly');
|
|
40
41
|
return new GoogleSearchAdapter(googleApiKey, googleSearchEngineId);
|
|
41
42
|
}
|
|
42
43
|
|
package/src/utils/Logger.js
CHANGED
|
@@ -116,6 +116,9 @@ export class Logger {
|
|
|
116
116
|
|
|
117
117
|
if (enableConsole) {
|
|
118
118
|
transports.push(new winston.transports.Console({
|
|
119
|
+
// Route ALL log levels to stderr so stdout stays reserved for structured
|
|
120
|
+
// output (MCP JSON-RPC protocol and CLI --json results).
|
|
121
|
+
stderrLevels: ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'],
|
|
119
122
|
format: winston.format.combine(
|
|
120
123
|
winston.format.colorize(),
|
|
121
124
|
winston.format.simple()
|