@jharding_npm/mcp-server-searxng
Version:
MCP server for SearXNG meta search integration with enhanced error messaging
164 lines (163 loc) • 7.58 kB
JavaScript
import fetch from 'node-fetch';
import { USER_AGENT, httpsAgent, httpAgent } from './config.js';
// Add debug logging function that can be enabled via environment variable
const DEBUG = process.env.MCP_SEARXNG_DEBUG === 'true';
function logDebug(message, data) {
if (DEBUG) {
console.error(`Debug: ${message}`, data ? `\n${JSON.stringify(data, null, 2)}` : '');
}
}
// Add console error wrapper
function logError(message, error) {
console.error(`Error: ${message}`, error ? `\n${error}` : '');
}
export class SearchHandler {
constructor(instances) {
this.instances = instances;
}
async search(params) {
logDebug("Search parameters", params);
// Handle offset by converting to page number
let pageNumber = params.page || 1;
if (params.offset && params.offset > 0) {
const resultsPerPage = params.max_results || 10;
pageNumber = Math.floor(params.offset / resultsPerPage) + 1;
}
const searchParams = {
q: params.query,
pageno: pageNumber,
language: params.language || 'all',
time_range: params.time_range === 'all_time' ? '' : (params.time_range || ''),
safesearch: params.safesearch ?? 0,
format: 'json'
};
const errors = [];
for (const instance of this.instances) {
try {
const searchUrl = new URL('/search', instance);
logDebug(`Attempting search with instance: ${instance}`);
const response = await fetch(searchUrl.toString(), {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT
},
agent: searchUrl.protocol === 'https:' ? httpsAgent : httpAgent,
body: new URLSearchParams(Object.entries(searchParams).reduce((acc, [key, value]) => {
acc[key] = String(value);
return acc;
}, {})).toString()
});
if (!response.ok) {
let errorText;
try {
errorText = await response.text();
}
catch {
errorText = 'No response body available';
}
const errorMsg = `${instance} returned HTTP ${response.status} ${response.statusText}. Response: ${errorText.substring(0, 200)}`;
logError(errorMsg);
errors.push(errorMsg);
continue;
}
const data = await response.json();
if (!data.results?.length) {
const errorMsg = `${instance} returned no results`;
logError(errorMsg);
errors.push(errorMsg);
continue;
}
logDebug(`Search successful with ${instance}, found ${data.results.length} results`);
return data;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMsg = `Failed to connect to ${instance}: ${errorMessage}`;
logError(errorMsg, error);
errors.push(errorMsg);
continue;
}
}
const errorDetails = errors.map((err, i) => ` [${i + 1}] ${err}`).join("\n");
throw new Error(`All SearXNG instances failed. Please ensure SearXNG is running on one of these instances: ${this.instances.join(', ')}\n\nDetails:\n${errorDetails}`);
}
}
export class ParallelSearchHandler extends SearchHandler {
async search(params) {
logDebug("Search parameters", params);
// Handle offset by converting to page number
let pageNumber = params.page || 1;
if (params.offset && params.offset > 0) {
const resultsPerPage = params.max_results || 10;
pageNumber = Math.floor(params.offset / resultsPerPage) + 1;
}
const searchParams = {
q: params.query,
pageno: pageNumber,
language: params.language || 'all',
time_range: params.time_range === 'all_time' ? '' : (params.time_range || ''),
safesearch: params.safesearch ?? 0,
format: 'json'
};
const searchPromises = this.instances.map(async (instance) => {
try {
const searchUrl = new URL('/search', instance);
logDebug(`Attempting search with instance: ${instance}`);
const response = await fetch(searchUrl.toString(), {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': USER_AGENT
},
agent: searchUrl.protocol === 'https:' ? httpsAgent : httpAgent,
body: new URLSearchParams(Object.entries(searchParams).reduce((acc, [key, value]) => {
acc[key] = String(value);
return acc;
}, {})).toString()
});
if (!response.ok) {
let errorText;
try {
errorText = await response.text();
}
catch {
errorText = 'No response body available';
}
const errorMsg = `${instance} returned HTTP ${response.status} ${response.statusText}. Response: ${errorText.substring(0, 200)}`;
logError(errorMsg);
throw new Error(errorMsg);
}
const data = await response.json();
if (!data.results?.length) {
const errorMsg = `${instance} returned no results`;
logError(errorMsg);
throw new Error(errorMsg);
}
logDebug(`Search successful with ${instance}, found ${data.results.length} results`);
return data;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorMsg = `Failed to connect to ${instance}: ${errorMessage}`;
logError(errorMsg, error);
throw new Error(errorMsg);
}
});
const results = await Promise.allSettled(searchPromises);
const fulfilled = results.filter(r => r.status === 'fulfilled').map(r => r.value);
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason.message);
if (fulfilled.length === 0) {
const errorDetails = errors.map((err, i) => ` [${i + 1}] ${err}`).join("\n");
throw new Error(`All SearXNG instances failed. Please ensure SearXNG is running on one of these instances: ${this.instances.join(', ')}\n\nDetails:\n${errorDetails}`);
}
// Aggregate results from all successful instances
const allResults = fulfilled.flatMap(r => r.results);
return {
results: allResults,
number_of_results: allResults.length
};
}
}