Skip to main content
Glama

MCP Server for Crawl4AI

by omgwtfwow
index.npx.test.ts8.62 kB
import { spawn } from 'child_process'; import * as path from 'path'; import * as url from 'url'; import * as fs from 'fs/promises'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); describe('NPX Execution Tests', () => { // These tests ensure the package works when installed and run via npx // This prevents issues like the one in v2.6.11 where the server wouldn't start describe('Simulated NPX execution', () => { it('should start server when run from dist/index.js directly', async () => { // This simulates how npx runs the built package const distIndexPath = path.join(__dirname, '..', '..', 'dist', 'index.js'); // Check if dist/index.js exists (it should after build) try { await fs.access(distIndexPath); } catch { console.warn('Skipping test - dist/index.js not found. Run "npm run build" first.'); return; } const child = spawn('node', [distIndexPath], { env: { ...process.env, CRAWL4AI_BASE_URL: 'http://localhost:11235', CRAWL4AI_API_KEY: 'test-key', // Don't load .env file to simulate production NODE_ENV: 'production', }, stdio: 'pipe', }); let stderr = ''; child.stderr.on('data', (data) => { stderr += data.toString(); }); // Wait for server to start await new Promise<void>((resolve) => { const timeout = setTimeout(() => { child.kill(); resolve(); }, 2000); child.stderr.on('data', (data) => { const output = data.toString(); if (output.includes('started')) { clearTimeout(timeout); child.kill(); resolve(); } }); }); // Server should have started successfully expect(stderr).toContain('crawl4ai-mcp'); expect(stderr).toContain('started'); }); it('should start server without dotenv when env vars are provided', async () => { // This tests that we don't require dotenv in production const distIndexPath = path.join(__dirname, '..', '..', 'dist', 'index.js'); try { await fs.access(distIndexPath); } catch { console.warn('Skipping test - dist/index.js not found.'); return; } // Temporarily rename node_modules/dotenv to simulate it not being available const dotenvPath = path.join(__dirname, '..', '..', 'node_modules', 'dotenv'); const dotenvBackupPath = path.join(__dirname, '..', '..', 'node_modules', 'dotenv.backup'); let dotenvRenamed = false; try { // Only rename if dotenv exists try { await fs.access(dotenvPath); await fs.rename(dotenvPath, dotenvBackupPath); dotenvRenamed = true; } catch { // dotenv doesn't exist, which is fine for this test } const child = spawn('node', [distIndexPath], { env: { CRAWL4AI_BASE_URL: 'http://localhost:11235', CRAWL4AI_API_KEY: 'test-key', PATH: process.env.PATH, }, stdio: 'pipe', }); let stderr = ''; child.stderr.on('data', (data) => { stderr += data.toString(); }); // Wait for server to start await new Promise<void>((resolve) => { setTimeout(() => { child.kill(); resolve(); }, 2000); }); // Server should still start even without dotenv expect(stderr).toContain('crawl4ai-mcp'); expect(stderr).toContain('started'); } finally { // Restore dotenv if we renamed it if (dotenvRenamed) { await fs.rename(dotenvBackupPath, dotenvPath); } } }); it('should handle MCP protocol initialization', async () => { // This simulates the full MCP handshake that Claude Desktop does const distIndexPath = path.join(__dirname, '..', '..', 'dist', 'index.js'); try { await fs.access(distIndexPath); } catch { console.warn('Skipping test - dist/index.js not found.'); return; } const child = spawn('node', [distIndexPath], { env: { ...process.env, CRAWL4AI_BASE_URL: 'http://localhost:11235', CRAWL4AI_API_KEY: 'test-key', }, stdio: 'pipe', }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); // Wait for server to start await new Promise((resolve) => setTimeout(resolve, 500)); // Send MCP initialization request (like Claude Desktop does) const initRequest = JSON.stringify({ jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'test-client', version: '1.0.0', }, }, id: 1, }) + '\n'; child.stdin.write(initRequest); // Wait for response await new Promise((resolve) => setTimeout(resolve, 1000)); // Parse the response const response = stdout.trim().split('\n').pop(); let parsed; try { parsed = JSON.parse(response || '{}'); } catch { // Response might not be valid JSON yet parsed = {}; } child.kill(); // Should have received an initialization response expect(stderr).toContain('started'); expect(parsed.id).toBe(1); expect(parsed.result).toBeDefined(); }); it('should fail gracefully when CRAWL4AI_BASE_URL is missing', async () => { const distIndexPath = path.join(__dirname, '..', '..', 'dist', 'index.js'); try { await fs.access(distIndexPath); } catch { console.warn('Skipping test - dist/index.js not found.'); return; } const child = spawn('node', [distIndexPath], { env: { // Explicitly set to empty string to prevent dotenv from loading CRAWL4AI_BASE_URL: '', PATH: process.env.PATH, }, stdio: 'pipe', }); let stderr = ''; child.stderr.on('data', (data) => { stderr += data.toString(); }); const exitCode = await new Promise<number | null>((resolve, reject) => { // Add timeout to prevent hanging const timeout = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('Process timeout')); }, 10000); // 10 second timeout child.on('exit', (code) => { clearTimeout(timeout); resolve(code); }); child.on('error', (err) => { clearTimeout(timeout); reject(err); }); }); // Should exit with error code expect(exitCode).toBe(1); expect(stderr).toContain('CRAWL4AI_BASE_URL environment variable is required'); // Ensure cleanup child.kill(); }, 15000); // 15 second test timeout }); describe('NPX-specific edge cases', () => { it('should work with different Node.js execution paths', async () => { // NPX might use different paths for node execution const distIndexPath = path.join(__dirname, '..', '..', 'dist', 'index.js'); try { await fs.access(distIndexPath); } catch { console.warn('Skipping test - dist/index.js not found.'); return; } // Test with different argv[1] values that npx might use const testPaths = [ distIndexPath, '/tmp/npx-12345/node_modules/.bin/mcp-crawl4ai-ts', path.join(process.env.HOME || '', '.npm/_npx/12345/node_modules/mcp-crawl4ai-ts/dist/index.js'), ]; for (const testPath of testPaths) { const child = spawn('node', [distIndexPath], { env: { ...process.env, CRAWL4AI_BASE_URL: 'http://localhost:11235', // Simulate different execution contexts npm_execpath: testPath, }, stdio: 'pipe', }); let started = false; child.stderr.on('data', (data) => { if (data.toString().includes('started')) { started = true; } }); // Give it time to start await new Promise((resolve) => setTimeout(resolve, 500)); child.kill(); expect(started).toBe(true); } }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/omgwtfwow/mcp-crawl4ai-ts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server