# MCP Search Servers Model Context Protocol (MCP) servers that expose **Google** and **DuckDuckGo** search as tools for LLM clients (Claude Desktop, Cursor, VS Code, ...), backed by a self-hosted [SearXNG](https://github.com/searxng/searxng) metasearch instance. SearXNG aggregates results from Google, DuckDuckGo, Brave, Wikipedia, and more with its own request handling, so a single SearXNG container serves both MCP servers — no browser fingerprinting, no API keys, fully self-hosted. ## Services | Service | Port | MCP tool | Description | |-------------------|------|--------------------|--------------------------------------| | `google-mcp` | 3001 | `google_search` | Google results via SearXNG | | `duckduckgo-mcp` | 3002 | `duckduckgo_search`| DuckDuckGo results via SearXNG | | `searxng` | — | — | Internal metasearch engine (not published) | Each server exposes: - `/sse` + `/messages/` — MCP SSE transport - `/search?q=...&num=N` — plain HTTP JSON endpoint - `/health` — liveness probe ## Quick Start ```bash cp .env.example .env # fill in SEARXNG_SECRET docker compose up -d --build ``` Generate a SearXNG secret: ```bash python3 -c "import secrets; print(secrets.token_hex(32))" ``` Verify: ```bash curl http://localhost:3001/health curl "http://localhost:3001/search?q=python&num=3" ``` Then point your LLM client at `http://localhost:3001/sse` (Google) and/or `http://localhost:3002/sse` (DuckDuckGo). Client configuration examples for Claude Desktop, Cursor/Windsurf, and VS Code are in [USAGE.md](USAGE.md). ## Tool Contract ```json { "name": "google_search", "arguments": { "query": "Python programming language", "num_results": 5 } } ``` Returns numbered results with title, URL, and snippet (max 20 results, queries capped at 500 characters). ## Configuration | Variable | Default | Description | |----------------------|----------------------|------------------------------------------| | `SEARXNG_SECRET` | — (required) | SearXNG session/CSRF secret | | `RATE_LIMIT_SECONDS` | 5 / 3 | Minimum seconds between search requests | | `MCP_PORT` | 3001 / 3002 | Port the MCP server listens on | | `SEARXNG_URL` | `http://searxng:8080`| URL of the SearXNG instance | ## Security Model - **Internal network** — SearXNG is on a `internal: true` Docker network; only the two MCP containers can reach it. It is not published to the host. - **Per-IP rate limiting** — HTTP endpoints allow 30 requests/minute per IP (`/health` exempt); excess requests get `429`. - **Request pacing** — each server also enforces a minimum interval between upstream SearXNG queries to avoid throttling. - **No auth on `/search` and `/sse`** — the servers assume they are bound to a trusted network. Expose them publicly only behind an auth proxy. - **Input validation** — query length capped, result counts clamped to 1..20, malformed parameters return `400` instead of crashing. ## Optional: Playwright Scraping Fallback `lib/google_search.py` and `lib/duckduckgo_search.py` contain a legacy Playwright-based scraping path (single shared browser, stealth user agent, HTML parsing with multiple fallback strategies). It is **not** used by the production servers — Google aggressively blocks headless browsers and SearXNG is the working path. If you want to run the scraper directly: ```bash pip install playwright playwright install chromium ``` ## Development ```bash python3 -m venv .venv && source .venv/bin/activate pip install -e ".[dev]" ruff check . pytest tests/ -v # End-to-end test (requires running containers) python3 integration_test.py ``` ## Project Structure ``` ├── docker-compose.yml # All 3 services ├── searxng-settings.yml # SearXNG engine config ├── google-mcp/ │ ├── Dockerfile │ └── server.py # Google MCP server (SearXNG-backed) ├── duckduckgo-mcp/ │ ├── Dockerfile │ └── server.py # DuckDuckGo MCP server (SearXNG-backed) ├── lib/ │ ├── playwright_manager.py # Single-instance browser manager (optional path) │ ├── rate_limiter.py # Async minimum-interval rate limiter │ ├── google_search.py # Playwright Google scraper (optional path) │ └── duckduckgo_search.py # Playwright DuckDuckGo scraper (optional path) ├── tests/ │ ├── test_rate_limiter.py │ └── test_search_parsing.py ├── integration_test.py # End-to-end MCP SSE test └── test_client.py # Manual stdio/SSE smoke client ``` ## License [MIT](LICENSE)