SearXNG MCP Server
by kevinwatt
#!/usr/bin/env node
import type { Response } from 'node-fetch';
import fetch from 'node-fetch';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { Agent as HttpsAgent } from 'node:https';
import { Agent as HttpAgent } from 'node:http';
// Add console error wrapper
function logError(message: string, error?: unknown) {
console.error(`Error: ${message}`, error ? `\n${error}` : '');
}
// Primary SearXNG instances for fallback
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 WEB_SEARCH_TOOL: Tool = {
name: "web_search",
description:
"Performs a web search using SearXNG, ideal for general queries, news, articles and online content. " +
"Supports multiple search categories, languages, time ranges and safe search filtering. " +
"Returns relevant results from multiple search engines combined.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query"
},
page: {
type: "number",
description: "Page number (default 1)",
default: 1
},
language: {
type: "string",
description: "Search language code (e.g. 'en', 'zh', 'jp', 'all')",
default: "all"
},
categories: {
type: "array",
items: {
type: "string",
enum: ["general", "news", "science", "files", "images", "videos", "music", "social media", "it"]
},
default: ["general"]
},
time_range: {
type: "string",
enum: ["all_time", "day", "week", "month", "year"],
default: "all_time"
},
safesearch: {
type: "number",
description: "0: None, 1: Moderate, 2: Strict",
default: 1
}
},
required: ["query"]
}
};
// Server implementation
const server = new Server(
{
name: "kevinwatt/mcp-server-searxng",
version: "0.3.5",
description: "SearXNG meta search integration for MCP"
},
{
capabilities: {
tools: {},
},
},
);
// Helper function to try different instances
async function searchWithFallback(params: any) {
const searchParams = {
q: params.query,
pageno: params.page || 1,
language: params.language || 'all',
categories: params.categories?.join(',') || 'general',
time_range: params.time_range === 'all_time' ? '' : (params.time_range || ''),
safesearch: params.safesearch ?? 1,
format: 'json'
};
for (const instance of SEARXNG_INSTANCES) {
try {
const searchUrl = new URL('/search', 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(searchParams).toString()
});
if (!response.ok) {
logError(`${instance} returned ${response.status}. Please check if SearXNG is running.`);
continue;
}
const data = await response.json();
if (!data.results?.length) {
logError(`${instance} returned no results`);
continue;
}
return data;
} catch (error) {
logError(`Failed to connect to ${instance}. Please check if SearXNG is running.`, error);
continue;
}
}
throw new Error("All SearXNG instances failed. Please ensure SearXNG is running on one of these instances: " + SEARXNG_INSTANCES.join(', '));
}
interface SearchResult {
title: string;
content?: string;
url: string;
engine?: string;
}
function formatSearchResult(result: SearchResult) {
const parts = [
`Title: ${result.title}`,
`URL: ${result.url}`
];
if (result.content) {
parts.push(`Content: ${result.content}`);
}
if (result.engine) {
parts.push(`Source: ${result.engine}`);
}
return parts.join('\n');
}
function isWebSearchArgs(args: unknown): args is {
query: string;
page?: number;
language?: string;
categories?: string[];
time_range?: string;
safesearch?: number;
} {
return (
typeof args === "object" &&
args !== null &&
"query" in args &&
typeof (args as { query: string }).query === "string"
);
}
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [WEB_SEARCH_TOOL]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (name !== "web_search" || !args) {
throw new Error("Invalid tool or arguments: expected 'web_search'");
}
if (!isWebSearchArgs(args)) {
throw new Error("Invalid arguments for web_search");
}
const results = await searchWithFallback(args);
return {
content: [{
type: "text",
text: results.results.map(formatSearchResult).join('\n\n')
}],
isError: false,
};
} catch (error) {
logError('Search failed', error);
return {
content: [{ type: "text", text: String(error) }],
isError: true,
};
}
});
// 修改 runServer 為可選的運行
export async function runServer() {
const transport = new StdioServerTransport();
try {
await server.connect(transport);
console.error("SearXNG Search MCP Server running on stdio");
} catch (error) {
logError('Fatal error running server', error);
process.exit(1);
}
}
runServer();
export {
formatSearchResult,
isWebSearchArgs,
searchWithFallback,
SEARXNG_INSTANCES
};