firecrawl-mcp
Version:
MCP server for FireCrawl web scraping integration. Supports both cloud and self-hosted instances. Features include web scraping, batch processing, structured data extraction, and LLM-powered content analysis.
226 lines (225 loc) • 8.35 kB
JavaScript
import FirecrawlApp from '@mendable/firecrawl-js';
import { describe, expect, jest, test, beforeEach, afterEach, } from '@jest/globals';
import { mock } from 'jest-mock-extended';
// Mock FirecrawlApp
jest.mock('@mendable/firecrawl-js');
describe('FireCrawl Tool Tests', () => {
let mockClient;
let requestHandler;
beforeEach(() => {
jest.clearAllMocks();
mockClient = mock();
// Set up mock implementations
const mockInstance = new FirecrawlApp({ apiKey: 'test' });
Object.assign(mockInstance, mockClient);
// Create request handler
requestHandler = async (request) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error('No arguments provided');
}
return handleRequest(name, args, mockClient);
};
});
afterEach(() => {
jest.clearAllMocks();
});
// Test scrape functionality
test('should handle scrape request', async () => {
const url = 'https://example.com';
const options = { formats: ['markdown'] };
const mockResponse = {
success: true,
markdown: '# Test Content',
html: undefined,
rawHtml: undefined,
url: 'https://example.com',
actions: undefined,
};
mockClient.scrapeUrl.mockResolvedValueOnce(mockResponse);
const response = await requestHandler({
method: 'call_tool',
params: {
name: 'firecrawl_scrape',
arguments: { url, ...options },
},
});
expect(response).toEqual({
content: [{ type: 'text', text: '# Test Content' }],
isError: false,
});
expect(mockClient.scrapeUrl).toHaveBeenCalledWith(url, {
formats: ['markdown'],
url,
});
});
// Test batch scrape functionality
test('should handle batch scrape request', async () => {
const urls = ['https://example.com'];
const options = { formats: ['markdown'] };
mockClient.asyncBatchScrapeUrls.mockResolvedValueOnce({
success: true,
id: 'test-batch-id',
});
const response = await requestHandler({
method: 'call_tool',
params: {
name: 'firecrawl_batch_scrape',
arguments: { urls, options },
},
});
expect(response.content[0].text).toContain('Batch operation queued with ID: batch_');
expect(mockClient.asyncBatchScrapeUrls).toHaveBeenCalledWith(urls, options);
});
// Test search functionality
test('should handle search request', async () => {
const query = 'test query';
const scrapeOptions = { formats: ['markdown'] };
const mockSearchResponse = {
success: true,
data: [
{
url: 'https://example.com',
title: 'Test Page',
description: 'Test Description',
markdown: '# Test Content',
actions: undefined,
},
],
};
mockClient.search.mockResolvedValueOnce(mockSearchResponse);
const response = await requestHandler({
method: 'call_tool',
params: {
name: 'firecrawl_search',
arguments: { query, scrapeOptions },
},
});
expect(response.isError).toBe(false);
expect(response.content[0].text).toContain('Test Page');
expect(mockClient.search).toHaveBeenCalledWith(query, scrapeOptions);
});
// Test crawl functionality
test('should handle crawl request', async () => {
const url = 'https://example.com';
const options = { maxDepth: 2 };
mockClient.asyncCrawlUrl.mockResolvedValueOnce({
success: true,
id: 'test-crawl-id',
});
const response = await requestHandler({
method: 'call_tool',
params: {
name: 'firecrawl_crawl',
arguments: { url, ...options },
},
});
expect(response.isError).toBe(false);
expect(response.content[0].text).toContain('test-crawl-id');
expect(mockClient.asyncCrawlUrl).toHaveBeenCalledWith(url, {
maxDepth: 2,
url,
});
});
// Test error handling
test('should handle API errors', async () => {
const url = 'https://example.com';
mockClient.scrapeUrl.mockRejectedValueOnce(new Error('API Error'));
const response = await requestHandler({
method: 'call_tool',
params: {
name: 'firecrawl_scrape',
arguments: { url },
},
});
expect(response.isError).toBe(true);
expect(response.content[0].text).toContain('API Error');
});
// Test rate limiting
test('should handle rate limits', async () => {
const url = 'https://example.com';
// Mock rate limit error
mockClient.scrapeUrl.mockRejectedValueOnce(new Error('rate limit exceeded'));
const response = await requestHandler({
method: 'call_tool',
params: {
name: 'firecrawl_scrape',
arguments: { url },
},
});
expect(response.isError).toBe(true);
expect(response.content[0].text).toContain('rate limit exceeded');
});
});
// Helper function to simulate request handling
async function handleRequest(name, args, client) {
try {
switch (name) {
case 'firecrawl_scrape': {
const response = await client.scrapeUrl(args.url, args);
if (!response.success) {
throw new Error(response.error || 'Scraping failed');
}
return {
content: [
{ type: 'text', text: response.markdown || 'No content available' },
],
isError: false,
};
}
case 'firecrawl_batch_scrape': {
const response = await client.asyncBatchScrapeUrls(args.urls, args.options);
return {
content: [
{
type: 'text',
text: `Batch operation queued with ID: batch_1. Use firecrawl_check_batch_status to check progress.`,
},
],
isError: false,
};
}
case 'firecrawl_search': {
const response = await client.search(args.query, args.scrapeOptions);
if (!response.success) {
throw new Error(response.error || 'Search failed');
}
const results = response.data
.map((result) => `URL: ${result.url}\nTitle: ${result.title || 'No title'}\nDescription: ${result.description || 'No description'}\n${result.markdown ? `\nContent:\n${result.markdown}` : ''}`)
.join('\n\n');
return {
content: [{ type: 'text', text: results }],
isError: false,
};
}
case 'firecrawl_crawl': {
const response = await client.asyncCrawlUrl(args.url, args);
if (!response.success) {
throw new Error(response.error);
}
return {
content: [
{
type: 'text',
text: `Started crawl for ${args.url} with job ID: ${response.id}`,
},
],
isError: false,
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
return {
content: [
{
type: 'text',
text: error instanceof Error ? error.message : String(error),
},
],
isError: true,
};
}
}