web-search-mcp
by rwese
README.md
# @rwese/web-search-mcp
Web search powered by your own [SearXNG](https://docs.searxng.org/) instance.
Search from the terminal, from an AI agent (MCP), or from pi — every search is
saved as a reusable **session** on disk.
> **New here?** No install needed — run it straight from npm with
> `npx` (see below), set `SEARXNG_URL`, and search. The
> [CLI guide](#cli--usage) covers everything, with example output for every
> command.
Install from the npm registry (bins land on your `PATH`):
```sh
npm install -g @rwese/web-search-mcp
export SEARXNG_URL=https://search.example.com # required
web-search --doctor
```
Or run without installing (subsequent runs reuse the npx cache):
```sh
export SEARXNG_URL=https://search.example.com # required
npx -y -p @rwese/web-search-mcp web-search --doctor
```
Output when your setup is healthy:
```
web-search doctor
✓ config: SEARXNG_URL=https://search.wze.nope.at timeout=10000ms store=/home/you/.local/share/web-search/sessions
✓ store-dir: /home/you/.local/share/web-search/sessions writable
✓ searxng: https://search.wze.nope.at reachable (32 categories, 86 enabled engines)
✓ engines: 86 enabled: wikipedia, arxiv, github, stackoverflow, youtube, ...
✓ search: probe query "test" returned 36 result(s) (3 unresponsive engine(s))
✓ llm: not configured (skipped)
6/6 checks passed
```
> The LLM check only matters for `--use-ai` (see [AI answers](#ai-answers)).
> Without LLM config it reports "skipped" and still passes.
>
> Prefer a local checkout? `git clone` the repo, then `pnpm install` +
> `pnpm build` (or just `npx web-search …` inside the checkout — npx
> resolves the local bins with no install step).
## CLI — usage
After `npm install -g @rwese/web-search-mcp` the `web-search` bin is on your
`PATH`. Without the global install, prefix every command with
`npx -y -p @rwese/web-search-mcp` (fetches the package on first use;
subsequent runs reuse the npx cache):
```sh
export SEARXNG_URL=https://search.example.com # required, once per shell
web-search "<query>" [flags]
```
Bare bin name below stands for that `npx … web-search` prefix:
```
web-search "<query>" [flags] search (AI answer when the LLM is configured, raw with --no-ai)
web-search --session <id> [--json] [--use-ai] re-read a saved session (raw unless --use-ai)
web-search --doctor [--json] validate your setup
web-search --help show help (includes the version)
web-search --version print the version and exit
```
| Flag | Meaning |
| ------------------- | -------------------------------------------------------------- |
| `--categories <csv>`| restrict to categories, e.g. `--categories general,news` |
| `--engines <csv>` | restrict to engines, e.g. `--engines wikipedia,arxiv` |
| `--language <code>` | e.g. `--language en`, `--language de` |
| `--time-range <x>` | `day` \| `month` \| `year` (e.g. `day` for news from the last 24h) |
| `--safesearch <n>` | `0` off, `1` moderate, `2` strict |
| `--page <n>` | result page number |
| `--session <id>` | show a saved session instead of searching (no query allowed) |
| `--use-ai` | answer with the AI loop (default when the LLM is configured) |
| `--no-ai` | list raw results even when the LLM is configured |
| `--debug` | verbose logging to stderr (never pollutes stdout/JSON) |
| `--doctor` | validate setup (takes no query, no other flags except `--json`)|
| `--json` | full structured output (default is readable markdown) |
| `--help` | show help (includes the version) |
| `--version` | print the version and exit |
Exit codes: `0` success (even with zero results), `1` runtime error,
`2` usage error (e.g. query combined with `--session`).
### Search
```sh
web-search "what is kubernetes"
```
Output is readable markdown showing the top 10 hits (the full result set is
always saved — see [sessions](#sessions)):
```markdown
**Session:** cc6a54d5-what-is-kubernetes
## Search results for "what is kubernetes" (27)
1. [Overview - Kubernetes](https://kubernetes.io/docs/concepts/overview/)
Kubernetes is a portable, extensible, open source platform for managing containerized workloads and services ...
*Engines: google cse, braveapi, exaapi · Category: general*
2. [What is Kubernetes? - Red Hat](https://www.redhat.com/en/topics/containers/what-is-kubernetes)
The core concepts of Kubernetes center around clusters, nodes, and pods working together ...
*Engines: google cse, braveapi, exaapi · Category: general*
3. [Kubernetes - Wikipedia](https://en.wikipedia.org/wiki/Kubernetes)
Kubernetes, also known as K8s, is an open-source container orchestration system for automating software deployment, scaling, and management. ...
*Engines: google cse, braveapi, exaapi · Category: general*
```
Refine with filters — all flags combine freely:
```sh
web-search "fusion breakthrough" --categories news --time-range day --language en
web-search "kubernetes ingress" --engines stackoverflow,github --page 2
```
> A query and `--session` are mutually exclusive. `--doctor` takes neither.
### JSON output
Add `--json` to any search for the full structured response — every result
with `title`, `url`, `snippet`, `publishedDate`, `score`, `engines`,
`category`, plus `suggestions`, `answers`, `corrections`, `infoboxes`, and
`unresponsiveEngines`:
```sh
web-search "what is kubernetes" --json
```
```json
{
"query": "what is kubernetes",
"sessionId": "5416a500-what-is-kubernetes",
"results": [
{
"title": "Overview - Kubernetes",
"url": "https://kubernetes.io/docs/concepts/overview/",
"snippet": "Kubernetes is a portable, extensible, open source platform for managing containerized workloads and services ...",
"publishedDate": "2026-05-30T00:00:00+00:00",
"score": 9,
"engines": ["google cse", "braveapi", "exaapi"],
"category": "general"
},
{
"title": "Kubernetes",
"url": "https://kubernetes.io/",
"snippet": "Kubernetes, also known as K8s, is an open source system for automating deployment, scaling, and management of containerized applications. ...",
"publishedDate": null,
"score": 2.7,
"engines": ["google cse", "braveapi", "exaapi"],
"category": "general"
}
],
"suggestions": [],
"answers": [],
"corrections": [],
"infoboxes": [],
"unresponsiveEngines": [
["brave", "Suspended: too many requests"],
["duckduckgo", "CAPTCHA"]
]
}
```
> `unresponsiveEngines` are normal: some backends fail non-fatally on any
> given search. They print as `warning:` lines on stderr and never pollute
> stdout, so `--json` stays machine-readable.
### Sessions
Every search persists an immutable session on disk and prints its id in the
`**Session:**` line. Re-read it any time — from any surface (CLI, MCP, pi):
```sh
NPX="web-search"
$NPX --session cc6a54d5-what-is-kubernetes
$NPX --session cc6a54d5-what-is-kubernetes --json
```
Sessions live under `$XDG_DATA_HOME/web-search/sessions/` (fallback
`~/.local/share/web-search/sessions/`) in folders named
`<8 hex>-<query slug>`, e.g. `cc6a54d5-what-is-kubernetes`.
### AI answers
A plain `web-search "<query>"` answers via the plan → search → aggregate → synthesize
loop whenever the LLM is configured (model + API key). The planner breaks the
request into 1–5 focused search queries and picks shared categories/engines
from your instance's live config. Each query runs in order and saves its own
session. The top 10 results per session are aggregated into a Markdown prompt,
under search-session and search-query headings, then synthesized into one
answer with globally numbered footnote citations.
The markdown output prints every `**Session:**` id and search query plus a
`Raw results: web-search --session <id>` hint so the raw hits stay reviewable.
`--json` carries `sessions` (query/id pairs), `plan` (including `queries`), and
`summary`; `sessionId` remains the first search's id for existing consumers.
Setup (needs an OpenAI-compatible endpoint in addition to SearXNG):
```sh
export OPENAI_BASE_URL=https://litellm.void.cold.at/v1
export OPENAI_MODEL=deepseek-v4-flash
export OPENAI_API_KEY=<key> # env wins; or openai.apiKey in the XDG config file (mode 0600)
web-search "latest pi 5 news" --use-ai
```
```
**Session:** 9be21cc4-latest-pi-5-news
Search query: "latest pi 5 news"
The Raspberry Pi 5 ... [^1] ... [^2]
Sources
[^1]: [Title one](https://example.com/one)
[^2]: [Title two](https://example.com/two)
Raw results: web-search --session 9be21cc4-latest-pi-5-news
```
Pass `--no-ai` for the raw top-10 result list instead, or `--use-ai` to
force the AI answer explicitly. `--session <id>` stays raw unless `--use-ai`
is passed. Explicit flags always override the AI plan, e.g.
`--use-ai --language de --engines wikipedia` forces those choices.
### Debugging
`--debug` (or `WEB_SEARCH_DEBUG=1`) prints verbose diagnostics — request URLs
and timing, session persistence, and with `--use-ai` the plan decision, tool
calls, and model-call counts. It always goes to stderr, so piping stdout to
`jq` keeps working.
## Configuration
Highest precedence first:
1. Environment variables / CLI flags (`SEARXNG_URL`, `SEARXNG_TIMEOUT_MS`, `OPENAI_*`)
2. XDG config file `$XDG_CONFIG_HOME/web-search/config.json`
(fallback `~/.config/web-search/config.json`)
3. Built-in defaults
`$PWD/.env` (gitignored, copy from `.env.example`) is loaded by the CLI/MCP
entrypoints before startup; the core itself never loads dotenv.
| Variable | Required | Default | Purpose |
| -------------------- | -------------- | --------- | --------------------------------------------------- |
| `SEARXNG_URL` | yes | — | SearXNG instance base URL; fails fast without it |
| `SEARXNG_TIMEOUT_MS` | no | `10000` | per-request timeout in ms |
| `WEB_SEARCH_DEBUG` | no | `0` | verbose stderr logging; `1` to enable |
| `WEB_SEARCH_RECURSION_LIMIT` | no | `50` | LangChain graph recursion cap for the `--use-ai` summarizer (default `maxModelCalls * 4 + 10`); raise if you hit "Recursion limit ... reached" |
| `OPENAI_BASE_URL` | for `--use-ai` | — | OpenAI-compatible endpoint |
| `OPENAI_MODEL` | for `--use-ai` | — | model name (e.g. `deepseek-v4-flash`) |
| `OPENAI_API_KEY` | for `--use-ai` | — | key; env wins, `openai.apiKey` in the XDG config file is the fallback (mode 0600, single line) |
| `PORT` | no | `3000` | MCP `--http` port when no port arg is given |
XDG config file example:
```json
{
"searxngUrl": "https://search.example.com",
"timeoutMs": 10000,
"storeDir": "/custom/path/to/sessions",
"debug": false,
"openai": {
"baseUrl": "https://litellm.void.cold.at/v1",
"model": "deepseek-v4-flash",
"apiKey": "sk-..."
}
}
```
Any standard SearXNG instance works — no instance-side setup needed. The core
uses the JSON API plus `/config` (the `--use-ai` planner constrains its
category/engine picks to what `/config` actually offers).
## Agent surfaces (MCP + pi)
All surfaces share the core and the session store, so sessions are
interchangeable. Pick whichever your harness speaks.
**1. MCP via stdio** (Claude Code, opencode, pi):
```json
{
"mcpServers": {
"web-search": {
"command": "npx",
"args": ["-y", "-p", "@rwese/web-search-mcp", "web-search-mcp"],
"env": {
"SEARXNG_URL": "https://search.example.com",
"OPENAI_API_KEY": "sk-..."
}
}
}
}
```
**2. MCP via streamable HTTP** (shared server for multiple agents):
```sh
web-search-mcp --http 3000 # or PORT=3000 ... web-search-mcp --http
# endpoint: POST/GET/DELETE http://localhost:3000/mcp
```
```json
{
"mcpServers": {
"web-search": { "type": "streamable-http", "url": "http://localhost:3000/mcp" }
}
}
```
**3. pi extension** (`package.json` already declares `./extensions` via the
`"pi"` key):
```sh
pi install npm:@rwese/web-search-mcp # then use the search tool
```
Tool contract (`search` — the only tool): required `query: string`; optional
`categories: string[]`, `engines: string[]` (default: all available engines),
`language: string`, `timeRange: day|month|year`, `safeSearch: 0|1|2`,
`pageNo: number`, `maxResults: number` (default 10, max 50). Returns lean
markdown: a `Session:` id line, then `title` / `url` / `snippet` / `engine` /
`category` per result. Agent guidance:
- `maxResults` only trims the rendered summary — the full result set stays in
the persisted session on disk.
- Re-read full detail via the CLI (`--session <id>`) or the session store;
there is no `search_details` MCP tool yet (deferred until the MCP server is
in active use).
- `unresponsiveEngines` are data, not errors — some backends failed
non-fatally.
- For AI-answer behavior from an agent, run the CLI rather than reimplementing
the loop; explicit flags always override the AI plan. When the LLM is
configured the CLI answers with AI by default — pass `--no-ai` for raw
results.
## Development
See [DEVELOPMENT.md](DEVELOPMENT.md) for build commands, architecture, the
`--use-ai` / `--doctor` internals, and contributor pointers.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues