searxng-mcp
by STIFLEUR390
README.md
# searxng-mcp
MCP (Model Context Protocol) server that plugs a [SearXNG](https://searxng.org) metasearch
instance into AI agents: privacy-friendly **web, news, image and video search**, plus **clean
page content extraction with structural filtering** (no headers, footers, navbars or sidebars
unless the agent asks for them).
[](https://modelcontextprotocol.io)
[](https://www.npmjs.com/package/@aplix39/searxng-mcp)
[](LICENSE)
[](#installation)
## Why
- **Metasearch** — one query fans out to many engines (Google, Bing, Brave, DuckDuckGo,
Wikipedia, news outlets, ...) while your identity stays with *your* instance.
- **Bring your own instance** — works with any SearXNG deployment (local Docker, LAN server,
public instance). The only configuration is the instance URL.
- **Agent-friendly output** — results are rendered as compact structured text, not raw HTML.
- **Content extraction with filtering** — the agent can fetch any page and decide it does *not*
need the site chrome: headers, footers, navigation menus, sidebars, cookie banners, ads and
comment sections are stripped by default and can be re-included per call.
## Tools
| Tool | What it does |
|------|--------------|
| `searxng_search` | General search on any SearXNG category, with language, pagination, time range, SafeSearch, engine selection and domain filters |
| `searxng_search_news` | News search (`categories=news`), friendly with `time_range` |
| `searxng_search_images` | Image search — includes full image URL + thumbnail |
| `searxng_search_videos` | Video search — includes page URL + thumbnail |
| `searxng_autocomplete` | Query suggestions from the instance's autocomplete provider |
| `searxng_config` | Instance overview: categories + enabled engines grouped by category |
| `searxng_extract_content` | Fetch a page and extract readable text with structural filtering |
## Requirements
- **Node.js >= 18** (or **Bun >= 1.x**) on the machine running the MCP server
- A running **SearXNG** instance with the **JSON format enabled**
### Enable JSON output on your SearXNG instance
The API returns 403 unless `json` is in `search.formats`:
```yaml
# settings.yml
search:
formats:
- html
- json
```
Restart SearXNG after editing. Verify with:
```bash
curl 'http://localhost:8888/search?q=test&format=json' | head -c 200
```
## Installation
### npm (global)
```bash
npm install -g @aplix39/searxng-mcp
searxng-mcp --url http://localhost:8888
```
### npx (zero install, run on demand)
```bash
npx --yes @aplix39/searxng-mcp --url http://localhost:8888
```
### Bun (global)
```bash
bun install -g @aplix39/searxng-mcp
searxng-mcp --url http://localhost:8888
```
Or straight from GitHub (the `dist/` bundle is committed, no build step needed):
```bash
bun install -g github:STIFLEUR390/searxng-mcp
```
### From GitHub with npm
npm does not reliably link global binaries from git dependencies (npm 10 keeps the package as
a symlink to an ephemeral cache clone), so pack the repo first, then install the tarball:
```bash
npm pack github:STIFLEUR390/searxng-mcp
npm install -g ./aplix39-searxng-mcp-*.tgz
```
### From source
```bash
git clone https://github.com/STIFLEUR390/searxng-mcp.git
cd searxng-mcp
bun install
bun run build # produces the self-contained dist/index.js
node dist/index.js --url http://localhost:8888
```
> All install flows (npm registry, npx, bun registry, bun GitHub, npm pack+tgz, source) are
> validated against the published **v1.0.0** release, including a Node 18 runtime check.
## Configuration
The SearXNG URL is resolved in this order:
| Precedence | Source | Example |
|-----------|--------|---------|
| 1 | `--url` / `-u` CLI flag | `searxng-mcp --url https://searx.example.org` |
| 2 | `SEARXNG_URL` environment variable | `SEARXNG_URL=https://searx.example.org searxng-mcp` |
| 3 | Built-in default | `http://localhost:8888` |
Trailing slashes and a pasted `/search` suffix are normalized automatically. If the host is
`localhost` and the connection fails (Node resolves it to IPv6 `::1` first), the server retries
once on `127.0.0.1`.
Environment variables:
| Variable | Default | Description |
|----------|---------|-------------|
| `SEARXNG_URL` | `http://localhost:8888` | Base URL of the SearXNG instance (see precedence above) |
| `SEARXNG_TIMEOUT_MS` | `30000` | Per-request timeout in ms (min 500, max 300000) — raise it for slow instances |
CLI flags: `--help`, `--version`.
## Client setup
### Claude Code
```bash
claude mcp add searxng --env SEARXNG_URL=http://localhost:8888 -- npx --yes @aplix39/searxng-mcp
```
### Claude Desktop / Cursor / generic MCP client
```json
{
"mcpServers": {
"searxng": {
"command": "npx",
"args": ["--yes", "@aplix39/searxng-mcp"],
"env": { "SEARXNG_URL": "http://localhost:8888" }
}
}
}
```
With a global install, `command` can simply be `searxng-mcp` with no args (the default URL is
`http://localhost:8888`), or pass `--url` in `args`.
## Tools reference
### `searxng_search`
| Parameter | Type | Description |
|-----------|------|-------------|
| `q` | string (required) | Search query, engine syntax supported |
| `categories` | string[] | e.g. `["general"]`, `["it", "science"]`. Default `["general"]` |
| `language` | string | Language code: `en`, `fr`, `de`, ... |
| `pageno` | int 1–50 | Results page |
| `time_range` | `day` \| `week` \| `month` \| `year` | Restrict to recent results |
| `safesearch` | int 0–2 | 0=off, 1=moderate, 2=strict |
| `engines` | string[] | Restrict to engines by name/shortcut, e.g. `["duckduckgo", "brave"]` |
| `limit` | int 1–50 | Max results rendered (default 10) |
| `include_domains` | string[] | Keep only results on these domains |
| `exclude_domains` | string[] | Drop results on these domains |
Output: numbered results (title, URL, snippet, engines, category, publication date), plus
answers, infoboxes, corrections, related searches and unresponsive-engine notes when present.
`searxng_search_news`, `searxng_search_images` and `searxng_search_videos` are the same tool
pinned to their category (images/videos also surface image + thumbnail URLs).
### `searxng_autocomplete`
| Parameter | Type | Description |
|-----------|------|-------------|
| `q` | string (required) | Partial query |
| `language` | string | Language code for suggestions, e.g. `"fr"`, `"en"` |
### `searxng_config`
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_engines` | bool | `true` | Set `false` for a compact view (counts only, no engine lists) |
Returns categories and enabled engines grouped by category — useful to discover
what the instance can search before picking categories/engines.
### `searxng_extract_content`
Fetches a page (typically a URL found with the search tools) and returns clean, readable text.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `url` | string (required) | — | Absolute http(s) URL |
| `include_header` | bool | `false` | Keep site header/banner blocks |
| `include_footer` | bool | `false` | Keep site footer blocks |
| `include_navigation` | bool | `false` | Keep menus, topbars, breadcrumbs |
| `include_sidebar` | bool | `false` | Keep sidebars / widgets |
| `include_comments` | bool | `false` | Keep comment sections |
| `include_links` | bool | `false` | Render links as markdown `[text](url)` |
| `include_images` | bool | `false` | Render images as markdown `` |
| `content_only` | bool | `true` | Extract only the main content area (`article`/`main`) when detectable |
| `selector` | string | — | Advanced: extract only nodes matching this CSS selector |
| `max_length` | int 500–200000 | `20000` | Truncate the content to this many characters |
| `start_char` | int ≥ 0 | `0` | Start the output window at this character offset (page through long content with `max_length`) |
| `headings_only` | bool | `false` | Return only the heading outline (h1–h6) instead of the full text |
HTML pages go through the structural filtering described below. JSON bodies are returned
pretty-printed in a fenced ```json block, other `text/*` bodies as fenced text; binary files
(PDF, images, archives) are refused with an explicit error.
Always removed regardless of options: `script`, `style`, `noscript`, `iframe`, forms, and noise
banners (cookies, consent, popups, ads, newsletter boxes). The response reports which sections
were removed so the agent can re-include them on the next call.
Sections kept via `include_*` that live outside the main content area (nav/header/footer usually
sit outside `<main>`) are appended to the output, so the opt-in is honored with the default
`content_only` too. With `selector`, extraction stays exact (nothing is appended).
Example: the agent found a page but only wants the article body:
```json
{ "name": "searxng_extract_content", "arguments": { "url": "https://example.org/post/1" } }
```
It needs the footer legal notice too:
```json
{
"name": "searxng_extract_content",
"arguments": { "url": "https://example.org/post/1", "include_footer": true }
}
```
## Example session
> **User:** what are the latest news about SearXNG?
>
> **Agent** → `searxng_search_news { "q": "searxng", "time_range": "month", "limit": 5 }`
>
> **User:** open the first result and summarize it
>
> **Agent** → `searxng_extract_content { "url": "…" }` (header/footer/nav already filtered out)
## Troubleshooting
| Symptom | Cause & fix |
|---------|-------------|
| `403 Forbidden` in tool errors | JSON format disabled → add `json` to `search.formats` in `settings.yml` |
| `429 Too Many Requests` | Instance rate limiter (`server.limiter`) blocking automated calls → adjust `limiter` or retry later |
| `Could not reach SearXNG ... fetch failed` | Instance not running, wrong URL, or IPv6/IPv4 mismatch — the server already retries `localhost` on `127.0.0.1`; otherwise use `http://127.0.0.1:8888` explicitly |
| `content-type ... is not supported` | The extractor handles HTML, JSON and plain text; binary files (PDF, images) are refused |
| Empty results | Some engines may be down on the instance — check `searxng_config` and the `unresponsive_engines` note in results |
## Development
```bash
bun install # install dependencies
bun run dev # run the server from source (TypeScript)
bun test # 106 unit + integration tests (bun:test)
bunx tsc --noEmit # strict typecheck
bun run build # bundle to dist/index.js (node target)
```
Project layout:
```
index.ts # CLI entry point (stdio MCP server)
src/config.ts # CLI/env parsing, URL normalization
src/client.ts # SearXNG HTTP client (/search, /autocompleter, /config)
src/extract.ts # HTML fetch + structural filtering + text rendering
src/format.ts # SearXNG response → agent-friendly text
src/server.ts # MCP tool registration
src/types.ts # SearXNG API types
test/ # bun:test suites
```
## Publishing to npm
> **Note:** the unscoped name `searxng-mcp` is already taken on npm by an unrelated project,
> so this package uses the scoped name **`@aplix39/searxng-mcp`** (scope = the maintainer's
> npm username). Nothing changes for GitHub installs, and the CLI binary stays `searxng-mcp`.
The package is **published**: [npmjs.com/package/@aplix39/searxng-mcp](https://www.npmjs.com/package/@aplix39/searxng-mcp)
(latest: **v1.0.4**, public, MIT). `prepublishOnly` runs typecheck + 106 tests + build on every
publish; the published v1.0.0 tarball was validated byte-identical to the committed `dist/` bundle,
and the published v1.0.3/v1.0.4 tarballs were re-validated the same way (shasum match, `--version` reporting
the published version, isolated `npm i` + MCP handshake). `npm version` now rebuilds `dist/` too, so the tag, the
commit and the published bundle always carry the same version string.
Future releases:
```bash
npm version patch # bumps package.json AND src/version.ts (auto-sync script),
# commits + tags — never bump package.json by hand
git push --follow-tags
npm publish --access public # with a token configured: no interactive 2FA prompt
```
To publish without confirming 2FA in the browser every time, create a token on npmjs.com
(avatar → Access Tokens → Generate New Token → **Granular Access Token**, packages: `@aplix39`
read-and-write; copy it once) and reference it through an environment variable so it never sits
in plaintext in `~/.npmrc`:
```bash
export NPM_TOKEN=npm_xxx # persist in ~/.bashrc or ~/.zshrc
npm config set '//registry.npmjs.org/:_authToken' '${NPM_TOKEN}'
npm whoami # must print the npm username owning the scope
```
If npm still prompts for a 2FA code with a granular token on your account, create a classic
**Automation** token instead (same `npm config set` line): it is designed to bypass publish 2FA.
Keep account 2FA enabled; tokens are expirable and revocable from the same Access Tokens page.
## License
[MIT](LICENSE) © STIFLEUR390
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues