import express from 'express'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { z } from 'zod'; const PORT = parseInt(process.env.PORT || '3102'); const SEARXNG_URL = (process.env.SEARXNG_URL || 'http://localhost:11437').replace(/\/$/, ''); function createServer() { const server = new McpServer({ name: 'searxng-mcp', version: '1.0.0' }); server.tool( 'searxng_search', 'Search the web using the self-hosted SearXNG meta-search engine. Returns titles, URLs, and content snippets from multiple search engines.', { query: z.string().describe('Search query'), categories: z.enum(['general', 'news', 'images', 'videos', 'science', 'files', 'social_media', 'it']) .optional() .describe('Search category, default: general'), language: z.string().optional().describe('Language code (e.g. "ru", "en", "auto"), default: auto'), time_range: z.enum(['day', 'week', 'month', 'year']).optional().describe('Limit results to time range'), limit: z.number().optional().describe('Max results to return, default 10'), }, async ({ query, categories, language, time_range, limit = 10 }) => { try { const params = new URLSearchParams({ q: query, format: 'json', categories: categories || 'general', language: language || 'auto', }); if (time_range) params.set('time_range', time_range); const res = await fetch(`${SEARXNG_URL}/search?${params}`); if (!res.ok) { return { content: [{ type: 'text', text: `SearXNG returned HTTP ${res.status}` }], isError: true }; } const data = await res.json(); const results = (data.results || []).slice(0, limit).map(r => ({ title: r.title || null, url: r.url || null, content: (r.content || '').slice(0, 500) || null, engine: r.engine || null, score: r.score != null ? Math.round(r.score * 100) / 100 : null, publishedDate: r.publishedDate || null, })); const out = { query: data.query, totalResults: data.number_of_results, count: results.length, results, }; return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] }; } catch (e) { return { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true }; } } ); return server; } const app = express(); app.use(express.json()); const sseTransports = new Map(); app.all('/mcp', async (req, res) => { try { const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); res.on('close', () => transport.close()); await createServer().connect(transport); await transport.handleRequest(req, res, req.body); } catch (e) { if (!res.headersSent) res.status(500).json({ error: e.message }); } }); app.get('/sse', async (req, res) => { const transport = new SSEServerTransport('/messages', res); sseTransports.set(transport.sessionId, transport); res.on('close', () => sseTransports.delete(transport.sessionId)); await createServer().connect(transport); }); app.post('/messages', async (req, res) => { const transport = sseTransports.get(req.query.sessionId); if (!transport) return res.status(400).send('Unknown session'); await transport.handlePostMessage(req, res); }); app.get('/health', (_, res) => res.json({ status: 'ok', searxng: SEARXNG_URL })); app.listen(PORT, () => console.log(`searxng-mcp listening on :${PORT}`));