Files
webfetch-mcp/server.mjs
T
Jay e1f6fff3fe feat: Implement smart time-based rate limiting v0.1.8
🎯 Major Rate Limiting Improvement - Research-Friendly Design

New Rate Limiting System:
✅ 12 calls per 5-minute window (vs 8 per session)
✅ 8 calls per 30-second burst protection
✅ Automatic time-based reset (no LM Studio restarts needed)
✅ Clear feedback with remaining calls and reset times

Key Benefits:
- Supports extended research sessions without interruption
- Prevents LLM spam while allowing legitimate research workflows
- Users can work continuously without restarting LM Studio
- Intelligent burst protection prevents overwhelming websites

Technical Implementation:
- Sliding window algorithm with timestamp tracking
- Dual-layer protection: burst + window limits
- Automatic cleanup of expired call history
- User-friendly error messages with precise wait times

This addresses the core user feedback that session-based limits were too restrictive for normal research use cases while maintaining responsible web scraping practices.

Breaking Change: Rate limiting behavior changed from session-based to time-based
Migration: No action needed - new system is more permissive
2025-08-10 23:47:42 -05:00

925 lines
28 KiB
JavaScript

#!/usr/bin/env node
/**
* WebFetch.MCP v0.1.8
* Live Web Access for Your Local AI — Tunable Search & Clean Content Extraction
*
* A production-ready Model Context Protocol (MCP) server that provides web search
* and content extraction capabilities for LM Studio and other MCP clients.
*
* Features:
* - Web search via local SearxNG instance
* - Advanced web content extraction with Mozilla Readability
* - Browser simulation to bypass bot detection
* - Rate limiting and responsible scraping practices
* - Comprehensive error handling and logging
*
* @author Jay Leon (@manull)
* @license MIT
* @version 0.1.8
* @repository https://github.com/manull/webfetch-mcp
*
* Copyright (c) 2025 Jay Leon (@manull)
* Licensed under the MIT License - see LICENSE file for details
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { JSDOM } from "jsdom";
import { Readability } from "@mozilla/readability";
import { writeFileSync, appendFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
// Use Node.js built-in fetch
const fetch = globalThis.fetch;
// ---------- Config ----------
const SEARXNG_BASE = process.env.SEARXNG_BASE || "http://localhost:8080";
const DEBUG = process.env.DEBUG === "true";
const DETAILED_LOG = process.env.DETAILED_LOG !== "false"; // Default to true
// Time-based rate limiting - more user-friendly approach
const RATE_LIMIT_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
const MAX_CALLS_PER_WINDOW = 12; // Allow more calls but over time
const BURST_LIMIT = 8; // Max calls in quick succession
const BURST_WINDOW_MS = 30 * 1000; // 30 seconds
let callHistory = []; // Array of timestamps
const checkCallLimit = () => {
const now = Date.now();
// Clean up old calls outside the main window
callHistory = callHistory.filter(timestamp => now - timestamp < RATE_LIMIT_WINDOW_MS);
// Check burst limit (quick succession)
const recentCalls = callHistory.filter(timestamp => now - timestamp < BURST_WINDOW_MS);
if (recentCalls.length >= BURST_LIMIT) {
return {
limited: true,
message: `🛑 **Burst Limit Reached**: ${BURST_LIMIT} calls in ${BURST_WINDOW_MS/1000} seconds. Please wait ${Math.ceil((BURST_WINDOW_MS - (now - recentCalls[0]))/1000)} seconds before making more requests. This prevents overwhelming websites and ensures reliable service.`
};
}
// Check overall window limit
if (callHistory.length >= MAX_CALLS_PER_WINDOW) {
const oldestCall = Math.min(...callHistory);
const resetTime = Math.ceil((RATE_LIMIT_WINDOW_MS - (now - oldestCall)) / 1000 / 60);
return {
limited: true,
message: `🛑 **Rate Limit Reached**: ${MAX_CALLS_PER_WINDOW} calls in ${RATE_LIMIT_WINDOW_MS/1000/60} minutes. Please wait ${resetTime} minute(s) for the limit to reset. This ensures responsible web scraping and prevents server overload.`
};
}
// Add current call to history
callHistory.push(now);
// Provide helpful warnings
const remaining = MAX_CALLS_PER_WINDOW - callHistory.length;
const recentCount = recentCalls.length + 1; // +1 for current call
if (remaining <= 2) {
return {
limited: false,
warning: `⚠️ **${remaining} calls remaining** in this ${RATE_LIMIT_WINDOW_MS/1000/60}-minute window.`
};
}
if (recentCount >= BURST_LIMIT - 2) {
return {
limited: false,
warning: `⚠️ **${BURST_LIMIT - recentCount} quick calls remaining** - Consider spacing out requests.`
};
}
return { limited: false };
};
// Modern browser user agents (updated regularly)
const BROWSER_USER_AGENTS = [
// Chrome on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
// Chrome on macOS
"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",
// Firefox on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
// Firefox on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0",
// Safari on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
// Edge on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"
];
// Simple user agent for search (less likely to be blocked)
const SEARCH_USER_AGENT = "MCP-WebTools/0.6 (+https://example.local)";
// Simple rate limiting to avoid overwhelming sites
const requestTimes = new Map();
const RATE_LIMIT_DELAY = 1000; // 1 second between requests to same domain
const getRateLimitDelay = (hostname) => {
const lastRequest = requestTimes.get(hostname) || 0;
const now = Date.now();
const timeSinceLastRequest = now - lastRequest;
if (timeSinceLastRequest < RATE_LIMIT_DELAY) {
return RATE_LIMIT_DELAY - timeSinceLastRequest;
}
return 0;
};
const updateRequestTime = (hostname) => {
requestTimes.set(hostname, Date.now());
};
// Get a random browser user agent
const getRandomUserAgent = () => {
return BROWSER_USER_AGENTS[Math.floor(Math.random() * BROWSER_USER_AGENTS.length)];
};
// Generate realistic browser headers
const getBrowserHeaders = (url) => {
const urlObj = new URL(url);
const userAgent = getRandomUserAgent();
return {
"User-Agent": userAgent,
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"DNT": "1",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Cache-Control": "max-age=0",
"sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"Referer": `https://${urlObj.hostname}/`
};
};
// Setup detailed logging
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const LOG_FILE = join(__dirname, 'mcp-server-detailed.log');
// Initialize log file
if (DETAILED_LOG) {
const logHeader = `\n${'='.repeat(80)}\nMCP Server Session Started: ${new Date().toISOString()}\n${'='.repeat(80)}\n`;
if (existsSync(LOG_FILE)) {
appendFileSync(LOG_FILE, logHeader);
} else {
writeFileSync(LOG_FILE, logHeader);
}
}
// Enhanced logging functions
const log = (...args) => {
if (DEBUG) {
console.error("[DEBUG]", new Date().toISOString(), ...args);
}
};
const detailedLog = (category, data) => {
if (!DETAILED_LOG) return;
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
category,
data
};
const logLine = `[${timestamp}] ${category}: ${JSON.stringify(data, null, 2)}\n`;
try {
appendFileSync(LOG_FILE, logLine);
} catch (error) {
console.error("Failed to write to log file:", error.message);
}
// Also log to console if debug is enabled
if (DEBUG) {
console.error(`[DETAILED-${category}]`, JSON.stringify(data, null, 2));
}
};
// Helper function to safely extract text
const safeText = (s = "", max = 20000) =>
String(s ?? "").replace(/\s+/g, " ").trim().slice(0, max);
// ---------- Create MCP server ----------
const server = new Server(
{
name: "mcp-web-tools-working",
version: "0.6.0",
},
{
capabilities: {
tools: {},
},
}
);
log("Starting MCP server...");
// ---------- Tool Definitions ----------
const TOOLS = [
{
name: "web_search",
description: "Search the web using a local SearxNG instance. Returns search results with titles, URLs, and snippets.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
limit: {
type: "number",
description: "Maximum number of results to return (1-20)",
minimum: 1,
maximum: 20,
default: 5,
},
site: {
type: "string",
description: "Restrict search to a specific site (e.g., 'weather.gov')",
},
engines: {
type: "string",
description: "Comma-separated list of search engines",
},
language: {
type: "string",
description: "Language code (e.g., 'en')",
},
safesearch: {
type: "number",
description: "Safe search level: 0=off, 1=moderate, 2=strict",
minimum: 0,
maximum: 2,
},
page: {
type: "number",
description: "Page number for pagination",
minimum: 1,
default: 1,
},
time_range: {
type: "string",
description: "Time range filter",
enum: ["day", "week", "month", "year"],
},
},
required: ["query"],
},
},
{
name: "web_fetch",
description: "Fetch and extract readable content from a web page URL using Mozilla Readability.",
inputSchema: {
type: "object",
properties: {
url: {
type: "string",
description: "HTTP/HTTPS URL to fetch (must be a valid URL)",
},
max_chars: {
type: "number",
description: "Maximum characters to return",
minimum: 1000,
maximum: 100000,
default: 20000,
},
},
required: ["url"],
},
},
];
// ---------- Tool Handlers ----------
async function handleWebSearch(args) {
log("web_search called with args:", args);
// Check call limit
const limitCheck = checkCallLimit();
if (limitCheck.limited) {
return {
content: [{ type: "text", text: limitCheck.message }]
};
}
const { query, limit = 5, site, engines, language, safesearch, page = 1, time_range } = args;
if (!query) {
return {
content: [{
type: "text",
text: "Error: Missing required parameter 'query'"
}]
};
}
try {
// Build search query
const searchQuery = site ? `${query} site:${site}` : query;
// Build URL with parameters
const url = new URL("/search", SEARXNG_BASE);
url.searchParams.set("format", "json");
url.searchParams.set("q", searchQuery);
url.searchParams.set("pageno", String(page));
url.searchParams.set("categories", "general");
if (limit) {
url.searchParams.set("count", String(limit));
}
if (engines) url.searchParams.set("engines", engines);
if (language) url.searchParams.set("language", language);
if (typeof safesearch === "number") url.searchParams.set("safesearch", String(safesearch));
if (time_range) url.searchParams.set("time_range", time_range);
log("Fetching URL:", url.toString());
const startTime = Date.now();
const response = await fetch(url.toString(), {
headers: {
"User-Agent": SEARCH_USER_AGENT,
"Accept": "application/json",
},
signal: AbortSignal.timeout(15000)
});
const fetchTime = Date.now() - startTime;
log(`Response received in ${fetchTime}ms, status: ${response.status}`);
if (!response.ok) {
const errorText = await response.text();
log("Error response:", errorText);
return {
content: [{
type: "text",
text: `Search failed: HTTP ${response.status} - ${response.statusText}`
}]
};
}
const responseText = await response.text();
log("Response text length:", responseText.length);
let data;
try {
data = JSON.parse(responseText);
} catch (parseError) {
log("JSON parse error:", parseError);
return {
content: [{
type: "text",
text: `Search failed: Invalid JSON response from search engine`
}]
};
}
log("Parsed response structure:", {
hasResults: Array.isArray(data.results),
resultsLength: data.results?.length || 0,
numberofResults: data.number_of_results,
});
const rawResults = data.results || [];
if (rawResults.length === 0) {
return {
content: [{
type: "text",
text: `No search results found for query: "${query}"`
}]
};
}
// Process and format results
const results = rawResults.slice(0, limit).map((item, index) => {
log(`Processing result ${index + 1}:`, {
title: item.title?.slice(0, 50) + "...",
url: item.url,
engine: item.engine,
});
return {
title: safeText(item.title || "No title", 300),
url: item.url || "",
snippet: safeText(item.content || item.description || "", 500),
engine: item.engine || "unknown",
score: item.score || 0,
category: item.category || "general"
};
});
// Create formatted response text
let formattedResponse = `Search Results for "${query}":\n\n`;
results.forEach((result, index) => {
formattedResponse += `${index + 1}. **${result.title}**\n`;
formattedResponse += ` URL: ${result.url}\n`;
formattedResponse += ` ${result.snippet}\n`;
formattedResponse += ` Source: ${result.engine}\n\n`;
});
formattedResponse += `\nFound ${results.length} results`;
if (data.number_of_results) {
formattedResponse += ` (${data.number_of_results} total available)`;
}
// Add warning if approaching limit
if (limitCheck.warning) {
formattedResponse += `\n\n${limitCheck.warning}`;
}
log("Returning successful result with", results.length, "items");
return {
content: [{ type: "text", text: formattedResponse }]
};
} catch (error) {
log("Exception in web_search:", error);
return {
content: [{
type: "text",
text: `Search failed: ${error.message}`
}]
};
}
}
async function handleWebFetch(args) {
log("web_fetch called with args:", args);
// Check call limit
const limitCheck = checkCallLimit();
if (limitCheck.limited) {
return {
content: [{ type: "text", text: limitCheck.message }]
};
}
const { url, max_chars = 20000, retry_count = 0 } = args;
if (!url) {
return {
content: [{
type: "text",
text: "Error: Missing required parameter 'url'"
}]
};
}
// Validate URL
let validUrl;
try {
validUrl = new URL(url);
if (!["http:", "https:"].includes(validUrl.protocol)) {
throw new Error("Only HTTP and HTTPS URLs are supported");
}
} catch (urlError) {
return {
content: [{
type: "text",
text: `Invalid URL: ${urlError.message}`
}]
};
}
try {
log("Fetching URL:", validUrl.toString());
// Apply rate limiting
const hostname = validUrl.hostname;
const rateLimitDelay = getRateLimitDelay(hostname);
if (rateLimitDelay > 0) {
log(`Rate limiting: waiting ${rateLimitDelay}ms for ${hostname}`);
await new Promise(resolve => setTimeout(resolve, rateLimitDelay));
}
updateRequestTime(hostname);
// Add realistic delay to simulate human browsing (only on first attempt)
if (retry_count === 0) {
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500));
}
// Get realistic browser headers
const browserHeaders = getBrowserHeaders(validUrl.toString());
// Add some randomization to headers to avoid fingerprinting
const headers = { ...browserHeaders };
// Sometimes remove optional headers to vary fingerprint
if (Math.random() > 0.7) {
delete headers["DNT"];
}
if (Math.random() > 0.8) {
delete headers["sec-ch-ua"];
delete headers["sec-ch-ua-mobile"];
delete headers["sec-ch-ua-platform"];
}
log("Using headers:", Object.keys(headers).join(", "));
const response = await fetch(validUrl.toString(), {
headers,
signal: AbortSignal.timeout(20000), // Increased timeout
redirect: 'follow',
// Add realistic fetch options
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'default'
});
log("Fetch response status:", response.status);
// Handle different HTTP status codes with retry logic
if (!response.ok) {
// Retry on certain status codes if we haven't retried yet
if (retry_count === 0 && [429, 503, 502, 504].includes(response.status)) {
log(`Retrying request due to ${response.status} status`);
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 3000));
return handleWebFetch({ ...args, retry_count: 1 });
}
// Handle specific error codes with helpful messages
let errorMessage;
switch (response.status) {
case 403:
errorMessage = `Access forbidden (${response.status}). The site may be blocking automated requests or require authentication.`;
break;
case 404:
errorMessage = `Page not found (${response.status}). The URL may be incorrect or the page may have been moved.`;
break;
case 429:
errorMessage = `Rate limited (${response.status}). The site is temporarily blocking requests due to too many attempts.`;
break;
case 503:
errorMessage = `Service unavailable (${response.status}). The site may be temporarily down or overloaded.`;
break;
default:
errorMessage = `Failed to fetch URL: HTTP ${response.status} - ${response.statusText}`;
}
return {
content: [{
type: "text",
text: errorMessage
}]
};
}
// Check content type to handle non-HTML content appropriately
const contentType = response.headers.get('content-type') || '';
log("Content-Type:", contentType);
// Handle non-HTML content types
if (!contentType.includes('text/html') && !contentType.includes('application/xhtml')) {
const fileExtension = validUrl.pathname.split('.').pop()?.toLowerCase();
if (['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(fileExtension || '')) {
return {
content: [{
type: "text",
text: `Cannot extract text from ${fileExtension?.toUpperCase() || 'binary'} files. This URL points to a ${contentType || 'binary'} file, not a web page. Please provide a URL to an HTML web page for text extraction.`
}]
};
}
if (contentType.includes('application/') || contentType.includes('image/') || contentType.includes('video/') || contentType.includes('audio/')) {
return {
content: [{
type: "text",
text: `Cannot extract text from ${contentType} content. This URL points to a binary file, not a web page. Please provide a URL to an HTML web page for text extraction.`
}]
};
}
}
const html = await response.text();
log("Fetched content length:", html.length);
// Check if content looks like binary data (common with encoding issues)
const binaryPattern = /[\x00-\x08\x0E-\x1F\x7F-\xFF]/g;
const binaryMatches = html.match(binaryPattern);
if (binaryMatches && binaryMatches.length > html.length * 0.1) {
return {
content: [{
type: "text",
text: `This URL appears to contain binary data or has encoding issues. The content cannot be properly extracted as readable text. Please verify the URL points to a standard HTML web page.`
}]
};
}
// Enhanced content extraction with better DOM parsing
const dom = new JSDOM(html, {
url: validUrl.toString(),
pretendToBeVisual: true,
resources: "usable"
});
// Wait a moment for any immediate DOM updates
await new Promise(resolve => setTimeout(resolve, 100));
const document = dom.window.document;
// Remove unwanted elements that interfere with content extraction
const unwantedSelectors = [
'script', 'style', 'nav', 'header', 'footer', 'aside',
'.advertisement', '.ads', '.social-share', '.comments',
'.sidebar', '.menu', '.navigation', '.cookie-notice',
'[class*="ad-"]', '[id*="ad-"]', '[class*="social"]'
];
unwantedSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
// Try Readability first
const reader = new Readability(document);
const article = reader.parse();
let extractedContent;
if (article && article.textContent && article.textContent.trim().length > 100) {
log("Readability extraction successful");
extractedContent = `**${article.title || document.title || "Untitled"}**\n\n`;
if (article.byline) {
extractedContent += `By: ${article.byline}\n\n`;
}
extractedContent += safeText(article.textContent || "", max_chars);
} else {
log("Readability failed, trying enhanced fallback extraction");
// Enhanced fallback: try to find main content areas
const contentSelectors = [
'main', 'article', '[role="main"]', '.main-content', '.content',
'.post-content', '.entry-content', '.article-content', '.story-body',
'#content', '#main', '.container .content', '.page-content'
];
let bestContent = "";
let bestScore = 0;
for (const selector of contentSelectors) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
const text = element.textContent || "";
const score = text.length;
if (score > bestScore && score > 200) {
bestContent = text;
bestScore = score;
}
}
}
// If no good content found, fall back to body
if (!bestContent || bestContent.trim().length < 100) {
bestContent = document.body?.textContent || "";
}
// Check if content is meaningful
if (bestContent.trim().length < 100) {
// Try one more approach: look for paragraphs
const paragraphs = Array.from(document.querySelectorAll('p'))
.map(p => p.textContent || "")
.filter(text => text.trim().length > 20)
.join("\n\n");
if (paragraphs.length > 100) {
bestContent = paragraphs;
} else {
return {
content: [{
type: "text",
text: `Unable to extract meaningful text content from this URL. The page may be:\n- A single-page application that loads content with JavaScript\n- A page with mostly images or media\n- Protected by authentication or paywall\n- Not a standard HTML page\n- Blocked by anti-bot measures\n\nPage title: ${document.title || "No title"}\nTry accessing the URL directly in a browser to verify the content is accessible.`
}]
};
}
}
extractedContent = `**${document.title || "Untitled"}**\n\n`;
extractedContent += safeText(bestContent, max_chars);
}
// Final check for garbled content
const cleanContent = extractedContent.replace(/[^\x20-\x7E\n\r\t]/g, '');
if (cleanContent.length < extractedContent.length * 0.5) {
return {
content: [{
type: "text",
text: `The extracted content contains significant encoding issues or non-text data. This may be due to:\n- Character encoding problems\n- Binary content mixed with text\n- Non-standard page format\n\nPage title: ${dom.window.document.title || "No title"}\nURL: ${validUrl.toString()}`
}]
};
}
log("Content extraction completed, content length:", extractedContent.length);
// Add warning if approaching limit
if (limitCheck.warning) {
extractedContent += `\n\n${limitCheck.warning}`;
}
return {
content: [{ type: "text", text: extractedContent }]
};
} catch (error) {
log("Exception in web_fetch:", error);
return {
content: [{
type: "text",
text: `Fetch failed: ${error.message}`
}]
};
}
}
// ---------- Server Handlers ----------
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
detailedLog("INCOMING_REQUEST", {
method: "tools/list",
request: request,
timestamp: new Date().toISOString()
});
const response = {
tools: TOOLS,
};
detailedLog("OUTGOING_RESPONSE", {
method: "tools/list",
response: response,
timestamp: new Date().toISOString()
});
return response;
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
detailedLog("INCOMING_REQUEST", {
method: "tools/call",
tool_name: name,
arguments: args,
full_request: request,
timestamp: new Date().toISOString()
});
log("Tool called:", name, "with args:", args);
let response;
let error = null;
try {
switch (name) {
case "web_search":
response = await handleWebSearch(args || {});
break;
case "web_fetch":
response = await handleWebFetch(args || {});
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (err) {
error = err;
response = {
content: [{
type: "text",
text: `Tool execution failed: ${err.message}`
}]
};
}
detailedLog("OUTGOING_RESPONSE", {
method: "tools/call",
tool_name: name,
response: response,
error: error ? {
message: error.message,
stack: error.stack
} : null,
timestamp: new Date().toISOString()
});
if (error) {
detailedLog("ERROR", {
method: "tools/call",
tool_name: name,
error_details: {
message: error.message,
stack: error.stack,
arguments: args
},
timestamp: new Date().toISOString()
});
}
return response;
});
// ---------- Connect and Start ----------
async function main() {
// Log server startup
detailedLog("SERVER_STARTUP", {
server_info: {
name: "mcp-web-tools-final",
version: "0.6.0",
searxng_base: SEARXNG_BASE,
debug_enabled: DEBUG,
detailed_log_enabled: DETAILED_LOG,
log_file: LOG_FILE
},
environment: {
node_version: process.version,
platform: process.platform,
cwd: process.cwd(),
argv: process.argv
},
timestamp: new Date().toISOString()
});
// Note: Initialization is handled automatically by the MCP SDK
// We'll capture it through transport-level logging if needed
// Log all unhandled requests
server.onerror = (error) => {
detailedLog("SERVER_ERROR", {
error: {
message: error.message,
stack: error.stack,
name: error.name
},
timestamp: new Date().toISOString()
});
console.error("Server error:", error);
};
const transport = new StdioServerTransport();
detailedLog("TRANSPORT_CONNECTING", {
transport_type: "stdio",
timestamp: new Date().toISOString()
});
await server.connect(transport);
detailedLog("SERVER_READY", {
message: "MCP Web Tools Server is ready and listening",
timestamp: new Date().toISOString()
});
console.error("MCP Web Tools Server ready");
console.error(`[mcp-web-tools-working] SearxNG Base: ${SEARXNG_BASE}`);
console.error(`[mcp-web-tools-working] Debug mode: ${DEBUG}`);
console.error(`[mcp-web-tools-working] Detailed logging: ${DETAILED_LOG ? 'ENABLED' : 'DISABLED'}`);
if (DETAILED_LOG) {
console.error(`[mcp-web-tools-working] Log file: ${LOG_FILE}`);
}
}
// Add process event handlers for cleanup logging
process.on('SIGINT', () => {
detailedLog("SERVER_SHUTDOWN", {
reason: "SIGINT",
timestamp: new Date().toISOString()
});
process.exit(0);
});
process.on('SIGTERM', () => {
detailedLog("SERVER_SHUTDOWN", {
reason: "SIGTERM",
timestamp: new Date().toISOString()
});
console.error("[mcp-web-tools-working] Shutting down gracefully...");
process.exit(0);
});
main().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});