Skip to content

Instantly share code, notes, and snippets.

@ewired
Created December 13, 2024 06:27
Show Gist options
  • Save ewired/90e2a0af9f83174afb1c6db5b99d1ee7 to your computer and use it in GitHub Desktop.
Save ewired/90e2a0af9f83174afb1c6db5b99d1ee7 to your computer and use it in GitHub Desktop.
SearXNG MCP server
#!/usr/bin/env -S deno run --allow-net
/*
{
"mcpServers": {
"searxng": {
"command": "/path/to/deno",
"args": [
"run",
"--allow-net",
"/home/<YOUR USERNAME>/Documents/Cline/MCP/searxng.ts",
"searx.foss.family,searx.perennialte.ch,search.mdosch.de,etsi.me",
"engines=google"
]
}
}
}
*/
import { Server } from "npm:@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "npm:@modelcontextprotocol/sdk/types.js";
interface SearchArgs {
query: string;
}
interface SearchResult {
url: string;
title: string;
content: string;
engine: string;
}
const isValidSearchArgs = (args: unknown): args is SearchArgs => {
if (typeof args !== "object" || args === null) return false;
const { query } = args as SearchArgs;
return typeof query === "string";
};
// Fisher-Yates shuffle
function shuffle<T>(array: T[]): T[] {
const result = [...array];
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
function formatResults(results: SearchResult[]): string {
return results.map((result, index) => {
return [
`Result ${index + 1}:`,
`Title: ${result.title}`,
`URL: ${result.url}`,
`Engine: ${result.engine}`,
`Summary: ${result.content}`,
"",
].join("\n");
}).join("\n");
}
class SearxNGServer {
private server: Server;
private instances: string[];
private searchParams: URLSearchParams;
constructor(domains: string[], searchParams: string[]) {
if (domains.length === 0) {
throw new Error("At least one SearxNG instance must be provided");
}
this.instances = domains.map((domain) => `https://${domain}`);
// Parse additional search parameters
this.searchParams = new URLSearchParams();
for (const param of searchParams) {
const [key, value] = param.split("=");
if (key && value) {
this.searchParams.append(key, value);
}
}
this.server = new Server(
{
name: "searxng-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
},
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error("[MCP Error]", error);
// Handle graceful shutdown
const shutdown = async () => {
await this.server.close();
Deno.exit(0);
};
Deno.addSignalListener("SIGINT", shutdown);
Deno.addSignalListener("SIGTERM", shutdown);
}
private setupToolHandlers() {
this.server.setRequestHandler(
ListToolsRequestSchema,
() =>
Promise.resolve({
tools: [
{
name: "search",
description: "Search the public web",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
},
required: ["query"],
},
},
],
}),
);
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== "search") {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`,
);
}
if (!isValidSearchArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid search arguments",
);
}
const { query } = request.params.arguments;
try {
// Randomize instance order for each query
const shuffledInstances = shuffle(this.instances);
// Try each instance until one succeeds
for (const instance of shuffledInstances) {
try {
const url = new URL("/search", instance);
// Set required parameters
url.searchParams.set("q", query);
url.searchParams.set("format", "json");
// Add any additional search parameters
for (const [key, value] of this.searchParams) {
url.searchParams.set(key, value);
}
const response = await fetch(url);
if (!response.ok) {
console.error(
`Instance ${instance} failed with status ${response.status}`,
);
continue;
}
const data = await response.json();
if (!data.results || !Array.isArray(data.results)) {
console.error(`Instance ${instance} returned invalid results`);
continue;
}
const formattedResults = formatResults(data.results);
return {
content: [
{
type: "text",
text: formattedResults,
},
],
};
} catch (error) {
console.error(`Instance ${instance} failed:`, error);
continue;
}
}
throw new Error("All instances failed");
} catch (error) {
const message = error instanceof Error ? error.message : error;
return {
content: [
{
type: "text",
text: `Search failed: ${message}`,
},
],
isError: true,
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("SearxNG MCP server running on stdio");
}
}
// Parse arguments
const [domainsArg, ...searchParams] = Deno.args;
const domains = domainsArg?.split(",") ?? [];
const server = new SearxNGServer(domains, searchParams);
server.run().catch(console.error);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment