web-search-mcp
README.md
# web-search-mcp
A [FastMCP](https://gofastmcp.com/) server that exposes **free web search** to any
MCP client (Claude Desktop, Claude Code, etc.), backed by a self-hosted
[SearXNG](https://docs.searxng.org/) metasearch instance.
It provides a single tool, `web_search`, which returns a list of
`{title, url, content}` results.
## Why self-host SearXNG?
SearXNG's JSON API (`/search?format=json`) is disabled by default, and virtually
all public instances keep it disabled (they return `403`). This repo runs your
own instance — natively (no Docker) or via Docker — with the JSON format enabled
in `searxng/settings.yml`.
## Prerequisites
- Python 3.10+ (to run the MCP server)
- `git` (to fetch SearXNG for the local run)
- Optionally [`uv`](https://docs.astral.sh/uv/) — handy but not required; you can
use plain `pip` + `python` instead (see below)
## Quickstart (local, no Docker)
### 1. Install SearXNG (one-time)
```bash
cd /home/remon/Documents/web-search-mcp
git clone --depth 1 https://github.com/searxng/searxng.git .searxng-src
python3 -m venv .searxng-src/venv
.searxng-src/venv/bin/pip install -r .searxng-src/requirements.txt
.searxng-src/venv/bin/pip install -e .searxng-src --no-build-isolation
```
> The two-step install matters: SearXNG's build imports the package, so the
> runtime requirements must be present **before** the editable install, hence
> `--no-build-isolation`.
### 2. Start SearXNG
```bash
./run-searxng.sh
```
This serves SearXNG on `http://127.0.0.1:8080` using `searxng/settings.yml`
(which has the JSON API enabled). Leave it running in its own terminal.
Verify the JSON API in another terminal:
```bash
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8080/search?q=fastmcp&format=json'
# expect: 200
```
### 3. Install the MCP server's dependencies
Pick **one** approach:
```bash
# A) plain pip + python (e.g. into your active conda/venv)
pip install -r requirements.txt
# B) uv (creates a managed .venv automatically)
uv sync
```
### 4. Run the MCP server
```bash
# matches whichever you chose above:
python server.py # if you used pip
uv run server.py # if you used uv
# or: fastmcp run server.py:mcp
```
It starts on **stdio** transport and waits for an MCP client — it prints little;
that's normal. By default it talks to `http://localhost:8080`; override with the
`SEARXNG_URL` env var.
> The deps are just `fastmcp` and `httpx` (see `requirements.txt`). With them
> installed in whatever Python is on your `PATH`, plain `python server.py` works
> — `uv` is optional.
### Quick test without an MCP client
With SearXNG running:
```bash
python -c "import asyncio, server; print(asyncio.run(server.web_search('claude code', num_results=3)))"
```
## Alternative: Docker
If you prefer Docker, skip the local install and use the bundled compose file:
```bash
docker compose up -d
```
Then run the MCP server (step 3 above). Same `searxng/settings.yml` is mounted
into the container.
## Connecting an MCP client
Example client config (e.g. Claude Desktop `mcpServers`) — using plain `python`
(deps installed via `pip install -r requirements.txt`):
```json
{
"mcpServers": {
"web-search": {
"command": "python",
"args": ["/home/remon/Documents/web-search-mcp/server.py"],
"env": { "SEARXNG_URL": "http://localhost:8080" }
}
}
}
```
> Use an absolute path to the `python` that has the deps (e.g.
> `/home/remon/miniconda3/bin/python`) if `python` on the client's `PATH` differs.
> Prefer `uv`? Swap to `"command": "uv"`, `"args": ["run", "--directory",
> "/home/remon/Documents/web-search-mcp", "server.py"]`.
For Claude Code (pip/python):
```bash
claude mcp add web-search -e SEARXNG_URL=http://localhost:8080 \
-- python /home/remon/Documents/web-search-mcp/server.py
```
## Connecting coding agents over HTTP (Cursor, Windsurf, …)
Run the server in HTTP mode and point editors at its URL. **Two processes must
be running** (two terminals):
```bash
# Terminal 1 — search backend
./run-searxng.sh # SearXNG on http://127.0.0.1:8080
# Terminal 2 — MCP server over HTTP
./run-mcp.sh # MCP endpoint at http://127.0.0.1:8000/mcp
```
`run-mcp.sh` just sets `MCP_TRANSPORT=http` (port `8000`, path `/mcp`, talking to
`SEARXNG_URL`). Override with `MCP_PORT=… ./run-mcp.sh` if 8000 is taken.
### Cursor — `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project)
```json
{
"mcpServers": {
"web-search": {
"url": "http://127.0.0.1:8000/mcp"
}
}
}
```
### Windsurf — `~/.codeium/windsurf/mcp_config.json`
```json
{
"mcpServers": {
"web-search": {
"serverUrl": "http://127.0.0.1:8000/mcp"
}
}
}
```
> Other clients (Claude Code, Cline, Zed, …) use the same idea — give them the
> URL `http://127.0.0.1:8000/mcp`. For Claude Code:
> `claude mcp add --transport http web-search http://127.0.0.1:8000/mcp`.
> After editing a config, restart / refresh the editor's MCP servers, then ask
> it to search the web — it should call the `web_search` tool.
No auth is needed for a localhost endpoint, so no `headers` block. If you expose
it beyond localhost, put it behind a reverse proxy with a bearer token (see
[DEPLOYMENT.md](DEPLOYMENT.md)) and add a matching `"headers": {"Authorization":
"Bearer …"}` to the client config.
## Using it in a LangGraph / LangChain agent
Bridge the MCP server into LangChain tools with
[`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters),
then hand them to a LangGraph agent. A complete, runnable example is in
[`examples/langgraph_agent.py`](examples/langgraph_agent.py).
```bash
pip install langgraph "langchain[anthropic]" langchain-mcp-adapters
export ANTHROPIC_API_KEY=sk-ant-... # for the Claude model
./run-searxng.sh # SearXNG must be running (separate terminal)
python examples/langgraph_agent.py "what is the model context protocol?"
```
The essence:
```python
from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent
client = MultiServerMCPClient({
"web-search": {
"command": "python",
"args": ["/home/remon/Documents/web-search-mcp/server.py"],
"transport": "stdio", # adapter launches server.py for you
"env": {"SEARXNG_URL": "http://localhost:8080"},
}
})
tools = await client.get_tools() # -> [web_search] as a LangChain tool
agent = create_react_agent(init_chat_model("anthropic:claude-opus-4-8"), tools)
result = await agent.ainvoke({"messages": [{"role": "user", "content": "..."}]})
```
> With `stdio` the adapter spawns `server.py` itself — you only need SearXNG
> running. To point at an already-running HTTP server instead (see
> [DEPLOYMENT.md](DEPLOYMENT.md)), use
> `{"url": "http://localhost:8000/mcp", "transport": "streamable_http"}`.
## Deploying on a server
To run this always-on for remote clients (HTTP transport, TLS, systemd), see
[DEPLOYMENT.md](DEPLOYMENT.md). The server reads `MCP_TRANSPORT=http` plus
`MCP_HOST`/`MCP_PORT`/`MCP_PATH` to switch from local stdio to a remote HTTP
endpoint — no code changes required.
## The `web_search` tool
| Parameter | Type | Default | Notes |
|---------------|-------|---------|--------------------------------------------------|
| `query` | str | — | Search terms (required). |
| `num_results` | int | 5 | Max results to return. |
| `categories` | str | none | e.g. `general`, `news`, `science`, `it`. |
| `language` | str | none | e.g. `en`, `de`, `all`. |
| `time_range` | str | none | `day`, `month`, or `year`. |
| `pageno` | int | 1 | Result page number. |
## Point at a remote instance instead
Any JSON-enabled SearXNG works — no local install needed:
```bash
SEARXNG_URL=https://my-searxng.example.com uv run server.py
```
## Troubleshooting
- **403 from the tool / curl** — JSON format isn't enabled. Confirm
`search.formats` includes `json` in `searxng/settings.yml`, then restart
SearXNG (`./run-searxng.sh` or `docker compose restart`).
- **Connection refused** — SearXNG isn't running, or `SEARXNG_URL` points to the
wrong host/port.
- **`ModuleNotFoundError` during install** — you skipped the requirements step;
run `pip install -r .searxng-src/requirements.txt` first, then the editable
install with `--no-build-isolation`.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues