@ariangibson/firecrawl-lite-mcp-server 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -0
  3. package/dist/index.js +890 -0
  4. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ariangibson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # Firecrawl Lite MCP Server
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ A **privacy-first, standalone** MCP server that provides web scraping and data extraction tools using local browser automation and your own LLM API key. **No external dependencies or API keys required** - completely decoupled from Firecrawl's cloud service.
6
+
7
+ ## � **What Makes Firecrawl Lite Special**
8
+
9
+ ### **🔒 Privacy-First Architecture**
10
+ - **Local Processing** - All web scraping and data extraction happens on your machine
11
+ - **Your Data Stays Local** - Content is processed locally, not sent to third parties
12
+ - **No External Service Lock-in** - Doesn't require Firecrawl's cloud API
13
+ - **Complete Control** - You own your data and infrastructure
14
+
15
+ ### **💰 Cost-Effective & Transparent**
16
+ - **Pay Only for LLM Usage** - No additional subscription or API fees
17
+ - **Your LLM Provider** - Compatible with OpenAI, xAI, Anthropic, Ollama, etc.
18
+ - **Predictable Costs** - Transparent pricing based on your chosen LLM rates
19
+
20
+ ### **⚡ Performance & Simplicity**
21
+ - **Lightning-Fast Startup** - Lightweight design means quick initialization
22
+ - **Single Container** - Simple deployment with Docker support
23
+ - **Minimal Resource Usage** - Optimized for efficiency and low memory footprint
24
+
25
+ ## 📊 **Feature Comparison**
26
+
27
+ | Feature | Firecrawl Lite ✅ | Original Firecrawl ❌ |
28
+ |---------|-------------------|----------------------|
29
+ | **🏠 Deployment** | **Standalone/Local** | Cloud Service |
30
+ | **🔑 API Keys Required** | **Your LLM key only** | Firecrawl API + LLM keys |
31
+ | **🔒 Data Privacy** | **100% local processing** | Cloud processing |
32
+ | **💰 Cost Model** | **LLM usage only** | Subscription + LLM costs |
33
+ | **⚙️ Setup Complexity** | **Single container** | Multi-service deployment |
34
+ | **📦 Bundle Size** | **~50MB lightweight** | Heavy multi-service |
35
+ | **🏠 Local LLM Support** | **✅ Ollama/Local LLMs** | Limited local options |
36
+ | **🎛️ Customization** | **Full control** | Limited customization |
37
+ | **🚀 Startup Time** | **< 5 seconds** | Variable (cloud dependent) |
38
+ | **🔧 Maintenance** | **Self-managed** | Managed service |
39
+
40
+ ## �️ **Available Tools**
41
+
42
+ This standalone version provides local web scraping and data extraction using Puppeteer and your own LLM:
43
+
44
+ ### ✅ **`scrape_page`** - Extract content from a single webpage
45
+ - **Implementation**: Local browser automation with Puppeteer
46
+ - **Use case**: Get webpage content for LLMs to read
47
+ - **Parameters**: `url`, `onlyMainContent`
48
+ - **Privacy**: All data processed locally
49
+
50
+ ### ✅ **`batch_scrape`** - Scrape multiple URLs in a single request
51
+ - **Implementation**: Sequential local scraping with rate limiting
52
+ - **Use case**: Process multiple pages efficiently
53
+ - **Parameters**: `urls[]`, `onlyMainContent`
54
+ - **Privacy**: All data processed locally
55
+
56
+ ### ✅ **`extract_data`** - Extract structured data using LLM
57
+ - **Implementation**: Local scraping + your LLM for data extraction
58
+ - **Use case**: Pull specific data from pages using natural language prompts
59
+ - **Parameters**: `urls[]`, `prompt`, `enableWebSearch`
60
+ - **Privacy**: Content scraped locally, sent to your LLM only
61
+
62
+ ### ✅ **`extract_with_schema`** - Extract data using JSON schema
63
+ - **Implementation**: Local scraping + schema-guided LLM extraction
64
+ - **Use case**: Extract structured data with predefined schema
65
+ - **Parameters**: `urls[]`, `schema`, `prompt`, `enableWebSearch`
66
+ - **Privacy**: Content scraped locally, sent to your LLM only
67
+
68
+ ## 🚀 **Quick Start**
69
+
70
+ ### **1. Install the package:**
71
+ ```bash
72
+ npm install -g @ariangibson/firecrawl-lite-mcp-server
73
+ ```
74
+
75
+ Or use npx to run without global installation (recommended).
76
+
77
+ ### **2. Configure your LLM:**
78
+ ```bash
79
+ # Create a .env file or set environment variables
80
+ LLM_API_KEY=your_llm_api_key_here
81
+ LLM_PROVIDER_BASE_URL=https://api.x.ai/v1
82
+ LLM_MODEL=grok-code-fast-1
83
+ ```
84
+
85
+ ### **3. Configure your MCP client:**
86
+
87
+ #### **Claude Desktop**
88
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
89
+ ```json
90
+ {
91
+ "mcpServers": {
92
+ "firecrawl-lite": {
93
+ "command": "npx",
94
+ "args": ["-y", "@ariangibson/firecrawl-lite-mcp-server"],
95
+ "env": {
96
+ "LLM_API_KEY": "your_llm_api_key_here",
97
+ "LLM_PROVIDER_BASE_URL": "https://api.x.ai/v1",
98
+ "LLM_MODEL": "grok-code-fast-1"
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ #### **Claude Code (CLI)**
106
+ ```bash
107
+ claude config mcp add firecrawl-lite \
108
+ --command "npx" \
109
+ --args "-y" --args "@ariangibson/firecrawl-lite-mcp-server" \
110
+ --env LLM_API_KEY=your_llm_api_key_here \
111
+ --env LLM_PROVIDER_BASE_URL=https://api.x.ai/v1 \
112
+ --env LLM_MODEL=grok-code-fast-1
113
+ ```
114
+
115
+ ### **4. Restart your MCP client and start scraping!**
116
+
117
+ ## ⚙️ **Configuration Guide**
118
+
119
+ ### **Required Environment Variables**
120
+ ```bash
121
+ # Your LLM API key (xAI, OpenAI, Anthropic, etc.)
122
+ LLM_API_KEY=your_api_key_here
123
+
124
+ # LLM provider base URL
125
+ LLM_PROVIDER_BASE_URL=https://api.x.ai/v1
126
+
127
+ # LLM model name
128
+ LLM_MODEL=grok-code-fast-1
129
+ ```
130
+
131
+ ### **LLM Provider Examples**
132
+ ```bash
133
+ # xAI (Grok)
134
+ LLM_PROVIDER_BASE_URL=https://api.x.ai/v1
135
+ LLM_API_KEY=xai-your-key-here
136
+ LLM_MODEL=grok-code-fast-1
137
+
138
+ # OpenAI
139
+ LLM_PROVIDER_BASE_URL=https://api.openai.com/v1
140
+ LLM_API_KEY=sk-your-key-here
141
+ LLM_MODEL=gpt-4o-mini
142
+
143
+ # Anthropic
144
+ LLM_PROVIDER_BASE_URL=https://api.anthropic.com
145
+ LLM_API_KEY=sk-ant-your-key-here
146
+ LLM_MODEL=claude-3-haiku-20240307
147
+
148
+ # Local LLM (Ollama)
149
+ LLM_PROVIDER_BASE_URL=http://localhost:11434/v1
150
+ LLM_API_KEY=your-local-key
151
+ LLM_MODEL=llama2
152
+ ```
153
+
154
+ ### **Optional Configuration**
155
+ ```bash
156
+ # Proxy configuration (for web scraping and LLM API calls)
157
+ PROXY_SERVER_URL=http://your-proxy.com:8080
158
+ PROXY_SERVER_USERNAME=your_proxy_username
159
+ PROXY_SERVER_PASSWORD=your_proxy_password
160
+
161
+ # Scraping configuration (anti-detection and rate limiting)
162
+ SCRAPE_USER_AGENT=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
163
+ SCRAPE_VIEWPORT_WIDTH=1920
164
+ SCRAPE_VIEWPORT_HEIGHT=1080
165
+ SCRAPE_DELAY_MIN=1000
166
+ SCRAPE_DELAY_MAX=3000
167
+ ```
168
+
169
+ ## 🛡️ **Anti-Detection Features**
170
+
171
+ Firecrawl Lite includes sophisticated anti-detection measures to handle modern websites with bot protection:
172
+
173
+ ### ✅ **Built-in Anti-Detection**
174
+ - **Realistic Browser Fingerprinting**: Spoofs navigator properties, plugins, and browser APIs
175
+ - **Random Delays**: Adds human-like delays between requests (configurable)
176
+ - **Modern User Agent**: Uses up-to-date Chrome user agent strings
177
+ - **Viewport Simulation**: Sets realistic desktop viewport sizes
178
+ - **Headless Optimization**: Configured for maximum stealth in headless mode
179
+
180
+ ### ✅ **Configurable Settings**
181
+ ```bash
182
+ # Control delays (in milliseconds)
183
+ SCRAPE_DELAY_MIN=1000 # Minimum delay before navigation
184
+ SCRAPE_DELAY_MAX=3000 # Maximum delay before navigation
185
+ SCRAPE_BATCH_DELAY_MIN=2000 # Minimum delay between batch requests
186
+ SCRAPE_BATCH_DELAY_MAX=5000 # Maximum delay between batch requests
187
+ ```
188
+
189
+ ## � **Docker Deployment (Optional)**
190
+
191
+ If you prefer Docker deployment:
192
+
193
+ ```bash
194
+ # Build and run with Docker
195
+ docker-compose up --build
196
+
197
+ # Run in background
198
+ docker-compose up -d --build
199
+ ```
200
+
201
+ The server will be available at `http://localhost:3000` with a health endpoint at `http://localhost:3000/health`.
202
+
203
+ ## 📊 **Usage Examples**
204
+
205
+ ### Scrape a webpage
206
+ ```json
207
+ {
208
+ "name": "scrape_page",
209
+ "arguments": {
210
+ "url": "https://example.com"
211
+ }
212
+ }
213
+ ```
214
+
215
+ ### Batch scrape multiple URLs
216
+ ```json
217
+ {
218
+ "name": "batch_scrape",
219
+ "arguments": {
220
+ "urls": ["https://example.com", "https://example.org"],
221
+ "onlyMainContent": true
222
+ }
223
+ }
224
+ ```
225
+
226
+ ### Extract data with prompt
227
+ ```json
228
+ {
229
+ "name": "extract_data",
230
+ "arguments": {
231
+ "urls": ["https://example.com"],
232
+ "prompt": "Extract the main article title and summary"
233
+ }
234
+ }
235
+ ```
236
+
237
+ ### Extract with schema
238
+ ```json
239
+ {
240
+ "name": "extract_with_schema",
241
+ "arguments": {
242
+ "urls": ["https://example.com"],
243
+ "schema": {
244
+ "type": "object",
245
+ "properties": {
246
+ "title": {"type": "string"},
247
+ "description": {"type": "string"}
248
+ }
249
+ }
250
+ }
251
+ }
252
+ ```
253
+
254
+ ## ❓ **Important Notes**
255
+
256
+ ### **🌐 Internet Requirements**
257
+ - **Requires Internet Access** - Still needs to access target websites
258
+ - **LLM API Access** - Requires connection to your chosen LLM provider
259
+ - **No Offline Operation** - Cannot work completely offline
260
+
261
+ ### **� Intentionally Excluded Features**
262
+ By design, this lite version excludes advanced features to maintain simplicity:
263
+ - Web search functionality
264
+ - Website URL discovery/mapping
265
+ - Multi-page website crawling
266
+ - LLMs.txt file generation
267
+ - Advanced research capabilities
268
+ - Crawl job status checking
269
+
270
+ ## 🙏 **Credits & Acknowledgments**
271
+
272
+ This project is inspired by and builds upon the excellent work of the original Firecrawl projects:
273
+
274
+ ### 🔥 **[Firecrawl](https://firecrawl.com)**
275
+ The original Firecrawl project by **Mendable.ai** - a comprehensive web scraping and crawling platform with advanced features like website mapping, multi-page crawling, and deep research capabilities.
276
+
277
+ ### 🔥 **[Firecrawl MCP Server](https://github.com/firecrawl/firecrawl-mcp-server)**
278
+ The official MCP server implementation by the Firecrawl team, providing MCP integration for their cloud-based scraping service.
279
+
280
+ **We give huge thanks to the Firecrawl team for their pioneering work in web scraping and MCP integration!** 🚀
281
+
282
+ > **💡 Looking for a very generous free tier and dead-simple cloud-hosted solution?**
283
+ > Visit **[firecrawl.com](https://firecrawl.com)** and sign up for a Firecrawl account! Their cloud service offers enterprise-grade web scraping with zero setup complexity.
284
+
285
+ ## �📝 **License**
286
+
287
+ MIT License - see [LICENSE](LICENSE) for details.
package/dist/index.js ADDED
@@ -0,0 +1,890 @@
1
+ #!/usr/bin/env node
2
+ // Core MCP SDK imports
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
6
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
8
+ // Web framework and utilities
9
+ import express from 'express';
10
+ import { randomUUID } from 'node:crypto';
11
+ // External dependencies
12
+ import dotenv from 'dotenv';
13
+ import axios from 'axios';
14
+ import puppeteer from 'puppeteer';
15
+ dotenv.config();
16
+ // Constants
17
+ const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
18
+ const DEFAULT_VIEWPORT_WIDTH = 1920;
19
+ const DEFAULT_VIEWPORT_HEIGHT = 1080;
20
+ const DEFAULT_SCRAPE_DELAY_MIN = 1000;
21
+ const DEFAULT_SCRAPE_DELAY_MAX = 3000;
22
+ const DEFAULT_BATCH_DELAY_MIN = 2000;
23
+ const DEFAULT_BATCH_DELAY_MAX = 5000;
24
+ const DEFAULT_RETRY_ATTEMPTS = 3;
25
+ const DEFAULT_RETRY_INITIAL_DELAY = 1000;
26
+ const DEFAULT_RETRY_MAX_DELAY = 10000;
27
+ const DEFAULT_RETRY_BACKOFF_FACTOR = 2;
28
+ // Security constants
29
+ const MAX_URLS_PER_REQUEST = 10;
30
+ // Input validation utilities
31
+ function isValidUrl(url) {
32
+ try {
33
+ const parsedUrl = new URL(url);
34
+ // Only allow http and https protocols
35
+ return ['http:', 'https:'].includes(parsedUrl.protocol);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ function sanitizeUrl(url) {
42
+ // Remove any potentially dangerous characters
43
+ return url.trim().replace(/[<>'"]/g, '');
44
+ }
45
+ function validatePrompt(prompt) {
46
+ // Basic prompt validation - prevent extremely long prompts
47
+ return prompt.length > 0 && prompt.length < 10000;
48
+ }
49
+ const SCRAPE_TOOL = {
50
+ name: 'scrape_page',
51
+ description: 'Extract content from a single webpage',
52
+ inputSchema: {
53
+ type: 'object',
54
+ properties: {
55
+ url: { type: 'string', description: 'Webpage URL to scrape' },
56
+ onlyMainContent: {
57
+ type: 'boolean',
58
+ description: 'Extract only main content',
59
+ default: true
60
+ },
61
+ },
62
+ required: ['url'],
63
+ },
64
+ };
65
+ const BATCH_SCRAPE_TOOL = {
66
+ name: 'batch_scrape',
67
+ description: 'Scrape multiple URLs in a single request',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ urls: {
72
+ type: 'array',
73
+ items: { type: 'string' },
74
+ description: 'Array of URLs to scrape'
75
+ },
76
+ onlyMainContent: {
77
+ type: 'boolean',
78
+ description: 'Extract only main content',
79
+ default: true
80
+ },
81
+ },
82
+ required: ['urls'],
83
+ },
84
+ };
85
+ const EXTRACT_DATA_TOOL = {
86
+ name: 'extract_data',
87
+ description: 'Extract structured data from webpages using LLM',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ urls: {
92
+ type: 'array',
93
+ items: { type: 'string' },
94
+ description: 'URLs to extract data from'
95
+ },
96
+ prompt: {
97
+ type: 'string',
98
+ description: 'Instructions for what data to extract'
99
+ },
100
+ enableWebSearch: {
101
+ type: 'boolean',
102
+ description: 'Enable web search for additional context',
103
+ default: false
104
+ },
105
+ },
106
+ required: ['urls', 'prompt'],
107
+ },
108
+ };
109
+ const EXTRACT_WITH_SCHEMA_TOOL = {
110
+ name: 'extract_with_schema',
111
+ description: 'Extract structured data using a JSON schema',
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ urls: {
116
+ type: 'array',
117
+ items: { type: 'string' },
118
+ description: 'URLs to extract data from'
119
+ },
120
+ schema: {
121
+ type: 'object',
122
+ description: 'JSON schema defining the data structure to extract'
123
+ },
124
+ prompt: {
125
+ type: 'string',
126
+ description: 'Optional instructions for extraction'
127
+ },
128
+ enableWebSearch: {
129
+ type: 'boolean',
130
+ description: 'Enable web search for additional context',
131
+ default: false
132
+ },
133
+ },
134
+ required: ['urls', 'schema'],
135
+ },
136
+ };
137
+ // Lightweight tool definitions for essential Firecrawl functionality
138
+ // Local web scraping functions
139
+ async function scrapeWebpage(url, onlyMainContent = true) {
140
+ // SECURITY: Validate and sanitize URL
141
+ if (!isValidUrl(url)) {
142
+ return {
143
+ url,
144
+ title: '',
145
+ content: '',
146
+ markdown: '',
147
+ html: '',
148
+ success: false,
149
+ error: 'Invalid URL format. Only HTTP and HTTPS URLs are allowed.'
150
+ };
151
+ }
152
+ const sanitizedUrl = sanitizeUrl(url);
153
+ let browser;
154
+ try {
155
+ // Get proxy configuration
156
+ const proxyUrl = CONFIG.proxy.url;
157
+ const proxyUsername = CONFIG.proxy.username;
158
+ const proxyPassword = CONFIG.proxy.password;
159
+ // Get scraping configuration
160
+ const customUserAgent = CONFIG.scraping.userAgent;
161
+ const viewportWidth = CONFIG.scraping.viewportWidth;
162
+ const viewportHeight = CONFIG.scraping.viewportHeight;
163
+ const delayMin = CONFIG.scraping.delayMin;
164
+ const delayMax = CONFIG.scraping.delayMax;
165
+ // Build Puppeteer launch options with enhanced anti-detection
166
+ // SECURITY: Removed --disable-web-security which is a major security risk
167
+ const launchOptions = {
168
+ headless: true,
169
+ args: [
170
+ '--no-sandbox',
171
+ '--disable-setuid-sandbox',
172
+ '--disable-dev-shm-usage',
173
+ '--disable-accelerated-2d-canvas',
174
+ '--no-first-run',
175
+ '--no-zygote',
176
+ '--disable-gpu',
177
+ '--disable-features=VizDisplayCompositor',
178
+ `--user-agent=${customUserAgent}`
179
+ ]
180
+ };
181
+ // Add proxy configuration if available
182
+ if (proxyUrl) {
183
+ launchOptions.args.push(`--proxy-server=${proxyUrl}`);
184
+ // If proxy requires authentication, we'll handle it in the page setup
185
+ if (proxyUsername && proxyPassword) {
186
+ console.error(`Using authenticated proxy: ${proxyUrl}`);
187
+ }
188
+ else {
189
+ console.error(`Using proxy: ${proxyUrl}`);
190
+ }
191
+ }
192
+ browser = await puppeteer.launch(launchOptions);
193
+ const page = await browser.newPage();
194
+ // Enhanced anti-detection setup
195
+ await page.setUserAgent(customUserAgent);
196
+ // Set viewport to common desktop size
197
+ await page.setViewport({ width: viewportWidth, height: viewportHeight });
198
+ // Add common browser properties to avoid detection
199
+ await page.evaluateOnNewDocument(() => {
200
+ // Override navigator properties
201
+ Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
202
+ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
203
+ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
204
+ // Mock common browser APIs
205
+ window.chrome = { runtime: {} };
206
+ });
207
+ // Handle proxy authentication if credentials are provided
208
+ if (proxyUrl && proxyUsername && proxyPassword) {
209
+ await page.authenticate({
210
+ username: proxyUsername,
211
+ password: proxyPassword
212
+ });
213
+ }
214
+ // Add random delay before navigation
215
+ const delay = Math.floor(Math.random() * (delayMax - delayMin)) + delayMin;
216
+ await new Promise(resolve => setTimeout(resolve, delay));
217
+ // SECURITY: Use sanitized URL and add timeout
218
+ await page.goto(sanitizedUrl, {
219
+ waitUntil: 'networkidle2',
220
+ timeout: 30000
221
+ });
222
+ // Wait additional time for dynamic content
223
+ await new Promise(resolve => setTimeout(resolve, 2000));
224
+ // Extract title
225
+ const title = await page.title();
226
+ // Extract content based on preference
227
+ let content = '';
228
+ let markdown = '';
229
+ if (onlyMainContent) {
230
+ // Try to extract main content using common selectors
231
+ const mainContent = await page.evaluate(() => {
232
+ const selectors = [
233
+ 'main',
234
+ '[role="main"]',
235
+ '.content',
236
+ '.post-content',
237
+ '.entry-content',
238
+ 'article',
239
+ '.article-content',
240
+ '#content',
241
+ '.main-content'
242
+ ];
243
+ for (const selector of selectors) {
244
+ const element = document.querySelector(selector);
245
+ if (element && element.textContent && element.textContent.trim().length > 100) {
246
+ return element.textContent.trim();
247
+ }
248
+ }
249
+ // Fallback to body content
250
+ return document.body.textContent || '';
251
+ });
252
+ content = mainContent;
253
+ markdown = content;
254
+ }
255
+ else {
256
+ // Extract full page content
257
+ content = await page.evaluate(() => document.body.textContent || '');
258
+ markdown = content;
259
+ }
260
+ // Get HTML
261
+ const html = await page.content();
262
+ return {
263
+ url,
264
+ title,
265
+ content,
266
+ markdown,
267
+ html,
268
+ success: true
269
+ };
270
+ }
271
+ catch (error) {
272
+ return {
273
+ url,
274
+ title: '',
275
+ content: '',
276
+ markdown: '',
277
+ html: '',
278
+ success: false,
279
+ error: error instanceof Error ? error.message : String(error)
280
+ };
281
+ }
282
+ finally {
283
+ if (browser) {
284
+ await browser.close();
285
+ }
286
+ }
287
+ }
288
+ async function extractDataWithLLM(url, prompt, schema) {
289
+ // SECURITY: Validate inputs
290
+ if (!isValidUrl(url)) {
291
+ return {
292
+ url,
293
+ data: null,
294
+ success: false,
295
+ error: 'Invalid URL format. Only HTTP and HTTPS URLs are allowed.'
296
+ };
297
+ }
298
+ if (!validatePrompt(prompt)) {
299
+ return {
300
+ url,
301
+ data: null,
302
+ success: false,
303
+ error: 'Invalid prompt. Prompt must be between 1 and 10,000 characters.'
304
+ };
305
+ }
306
+ const sanitizedUrl = sanitizeUrl(url);
307
+ const sanitizedPrompt = prompt.trim();
308
+ try {
309
+ // First scrape the webpage
310
+ const scraped = await scrapeWebpage(url, true);
311
+ if (!scraped.success) {
312
+ return {
313
+ url,
314
+ data: null,
315
+ success: false,
316
+ error: scraped.error
317
+ };
318
+ }
319
+ // Get LLM configuration
320
+ const LLM_API_KEY = CONFIG.llm.apiKey;
321
+ const LLM_PROVIDER_BASE_URL = CONFIG.llm.providerBaseUrl;
322
+ const LLM_MODEL = CONFIG.llm.model;
323
+ if (!LLM_API_KEY || !LLM_PROVIDER_BASE_URL || !LLM_MODEL) {
324
+ return {
325
+ url,
326
+ data: null,
327
+ success: false,
328
+ error: 'LLM configuration not available'
329
+ };
330
+ }
331
+ // Prepare the extraction prompt
332
+ const extractionPrompt = `
333
+ You are a data extraction assistant. Extract information from the following webpage content based on the user's request.
334
+
335
+ Webpage URL: ${sanitizedUrl}
336
+ Webpage Title: ${scraped.title}
337
+
338
+ Content:
339
+ ${scraped.content}
340
+
341
+ ${schema ? `Extract data according to this JSON schema: ${JSON.stringify(schema, null, 2)}` : ''}
342
+
343
+ User Request: ${sanitizedPrompt}
344
+
345
+ Please provide the extracted data in JSON format. ${schema ? 'Ensure the response matches the provided schema.' : 'Structure the data logically based on the content and request.'}
346
+ `;
347
+ // Get proxy configuration for LLM API calls
348
+ const proxyUrl = CONFIG.proxy.url;
349
+ const proxyUsername = CONFIG.proxy.username;
350
+ const proxyPassword = CONFIG.proxy.password;
351
+ // Build axios configuration
352
+ const axiosConfig = {
353
+ headers: {
354
+ 'Authorization': `Bearer ${LLM_API_KEY}`,
355
+ 'Content-Type': 'application/json'
356
+ }
357
+ };
358
+ // Add proxy configuration if available
359
+ if (proxyUrl) {
360
+ const proxyConfig = {
361
+ host: proxyUrl.replace(/^https?:\/\//, '').split(':')[0],
362
+ port: parseInt(proxyUrl.split(':').pop() || '80'),
363
+ protocol: proxyUrl.startsWith('https') ? 'https' : 'http'
364
+ };
365
+ if (proxyUsername && proxyPassword) {
366
+ proxyConfig.auth = {
367
+ username: proxyUsername,
368
+ password: proxyPassword
369
+ };
370
+ }
371
+ axiosConfig.proxy = proxyConfig;
372
+ console.error(`Using proxy for LLM API: ${proxyUrl}`);
373
+ }
374
+ // Call LLM API with timeout for security
375
+ const response = await axios.post(`${LLM_PROVIDER_BASE_URL}/chat/completions`, {
376
+ model: LLM_MODEL,
377
+ messages: [
378
+ {
379
+ role: 'user',
380
+ content: extractionPrompt
381
+ }
382
+ ],
383
+ temperature: 0.1,
384
+ max_tokens: 2000
385
+ }, {
386
+ ...axiosConfig,
387
+ timeout: 60000, // 60 second timeout for security
388
+ maxContentLength: 10 * 1024 * 1024, // 10MB max response size
389
+ maxBodyLength: 10 * 1024 * 1024
390
+ });
391
+ const llmResponse = response.data.choices[0].message.content;
392
+ // Try to parse JSON from the response
393
+ try {
394
+ const extractedData = JSON.parse(llmResponse);
395
+ return {
396
+ url,
397
+ data: extractedData,
398
+ success: true
399
+ };
400
+ }
401
+ catch (parseError) {
402
+ // If JSON parsing fails, return the raw response
403
+ return {
404
+ url,
405
+ data: { raw_response: llmResponse },
406
+ success: true
407
+ };
408
+ }
409
+ }
410
+ catch (error) {
411
+ // SECURITY: Prevent information disclosure in error messages
412
+ const isAxiosError = axios.isAxiosError(error);
413
+ let safeErrorMessage = 'An error occurred while processing the request';
414
+ if (isAxiosError) {
415
+ // Only expose safe error information
416
+ if (error.response?.status === 401) {
417
+ safeErrorMessage = 'Authentication failed with LLM provider';
418
+ }
419
+ else if (error.response?.status === 429) {
420
+ safeErrorMessage = 'Rate limit exceeded with LLM provider';
421
+ }
422
+ else if (error.code === 'ECONNABORTED') {
423
+ safeErrorMessage = 'Request timeout - LLM provider took too long to respond';
424
+ }
425
+ }
426
+ return {
427
+ url: sanitizedUrl,
428
+ data: null,
429
+ success: false,
430
+ error: safeErrorMessage
431
+ };
432
+ }
433
+ }
434
+ // Type guards
435
+ function isScrapeOptions(args) {
436
+ return (typeof args === 'object' &&
437
+ args !== null &&
438
+ 'url' in args &&
439
+ typeof args.url === 'string');
440
+ }
441
+ function isBatchScrapeOptions(args) {
442
+ return (typeof args === 'object' &&
443
+ args !== null &&
444
+ 'urls' in args &&
445
+ Array.isArray(args.urls));
446
+ }
447
+ function isExtractOptions(args) {
448
+ return (typeof args === 'object' &&
449
+ args !== null &&
450
+ 'urls' in args &&
451
+ 'prompt' in args &&
452
+ Array.isArray(args.urls) &&
453
+ typeof args.prompt === 'string');
454
+ }
455
+ // Remove all complex tools - keep only essential ones above
456
+ // Server implementation
457
+ const server = new Server({
458
+ name: 'firecrawl-lite-mcp-server',
459
+ version: '1.0.0',
460
+ }, {
461
+ capabilities: {
462
+ tools: {},
463
+ },
464
+ });
465
+ // Configuration for retries and monitoring
466
+ const CONFIG = {
467
+ scraping: {
468
+ userAgent: process.env.SCRAPE_USER_AGENT || DEFAULT_USER_AGENT,
469
+ viewportWidth: Number(process.env.SCRAPE_VIEWPORT_WIDTH) || DEFAULT_VIEWPORT_WIDTH,
470
+ viewportHeight: Number(process.env.SCRAPE_VIEWPORT_HEIGHT) || DEFAULT_VIEWPORT_HEIGHT,
471
+ delayMin: Number(process.env.SCRAPE_DELAY_MIN) || DEFAULT_SCRAPE_DELAY_MIN,
472
+ delayMax: Number(process.env.SCRAPE_DELAY_MAX) || DEFAULT_SCRAPE_DELAY_MAX,
473
+ batchDelayMin: Number(process.env.SCRAPE_BATCH_DELAY_MIN) || DEFAULT_BATCH_DELAY_MIN,
474
+ batchDelayMax: Number(process.env.SCRAPE_BATCH_DELAY_MAX) || DEFAULT_BATCH_DELAY_MAX,
475
+ },
476
+ retry: {
477
+ maxAttempts: Number(process.env.FIRECRAWL_RETRY_MAX_ATTEMPTS) || DEFAULT_RETRY_ATTEMPTS,
478
+ initialDelay: Number(process.env.FIRECRAWL_RETRY_INITIAL_DELAY) || DEFAULT_RETRY_INITIAL_DELAY,
479
+ maxDelay: Number(process.env.FIRECRAWL_RETRY_MAX_DELAY) || DEFAULT_RETRY_MAX_DELAY,
480
+ backoffFactor: Number(process.env.FIRECRAWL_RETRY_BACKOFF_FACTOR) || DEFAULT_RETRY_BACKOFF_FACTOR,
481
+ },
482
+ llm: {
483
+ apiKey: process.env.LLM_API_KEY,
484
+ providerBaseUrl: process.env.LLM_PROVIDER_BASE_URL,
485
+ model: process.env.LLM_MODEL,
486
+ },
487
+ proxy: {
488
+ url: process.env.PROXY_SERVER_URL,
489
+ username: process.env.PROXY_SERVER_USERNAME,
490
+ password: process.env.PROXY_SERVER_PASSWORD,
491
+ },
492
+ };
493
+ // Get LLM configuration
494
+ const LLM_API_KEY = CONFIG.llm.apiKey;
495
+ const LLM_PROVIDER_BASE_URL = CONFIG.llm.providerBaseUrl;
496
+ const LLM_MODEL = CONFIG.llm.model;
497
+ // Add utility function for delay
498
+ function delay(ms) {
499
+ return new Promise((resolve) => setTimeout(resolve, ms));
500
+ }
501
+ let isStdioTransport = false;
502
+ function safeLog(level, data) {
503
+ try {
504
+ // Always log to stderr to avoid relying on MCP logging capability
505
+ const message = `[${level}] ${typeof data === 'object' ? JSON.stringify(data) : String(data)}`;
506
+ console.error(message);
507
+ }
508
+ catch (_) {
509
+ // ignore
510
+ }
511
+ }
512
+ // Tool handlers
513
+ server.setRequestHandler(ListToolsRequestSchema, async function listToolsRequestHandler() {
514
+ return {
515
+ tools: [
516
+ SCRAPE_TOOL,
517
+ BATCH_SCRAPE_TOOL,
518
+ EXTRACT_DATA_TOOL,
519
+ EXTRACT_WITH_SCHEMA_TOOL,
520
+ ],
521
+ };
522
+ });
523
+ server.setRequestHandler(CallToolRequestSchema, async function callToolRequestHandler(request) {
524
+ const startTime = Date.now();
525
+ try {
526
+ const { name, arguments: args } = request.params;
527
+ // Log incoming request with timestamp
528
+ safeLog('info', `[${new Date().toISOString()}] Received request for tool: ${name}`);
529
+ if (!args) {
530
+ throw new Error('No arguments provided');
531
+ }
532
+ switch (name) {
533
+ case 'scrape_page': {
534
+ if (!isScrapeOptions(args)) {
535
+ throw new Error('Invalid arguments for scrape_page');
536
+ }
537
+ // SECURITY: Validate URL before processing
538
+ if (!isValidUrl(args.url)) {
539
+ throw new Error('Invalid URL format. Only HTTP and HTTPS URLs are allowed.');
540
+ }
541
+ const result = await scrapeWebpage(args.url, args.onlyMainContent !== false);
542
+ return {
543
+ content: [{ type: 'text', text: result.success ? result.markdown : `Error: ${result.error}` }],
544
+ isError: !result.success,
545
+ };
546
+ }
547
+ case 'batch_scrape': {
548
+ if (!isBatchScrapeOptions(args)) {
549
+ throw new Error('Invalid arguments for batch_scrape: urls array required');
550
+ }
551
+ // SECURITY: Validate all URLs before processing
552
+ const invalidUrls = args.urls.filter(url => !isValidUrl(url));
553
+ if (invalidUrls.length > 0) {
554
+ throw new Error(`Invalid URL format detected: ${invalidUrls.join(', ')}. Only HTTP and HTTPS URLs are allowed.`);
555
+ }
556
+ // SECURITY: Limit batch size to prevent abuse
557
+ if (args.urls.length > 10) {
558
+ throw new Error('Batch size limited to 10 URLs maximum for security and performance reasons.');
559
+ }
560
+ const results = [];
561
+ for (const url of args.urls) {
562
+ try {
563
+ const result = await scrapeWebpage(url, args.onlyMainContent !== false);
564
+ results.push({
565
+ url,
566
+ success: result.success,
567
+ title: result.title,
568
+ content: result.success ? result.markdown : `Error: ${result.error}`
569
+ });
570
+ }
571
+ catch (error) {
572
+ results.push({
573
+ url,
574
+ success: false,
575
+ error: error instanceof Error ? error.message : String(error)
576
+ });
577
+ }
578
+ // Add random delay between requests to avoid rate limiting
579
+ const batchDelay = Math.floor(Math.random() * (CONFIG.scraping.batchDelayMax - CONFIG.scraping.batchDelayMin)) + CONFIG.scraping.batchDelayMin;
580
+ await delay(batchDelay);
581
+ }
582
+ return {
583
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
584
+ isError: false,
585
+ };
586
+ }
587
+ case 'extract_data': {
588
+ if (!isExtractOptions(args)) {
589
+ throw new Error('Invalid arguments for extract_data: urls array and prompt required');
590
+ }
591
+ // SECURITY: Validate all URLs
592
+ const invalidUrls = args.urls.filter(url => !isValidUrl(url));
593
+ if (invalidUrls.length > 0) {
594
+ throw new Error(`Invalid URL format detected: ${invalidUrls.join(', ')}. Only HTTP and HTTPS URLs are allowed.`);
595
+ }
596
+ // SECURITY: Validate prompt
597
+ if (!validatePrompt(args.prompt)) {
598
+ throw new Error('Invalid prompt. Prompt must be between 1 and 10,000 characters.');
599
+ }
600
+ // SECURITY: Limit batch size
601
+ if (args.urls.length > 5) {
602
+ throw new Error('Extraction limited to 5 URLs maximum for security and performance reasons.');
603
+ }
604
+ const results = [];
605
+ for (const url of args.urls) {
606
+ try {
607
+ const result = await extractDataWithLLM(url, args.prompt);
608
+ results.push({
609
+ url,
610
+ success: result.success,
611
+ data: result.success ? result.data : `Error: ${result.error}`
612
+ });
613
+ }
614
+ catch (error) {
615
+ results.push({
616
+ url,
617
+ success: false,
618
+ error: error instanceof Error ? error.message : String(error)
619
+ });
620
+ }
621
+ // Add delay between requests
622
+ await delay(1000);
623
+ }
624
+ return {
625
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
626
+ isError: false,
627
+ };
628
+ }
629
+ case 'extract_with_schema': {
630
+ if (!isExtractOptions(args) || !args.schema) {
631
+ throw new Error('Invalid arguments for extract_with_schema: urls array, schema, and prompt required');
632
+ }
633
+ // Security validations
634
+ if (!Array.isArray(args.urls) || args.urls.length === 0) {
635
+ throw new Error('Invalid arguments: urls must be a non-empty array');
636
+ }
637
+ if (args.urls.length > MAX_URLS_PER_REQUEST) {
638
+ throw new Error(`Too many URLs: maximum ${MAX_URLS_PER_REQUEST} URLs allowed per request`);
639
+ }
640
+ // Validate and sanitize each URL
641
+ const sanitizedUrls = [];
642
+ for (const url of args.urls) {
643
+ if (typeof url !== 'string') {
644
+ throw new Error('Invalid URL format: all URLs must be strings');
645
+ }
646
+ if (!isValidUrl(url)) {
647
+ throw new Error(`Invalid URL: ${url}`);
648
+ }
649
+ const sanitizedUrl = sanitizeUrl(url);
650
+ if (!sanitizedUrl) {
651
+ throw new Error(`Failed to sanitize URL: ${url}`);
652
+ }
653
+ sanitizedUrls.push(sanitizedUrl);
654
+ }
655
+ // Validate and sanitize prompt
656
+ if (typeof args.prompt !== 'string') {
657
+ throw new Error('Invalid prompt: must be a string');
658
+ }
659
+ if (!validatePrompt(args.prompt)) {
660
+ throw new Error('Invalid prompt: must be between 1 and 10,000 characters');
661
+ }
662
+ const sanitizedPrompt = args.prompt.trim();
663
+ // Validate schema (basic validation)
664
+ if (typeof args.schema !== 'object' || args.schema === null) {
665
+ throw new Error('Invalid schema: must be a valid object');
666
+ }
667
+ const results = [];
668
+ for (const url of sanitizedUrls) {
669
+ try {
670
+ const result = await extractDataWithLLM(url, sanitizedPrompt, args.schema);
671
+ results.push({
672
+ url,
673
+ success: result.success,
674
+ data: result.success ? result.data : `Error: ${result.error}`
675
+ });
676
+ }
677
+ catch (error) {
678
+ // Prevent information disclosure in error messages
679
+ const safeErrorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
680
+ results.push({
681
+ url,
682
+ success: false,
683
+ error: safeErrorMessage.replace(/[^\w\s\-.:]/g, '') // Remove potentially sensitive characters
684
+ });
685
+ }
686
+ // Add delay between requests to prevent rate limiting
687
+ await delay(1000);
688
+ }
689
+ return {
690
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
691
+ isError: false,
692
+ };
693
+ }
694
+ default:
695
+ return {
696
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
697
+ isError: true,
698
+ };
699
+ }
700
+ }
701
+ catch (error) {
702
+ // Log detailed error information
703
+ safeLog('error', {
704
+ message: `Request failed: ${error instanceof Error ? error.message : String(error)}`,
705
+ tool: request.params.name,
706
+ arguments: request.params.arguments,
707
+ timestamp: new Date().toISOString(),
708
+ duration: Date.now() - startTime,
709
+ });
710
+ return {
711
+ content: [
712
+ {
713
+ type: 'text',
714
+ text: trimResponseText(`Error: ${error instanceof Error ? error.message : String(error)}`),
715
+ },
716
+ ],
717
+ isError: true,
718
+ };
719
+ }
720
+ finally {
721
+ // Log request completion with performance metrics
722
+ safeLog('info', `Request completed in ${Date.now() - startTime}ms`);
723
+ }
724
+ });
725
+ // Helper function to format results
726
+ function formatResults(data) {
727
+ return data
728
+ .map((doc) => {
729
+ const content = doc.markdown || doc.content || 'No content';
730
+ return `Title: ${doc.title}
731
+ Content: ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`;
732
+ })
733
+ .join('\n\n');
734
+ }
735
+ // Utility function to trim trailing whitespace from text responses
736
+ // This prevents Claude API errors with "final assistant content cannot end with trailing whitespace"
737
+ function trimResponseText(text) {
738
+ return text.trim();
739
+ }
740
+ // Server startup
741
+ async function runLocalServer() {
742
+ try {
743
+ console.error('Initializing Firecrawl Lite MCP Server...');
744
+ const transport = new StdioServerTransport();
745
+ // Detect if we're using stdio transport
746
+ isStdioTransport = transport instanceof StdioServerTransport;
747
+ if (isStdioTransport) {
748
+ console.error('Running in stdio mode, logging will be directed to stderr');
749
+ }
750
+ await server.connect(transport);
751
+ // Now that we're connected, we can send logging messages
752
+ safeLog('info', 'Firecrawl Lite MCP Server initialized successfully');
753
+ safeLog('info', `LLM Configuration: ${LLM_PROVIDER_BASE_URL ? 'Configured' : 'Not configured'} (${LLM_MODEL || 'no model'})`);
754
+ console.error('Firecrawl Lite MCP Server running on stdio');
755
+ }
756
+ catch (error) {
757
+ console.error('Fatal error running server:', error);
758
+ process.exit(1);
759
+ }
760
+ }
761
+ async function runSSELocalServer() {
762
+ let transport = null;
763
+ const app = express();
764
+ app.get('/sse', async (req, res) => {
765
+ transport = new SSEServerTransport(`/messages`, res);
766
+ res.on('close', () => {
767
+ transport = null;
768
+ });
769
+ await server.connect(transport);
770
+ });
771
+ // Endpoint for the client to POST messages
772
+ // Remove express.json() middleware - let the transport handle the body
773
+ app.post('/messages', (req, res) => {
774
+ if (transport) {
775
+ transport.handlePostMessage(req, res);
776
+ }
777
+ });
778
+ const PORT = process.env.PORT || 3000;
779
+ console.log('Starting server on port', PORT);
780
+ try {
781
+ app.listen(PORT, () => {
782
+ console.log(`MCP SSE Server listening on http://localhost:${PORT}`);
783
+ console.log(`SSE endpoint: http://localhost:${PORT}/sse`);
784
+ console.log(`Message endpoint: http://localhost:${PORT}/messages`);
785
+ });
786
+ }
787
+ catch (error) {
788
+ console.error('Error starting server:', error);
789
+ }
790
+ }
791
+ async function runHTTPStreamableServer() {
792
+ const app = express();
793
+ app.use(express.json());
794
+ // Health check endpoint
795
+ app.get('/health', (req, res) => {
796
+ res.status(200).json({
797
+ status: 'OK',
798
+ server: 'Firecrawl Lite MCP Server',
799
+ version: '1.0.0',
800
+ timestamp: new Date().toISOString()
801
+ });
802
+ });
803
+ const transports = {};
804
+ // A single endpoint handles all MCP requests.
805
+ app.all('/mcp', async (req, res) => {
806
+ try {
807
+ const sessionId = req.headers['mcp-session-id'];
808
+ let transport;
809
+ if (sessionId && transports[sessionId]) {
810
+ transport = transports[sessionId];
811
+ }
812
+ else if (!sessionId &&
813
+ req.method === 'POST' &&
814
+ req.body &&
815
+ typeof req.body === 'object' &&
816
+ req.body.method === 'initialize') {
817
+ transport = new StreamableHTTPServerTransport({
818
+ sessionIdGenerator: () => {
819
+ const id = randomUUID();
820
+ return id;
821
+ },
822
+ onsessioninitialized: (sid) => {
823
+ transports[sid] = transport;
824
+ },
825
+ });
826
+ transport.onclose = () => {
827
+ const sid = transport.sessionId;
828
+ if (sid && transports[sid]) {
829
+ delete transports[sid];
830
+ }
831
+ };
832
+ console.log('Creating server instance');
833
+ console.log('Connecting transport to server');
834
+ await server.connect(transport);
835
+ await transport.handleRequest(req, res, req.body);
836
+ return;
837
+ }
838
+ else {
839
+ res.status(400).json({
840
+ jsonrpc: '2.0',
841
+ error: {
842
+ code: -32000,
843
+ message: 'Invalid or missing session ID',
844
+ },
845
+ id: null,
846
+ });
847
+ return;
848
+ }
849
+ await transport.handleRequest(req, res, req.body);
850
+ }
851
+ catch (error) {
852
+ if (!res.headersSent) {
853
+ res.status(500).json({
854
+ jsonrpc: '2.0',
855
+ error: {
856
+ code: -32603,
857
+ message: 'Internal server error',
858
+ },
859
+ id: null,
860
+ });
861
+ }
862
+ }
863
+ });
864
+ const PORT = 3000;
865
+ const appServer = app.listen(PORT, () => {
866
+ console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
867
+ });
868
+ process.on('SIGINT', async () => {
869
+ console.log('Shutting down server...');
870
+ for (const sessionId in transports) {
871
+ try {
872
+ console.log(`Closing transport for session ${sessionId}`);
873
+ await transports[sessionId].close();
874
+ delete transports[sessionId];
875
+ }
876
+ catch (error) {
877
+ console.error(`Error closing transport for session ${sessionId}:`, error);
878
+ }
879
+ }
880
+ appServer.close(() => {
881
+ console.log('Server shutdown complete');
882
+ process.exit(0);
883
+ });
884
+ });
885
+ }
886
+ // Server startup - standalone MCP server
887
+ runLocalServer().catch((error) => {
888
+ console.error('Fatal error running server:', error);
889
+ process.exit(1);
890
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@ariangibson/firecrawl-lite-mcp-server",
3
+ "version": "1.1.1",
4
+ "description": "Privacy-first, standalone MCP server for web scraping and data extraction using local browser automation and your own LLM API key",
5
+ "type": "module",
6
+ "bin": {
7
+ "firecrawl-lite-mcp-server": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "start": "node dist/index.js",
15
+ "dev": "tsc && node dist/index.js",
16
+ "lint": "tsc --noEmit"
17
+ },
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.17.3",
21
+ "@types/cheerio": "^0.22.35",
22
+ "axios": "^1.11.0",
23
+ "cheerio": "^1.1.2",
24
+ "dotenv": "^16.4.7",
25
+ "express": "^5.1.0",
26
+ "puppeteer": "^24.17.1",
27
+ "ws": "^8.18.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/express": "^5.0.1",
31
+ "@types/node": "^20.10.5",
32
+ "typescript": "^5.9.2"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/ariangibson/firecrawl-lite-mcp-server.git"
40
+ },
41
+ "homepage": "https://github.com/ariangibson/firecrawl-lite-mcp-server#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/ariangibson/firecrawl-lite-mcp-server/issues"
44
+ }
45
+ }