@jharding_npm/mcp-server-searxng
Version:
MCP server for SearXNG meta search integration with enhanced error messaging
215 lines (214 loc) • 8.55 kB
JavaScript
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 { Agent as HttpsAgent } from 'node:https';
import { Agent as HttpAgent } from 'node:http';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const { version } = require('../package.json');
// Import SearchHandler and ParallelSearchHandler from search-handler.ts
import { ParallelSearchHandler } from './search-handler.js';
// Import utility functions from utils.ts
import { buildStructuredResponse, isWebSearchArgs } from './utils.js';
// Add console error wrapper
function logError(message, error) {
console.error(`Error: ${message}`, error ? `\n${error}` : '');
}
// 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)}` : '');
}
}
// Primary SearXNG instances for fallback
export const SEARXNG_INSTANCES = process.env.SEARXNG_INSTANCES
? process.env.SEARXNG_INSTANCES.split(',')
: ['http://localhost:8080'];
// HTTP headers with user agent from env
const USER_AGENT = process.env.SEARXNG_USER_AGENT || 'MCP-SearXNG/1.0';
// Add HTTP/HTTPS agent configuration
const httpsAgent = new HttpsAgent({
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'
});
const httpAgent = new HttpAgent();
const PARAMETER_HELP = `
# Pagination: Use offset (not page) - offset=0=first result, offset=10=11th result
# Example: offset=39, max_results=4 gets results 40-43
`;
const WEB_SEARCH_TOOL = {
name: "web_search",
description: "Performs a web search using SearXNG and returns structured JSON results.\n" +
"\n" +
"# IMPORTANT: Pagination is offset-based, NOT page-based.\n" +
"To get a specific range of results, set 'offset' to the zero-based index of the first result you want, and 'max_results' to how many results you want.\n" +
"For example, to get results 40-43, set offset=39 and max_results=4.\n" +
PARAMETER_HELP,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search terms. Example: 'climate change'"
},
max_results: {
type: "number",
description: "Number of results to return (1-100, default: 10)",
default: 10,
minimum: 1,
maximum: 100
},
offset: {
type: "number",
description: "Number of results to skip (default: 0)",
default: 0,
minimum: 0
},
content_length: {
type: "number",
description: "Max characters per result (0 for no content, only metadata; 1-1000, default: 200)",
default: 200,
minimum: 0,
maximum: 1000
},
page: {
type: "number",
description: "(Advanced) Page number. Usually leave as default.",
default: 1
},
language: {
type: "string",
description: "Language code (e.g. 'en', 'all'). Default: 'all'",
default: "all"
},
time_range: {
type: "string",
enum: ["all_time", "day", "week", "month", "year"],
description: "Time range: 'all_time', 'day', 'week', 'month', 'year'",
default: "all_time"
},
safesearch: {
type: "number",
description: "Safe search: 0 (off, default), 1 (moderate), 2 (strict)",
default: 0
}
},
required: ["query"]
}
};
const serverConfig = {
name: "@jharding_npm/mcp-server-searxng",
version,
description: "SearXNG meta search integration for MCP with enhanced error handling and parameter control"
};
const server = new Server(serverConfig, {
capabilities: {
tools: {},
},
});
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [WEB_SEARCH_TOOL]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
logDebug('Tool request received', { name, args });
if (name !== "web_search") {
const errorMsg = `Invalid tool: expected 'web_search', got '${name}'`;
logError(errorMsg);
return {
content: [{ type: "text", text: errorMsg }],
isError: true,
};
}
if (!args) {
const errorMsg = "Missing arguments for web_search";
logError(errorMsg);
return {
content: [{ type: "text", text: errorMsg }],
isError: true,
};
}
// Validate arguments with improved validation
const validation = isWebSearchArgs(args);
if (!validation.valid) {
const errorMsg = validation.error || "Invalid arguments for web_search";
logError(errorMsg, args);
// For time_range parameter specifically, provide more helpful guidance
if (errorMsg.includes("time_range") && args.time_range) {
const receivedValue = String(args.time_range);
logDebug(`Invalid time_range value received: "${receivedValue}"`, {
received: receivedValue,
validValues: ["all_time", "day", "week", "month", "year"]
});
// Return enhanced error message with examples
return {
content: [{
type: "text",
text: `${errorMsg}\n\nYou provided: "${receivedValue}"\nValid examples: "day" (not "1d"), "week" (not "7d"), "month" (not "30d")`
}],
isError: true,
};
}
return {
content: [{ type: "text", text: errorMsg }],
isError: true,
};
}
const startTime = Date.now();
const searchHandler = new ParallelSearchHandler(SEARXNG_INSTANCES);
const data = await searchHandler.search(args);
const structuredResponse = buildStructuredResponse(data, args.query, args, startTime);
return {
content: [{
type: "text",
text: JSON.stringify(structuredResponse, null, 2)
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logError("Search failed", error);
return {
content: [{
type: "text",
text: `Search failed: ${errorMessage}`
}],
isError: true
};
}
});
export async function searchWithFallback(params) {
const searchHandler = new ParallelSearchHandler(SEARXNG_INSTANCES);
return await searchHandler.search(params);
}
export async function runServer() {
await server.connect(new StdioServerTransport());
}
// Start the server only when this file is executed directly (not imported or in tests)
// Check if this is the main module being executed and not in a test environment
const isMainModule = process.argv[1] &&
!process.env.NODE_ENV?.includes('test') &&
!process.argv[1].includes('jest') &&
(process.argv[1].endsWith('index.js') ||
process.argv[1].includes('mcp-server-searxng') ||
import.meta.url === `file://${process.argv[1]}`);
logDebug("Server startup check", {
importMetaUrl: import.meta.url,
processArgv1: process.argv[1],
isMainModule,
endsWithIndex: process.argv[1]?.endsWith('index.js'),
includesPackageName: process.argv[1]?.includes('mcp-server-searxng')
});
if (isMainModule) {
logDebug("Starting MCP server...");
runServer().catch((error) => {
logError("Failed to start server", error);
process.exit(1);
});
}
else {
logDebug("Not starting server - file was imported, not executed directly");
}