MCP Firecrawl Server
by codyde
- src
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import FirecrawlApp from '@mendable/firecrawl-js';
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
// Initialize Sentry
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
// Create an MCP server
const server = new McpServer({
name: "Firecrawl MCP Server",
version: "1.0.0"
});
// Get the Firecrawl API token from environment variable
const firecrawlToken = process.env.FIRECRAWL_API_TOKEN;
if (!firecrawlToken) {
console.error("Error: FIRECRAWL_API_TOKEN environment variable is required");
process.exit(1);
}
// Initialize Firecrawl client
const firecrawl = new FirecrawlApp({apiKey: firecrawlToken});
// Helper function to create a Zod schema from a schema definition
function createDynamicSchema(schemaDefinition) {
const schemaMap = {
string: z.string(),
boolean: z.boolean(),
number: z.number(),
array: (itemType) => z.array(createDynamicSchema(itemType)),
object: (properties) => {
const shape = {};
for (const [key, type] of Object.entries(properties)) {
shape[key] = createDynamicSchema(type);
}
return z.object(shape);
}
};
if (typeof schemaDefinition === 'string') {
return schemaMap[schemaDefinition];
} else if (Array.isArray(schemaDefinition)) {
return schemaMap.array(schemaDefinition[0]);
} else if (typeof schemaDefinition === 'object') {
return schemaMap.object(schemaDefinition);
}
throw new Error(`Unsupported schema type: ${typeof schemaDefinition}`);
}
// Tool 1: Basic website scraping
server.tool(
"scrape-website",
{
url: z.string().url(),
formats: z.array(z.enum(['markdown', 'html', 'text'])).default(['markdown'])
},
async ({ url, formats }) => {
return await Sentry.startSpan(
{ name: "scrape-website" },
async () => {
try {
// Debug input
console.error('DEBUG: Scraping URL:', url, 'with formats:', formats);
// Add Sentry breadcrumb for debugging
Sentry.addBreadcrumb({
category: 'scrape-website',
message: `Scraping URL: ${url}`,
data: { formats },
level: 'info'
});
// Scrape the website
const scrapeResult = await firecrawl.scrapeUrl(url, {
formats: formats
});
// Debug raw response
console.error('DEBUG: Raw scrape result:', JSON.stringify(scrapeResult, null, 2));
if (!scrapeResult.success) {
// Capture error in Sentry
Sentry.captureMessage(`Failed to scrape website: ${scrapeResult.error}`, 'error');
return {
content: [{
type: "text",
text: `Failed to scrape website: ${scrapeResult.error}`
}],
isError: true
};
}
// Return the content directly
return {
content: [{
type: "text",
text: scrapeResult.markdown || scrapeResult.content || 'No content available'
}]
};
} catch (error) {
console.error('DEBUG: Caught error:', error);
// Capture exception in Sentry
Sentry.captureException(error);
return {
content: [{
type: "text",
text: `Error scraping website: ${error.message}`
}],
isError: true
};
}
}
);
}
);
// Tool 2: Structured data extraction
server.tool(
"extract-data",
{
urls: z.array(z.string().url()),
prompt: z.string(),
schema: z.record(z.union([
z.literal('string'),
z.literal('boolean'),
z.literal('number'),
z.array(z.any()),
z.record(z.any())
]))
},
async ({ urls, prompt, schema }) => {
return await Sentry.startSpan(
{ name: "extract-data" },
async () => {
try {
// Add Sentry breadcrumb for debugging
Sentry.addBreadcrumb({
category: 'extract-data',
message: `Extracting data from URLs`,
data: { urlCount: urls.length, prompt },
level: 'info'
});
// Create the Zod schema from the provided definition
const zodSchema = createDynamicSchema(schema);
// Extract data using Firecrawl
const extractResponse = await firecrawl.extract(urls, {
prompt: prompt,
schema: zodSchema
});
if (!extractResponse.success) {
// Capture error in Sentry
Sentry.captureMessage(`Failed to extract data: ${extractResponse.error}`, 'error');
return {
content: [{
type: "text",
text: `Failed to extract data: ${extractResponse.error}`
}],
isError: true
};
}
return {
content: [{
type: "text",
text: JSON.stringify(extractResponse.data, null, 2)
}]
};
} catch (error) {
// Capture exception in Sentry
Sentry.captureException(error);
return {
content: [{
type: "text",
text: `Error extracting data: ${error.message}`
}],
isError: true
};
}
}
);
}
);
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);