Firecrawl MCP server — self-hostable, Vercel-compatible, Effect-TS
firecrawl-mcp
A self-hostable, Vercel-compatible MCP (Model Context Protocol) server for Firecrawl, built with strict TypeScript ES2024, Effect-TS, and the Bun runtime.
Quick Start:
- Add to Claude Desktop (or run
./scripts/setup-claude.sh) - Add to LM Studio
- Full Documentation (including developer docs in
docs/dev/)
Architecture
firecrawl-mcp/
├── api/ # Vercel serverless function entrypoints
│ ├── health.ts # GET /health — LB probe
│ └── mcp.ts # POST / — MCP JSON-RPC 2.0 handler
├── src/
│ ├── config/
│ │ └── app-config.ts # Effect Config — typed secret management
│ ├── errors/
│ │ └── mcp-errors.ts # Tagged domain errors (Data.TaggedError)
│ ├── services/
│ │ ├── firecrawl-client.ts # Effect Layer wrapping @mendable/firecrawl-js
│ │ └── logger.ts # Structured JSON logger service
│ ├── api/
│ │ ├── groups/
│ │ │ └── mcp-handler.ts # MCP JSON-RPC dispatch + web adapter
│ │ └── schemas/
│ │ └── firecrawl-schemas.ts # Effect Schema types for all tools
│ ├── tools/
│ │ ├── tool-definitions.ts # Static tool metadata (tools/list)
│ │ ├── tool-registry.ts # Handler map — name -> Effect handler
│ │ └── tool-schemas.ts # Schema re-exports (avoids circular imports)
│ ├── lib/
│ │ ├── app-layer.ts # Full Layer DAG composition
│ │ └── utils.ts # Pure utilities (sanitization, parsing)
│ └── server.ts # Local Bun dev server
├── .env.example
├── dprint.json
├── package.json
├── tsconfig.json
└── vercel.json
SOLID alignment
| Principle | Implementation |
| ------------------------- | -------------------------------------------------------------------------------------------- |
| Single Responsibility | Each file owns one concern: config, errors, client, handler, schemas |
| Open/Closed | New tools added via TOOL_DEFINITIONS + REGISTRY without touching existing handlers |
| Liskov Substitution | FirecrawlClientOps interface — cloud and self-hosted clients are interchangeable |
| Interface Segregation | FirecrawlClientOps exposes only what handlers need, not the full SDK surface |
| Dependency Inversion | All services resolved via Effect Layer / Context.Tag — no direct new calls in handlers |
Requirements
- Bun >= 1.1.0
- Firecrawl API key or a self-hosted Firecrawl instance URL
Runtime model
| Context | Runtime |
| ---------------- | ----------------------------------------------------- |
| Local dev server | Bun (native, bun run dev) |
| Type-checking | Bun invoking tsc (bun tsc --noEmit) |
| Formatting | dprint (bun run fmt) |
| Bundler | Bun (bun build --target node --format esm) |
| Vercel execution | Node.js 22 running the compiled ESM output in dist/ |
Bun is the exclusive toolchain — installer, bundler, test runner, dev server, and
type-checker host. Vercel runs the compiled output under Node.js 22. There is no
runtime conflict: the source is never handed to Node raw. The functions block in
vercel.json is intentionally absent; Vercel auto-detects api/ handlers and
serves the bundled dist/ output.
Local development
# Install dependencies
bun install
# Configure environment
cp .env.example .env.local
# Edit .env.local: set FIRECRAWL_API_KEY or FIRECRAWL_API_URL
# Start dev server with hot reload
bun run dev
# Server listens on http://localhost:3000
# Type-check (bun hosts tsc — no separate tsc binary needed)
bun run typecheck
# Format all source files
bun run fmt
# Check formatting without writing (CI)
bun run fmt:check
Sending a test request
# List available tools
curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# Scrape a page
curl -X POST http://localhost:3000 \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "firecrawl_scrape",
"arguments": {
"url": "https://example.com",
"formats": ["markdown"],
"onlyMainContent": true
}
}
}'
# Export Prometheus metrics
curl -X GET http://localhost:3000/metrics
Vercel deployment
# Install Vercel CLI
bun add -g vercel
# Deploy
vercel
# Set environment variables in Vercel dashboard or via CLI
vercel env add FIRECRAWL_API_KEY
Environment variables
| Variable | Required | Description |
| ------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| FIRECRAWL_API_KEY | One of these two | Firecrawl cloud API key |
| FIRECRAWL_API_URL | One of these two | Self-hosted instance base URL |
| CLOUD_SERVICE | No | true to require per-request API key via header |
| SAFE_MODE | No | true to disable browser execute and crawl webhooks |
| MAX_REQUEST_BODY_BYTES | No | Max request body size in bytes (default: 1048576) |
| REQUEST_TIMEOUT_MS | No | Request timeout budget in ms (default: 25000) |
| RATE_LIMIT_ENABLED | No | Enable in-memory per-IP rate limiting (default: true) |
| RATE_LIMIT_REQUESTS | No | Allowed requests per window (default: 120) |
| RATE_LIMIT_WINDOW_MS | No | Rate-limit window in ms (default: 60000) |
| PORT | No | Local server port (default: 3000) |
| LOG_LEVEL | No | debug | info | warn | error (default: info) |
| MCP_VERSION | No | MCP protocol version (default: 2025-11-25) |
| ALLOWED_ORIGINS | No | Comma-separated allowlist of permitted Origin values for CSRF protection. Omit or set to * to disable (default: disabled) |
Cloud service mode
When CLOUD_SERVICE=true, the API key is read from each incoming request rather
than from the environment:
POST / HTTP/1.1
x-firecrawl-api-key: fc-your-key-here
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/list"}
Accepted headers (in priority order):
x-firecrawl-api-keyx-api-keyAuthorization: Bearer <key>
Safe mode
When SAFE_MODE=true (automatically enabled when CLOUD_SERVICE=true):
firecrawl_scrapestripsactionsthat involve user interaction (click, write, executeJavascript, generatePDF)firecrawl_crawlstrips thewebhookoptionfirecrawl_browser_executereturns an error
Observability
- Every response includes
X-Request-Idfor end-to-end correlation. - Structured logs capture tool failures with sanitized upstream diagnostics.
GET /metricsexposes Prometheus text metrics:mcp_requests_totalmcp_request_duration_ms_totalmcp_tool_calls_totalmcp_tool_duration_ms_total
CI/CD
- CI workflow (
.github/workflows/ci.yml) runs on PRs andmainpushes:- formatting check
- typecheck
- tests
- smoke checks
- Dependency policy workflow (
.github/workflows/dependency-policy.yml) enforces exact pinned versions and rejects committedbun.lock. - Security workflow (
.github/workflows/security.yml) runsbun auditand CodeQL analysis. - Dependabot config (
.github/dependabot.yml) manages Bun and GitHub Actions updates weekly. - CD workflow (
.github/workflows/deploy.yml) deploys to Vercel after CI succeeds onmain.
Required repository secrets for deployment:
VERCEL_TOKENVERCEL_ORG_IDVERCEL_PROJECT_ID
Adding a new tool
- Add a
ToolDefinitionentry tosrc/tools/tool-definitions.ts - Add a Schema to
src/api/schemas/firecrawl-schemas.ts - Re-export the Schema in
src/tools/tool-schemas.ts - Add a handler function and register it in
src/tools/tool-registry.ts
No other files need modification.
Effect Layer DAG
AppConfigLive (Config env)
|
+-- AppLoggerLive
|
+-- FirecrawlClientLive
|
+-- AppLive (merged)
|
+-- ManagedRuntime (api/mcp.ts)