Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,52 @@ env SSE_LOCAL=true FIRECRAWL_API_KEY=fc-YOUR_API_KEY npx -y firecrawl-mcp

Use the url: http://localhost:3000/sse

### Running with HTTP Streamable Mode

To run the server using HTTP Streamable transport:

```bash
env HTTP_STREAMABLE_SERVER=true FIRECRAWL_API_KEY=fc-YOUR_API_KEY npx -y firecrawl-mcp
```

This will start the server on port 8080. Clients can connect to the MCP server using the endpoint:
```
POST http://localhost:8080/{apiKey}/mcp
```

For MCP client integration, use the following configuration:

```javascript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

// Create an MCP client
const client = new Client();

// Configure HTTP Streamable transport
const transport = new StreamableHTTPClientTransport({
url: `http://localhost:8080/your-api-key/mcp`,
});

// Connect to the server
await client.connect(transport);

// List available tools
const { tools } = await client.listTools();
console.log('Available tools:', tools.map(t => t.name));

// Call tool example
const result = await client.callTool({
name: 'firecrawl_scrape',
arguments: {
url: 'https://example.com',
formats: ['markdown']
}
});

console.log('Scrape result:', result);
```

### Installing via Smithery (Legacy)

To install Firecrawl for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@mendableai/mcp-server-firecrawl):
Expand Down Expand Up @@ -182,13 +228,17 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace

#### Optional Configuration

##### Retry Configuration

- `FIRECRAWL_RETRY_MAX_ATTEMPTS`: Maximum number of retry attempts (default: 3)
- `FIRECRAWL_RETRY_INITIAL_DELAY`: Initial delay in milliseconds before first retry (default: 1000)
- `FIRECRAWL_RETRY_MAX_DELAY`: Maximum delay in milliseconds between retries (default: 10000)
- `FIRECRAWL_RETRY_BACKOFF_FACTOR`: Exponential backoff multiplier (default: 2)

##### Server Mode Configuration

- `SSE_LOCAL`: Set to "true" to use Server-Sent Events (SSE) transport (default: false)
- `HTTP_STREAMABLE_SERVER`: Set to "true" to use HTTP Streamable transport (default: false)
- `CLOUD_SERVICE`: Set to "true" for cloud mode operation (default: false)

##### Credit Usage Monitoring

- `FIRECRAWL_CREDIT_WARNING_THRESHOLD`: Credit usage warning threshold (default: 1000)
Expand Down Expand Up @@ -227,6 +277,18 @@ export FIRECRAWL_RETRY_MAX_ATTEMPTS=10
export FIRECRAWL_RETRY_INITIAL_DELAY=500 # Start with faster retries
```

For HTTP Streamable server mode:

```bash
# Run in HTTP Streamable mode
export HTTP_STREAMABLE_SERVER=true
export FIRECRAWL_API_KEY=your-api-key
export PORT=8080 # Optional: customize port (default: 8080)

# Start the server
npx -y firecrawl-mcp
```

### Usage with Claude Desktop

Add this to your `claude_desktop_config.json`:
Expand Down Expand Up @@ -586,4 +648,4 @@ Thanks to MCP.so and Klavis AI for hosting and [@gstarwd](https://github.com/gst

## License

MIT License - see LICENSE file for details
MIT License - see LICENSE file for details
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"license": "MIT",
"dependencies": {
"@mendable/firecrawl-js": "^1.19.0",
"@modelcontextprotocol/sdk": "^1.4.1",
"@modelcontextprotocol/sdk": "^1.11.1",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"shx": "^0.3.4",
Expand Down
19 changes: 10 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 103 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Tool,
CallToolRequestSchema,
ListToolsRequestSchema,
isInitializeRequest,
} from '@modelcontextprotocol/sdk/types.js';
import FirecrawlApp, {
type ScrapeParams,
Expand All @@ -17,6 +18,8 @@ import FirecrawlApp, {

import express, { Request, Response } from 'express';
import dotenv from 'dotenv';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { randomUUID } from 'node:crypto';

dotenv.config();

Expand Down Expand Up @@ -768,7 +771,7 @@ async function withRetry<T>(
if (isRateLimit && attempt < CONFIG.retry.maxAttempts) {
const delayMs = Math.min(
CONFIG.retry.initialDelay *
Math.pow(CONFIG.retry.backoffFactor, attempt - 1),
Math.pow(CONFIG.retry.backoffFactor, attempt - 1),
CONFIG.retry.maxDelay
);

Expand Down Expand Up @@ -973,9 +976,8 @@ Status: ${response.status}
Progress: ${response.completed}/${response.total}
Credits Used: ${response.creditsUsed}
Expires At: ${response.expiresAt}
${
response.data.length > 0 ? '\nResults:\n' + formatResults(response.data) : ''
}`;
${response.data.length > 0 ? '\nResults:\n' + formatResults(response.data) : ''
}`;
return {
content: [{ type: 'text', text: trimResponseText(status) }],
isError: false,
Expand Down Expand Up @@ -1255,9 +1257,8 @@ ${result.markdown ? `\nContent:\n${result.markdown}` : ''}`
} catch (error) {
// Log detailed error information
safeLog('error', {
message: `Request failed: ${
error instanceof Error ? error.message : String(error)
}`,
message: `Request failed: ${error instanceof Error ? error.message : String(error)
}`,
tool: request.params.name,
arguments: request.params.arguments,
timestamp: new Date().toISOString(),
Expand Down Expand Up @@ -1361,6 +1362,96 @@ async function runSSELocalServer() {
}
}


async function runHTTPStreamableServer() {
const app = express();
app.use(express.json());

const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

// A single endpoint handles all MCP requests.
app.all('/:apiKey/mcp', async (req: Request, res: Response) => {

try {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {

transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => {
const id = randomUUID();
return id;
},
onsessioninitialized: (sid: string) => {
transports[sid] = transport;
}
});

transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
delete transports[sid];
}
};
console.log('Creating server instance');
console.log('Connecting transport to server');
await server.connect(transport);

await transport.handleRequest(req, res, req.body);
return;
} else {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Invalid or missing session ID',
},
id: null,
});
return;
}

await transport.handleRequest(req, res, req.body);
} catch (error) {
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});

const PORT = 8080;
const appServer = app.listen(PORT, () => {
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
});

process.on('SIGINT', async () => {
console.log('Shutting down server...');
for (const sessionId in transports) {
try {
console.log(`Closing transport for session ${sessionId}`);
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
appServer.close(() => {
console.log('Server shutdown complete');
process.exit(0);
});
});
}

async function runSSECloudServer() {
const transports: { [sessionId: string]: SSEServerTransport } = {};
const app = express();
Expand Down Expand Up @@ -1435,6 +1526,11 @@ if (process.env.CLOUD_SERVICE === 'true') {
console.error('Fatal error running server:', error);
process.exit(1);
});
} else if (process.env.HTTP_STREAMABLE_SERVER === 'true') {
runHTTPStreamableServer().catch((error: any) => {
console.error('Fatal error running server:', error);
process.exit(1);
});
} else {
runLocalServer().catch((error: any) => {
console.error('Fatal error running server:', error);
Expand Down