serper-proxy-mcp
by Edgaras0x4E
README.md
# Serper Search Proxy for OpenWebUI
A proxy that connects [Open WebUI](https://github.com/open-webui/open-webui) to the [Serper](https://serper.dev) Google Search and scrape APIs. One container provides three integrations:
- **Web search** (`POST /search`) - Open WebUI external web search engine
- **Web loader** (`POST /loader`) - Open WebUI external web loader, backed by the Serper scrape API
- **MCP server** (`/mcp`) - Streamable HTTP endpoint with two tools, `google_search` and `google_fetch_webpage`, so the model can search and read pages during a conversation
Open WebUI has a native Serper search integration. This proxy is for setups that also want the Serper scrape API as the web loader and as an MCP fetch tool, through one container and one API key. It also fits network-isolated deployments where Open WebUI cannot reach Serper directly: the proxy runs on a host with internet egress and Open WebUI only needs a route to the proxy.
## How it works
No credentials are stored on the server. Every request to `/search`, `/loader`, and the MCP tools must carry `Authorization: Bearer <your Serper API key>`. The key is forwarded to Serper as the `X-API-KEY` header. If Serper rejects it, `/search` and `/loader` respond with the same 401 or 403 status, and the MCP tools return a tool error carrying that status.
Search responses are normalized into the `[{link, title, snippet}]` format Open WebUI expects: results from `answerBox`, `knowledgeGraph`, `organic`, and `news` are merged, deduplicated by link, and capped at the requested count. `/loader` returns `[{page_content, metadata}]`, with `source` and `title` in the metadata.
## Endpoints
| Method | Path | Purpose | Auth |
| -------- | --------- | ---------------------------------------------- | ------------------------ |
| GET | `/` | Service name and version | none |
| GET | `/health` | Health check | none |
| POST | `/search` | Open WebUI external web search | bearer (Serper API key) |
| POST | `/loader` | Open WebUI external web loader | bearer (Serper API key) |
| POST/GET | `/mcp` | MCP Streamable HTTP endpoint | bearer (Serper API key) |
## Quick start
From [Docker Hub](https://hub.docker.com/r/edgaras0x4e/serper-proxy):
```bash
docker run -d --name serper-proxy -p 8000:8000 edgaras0x4e/serper-proxy
```
Or build from source:
```bash
docker build -t serper-proxy .
docker run -d --name serper-proxy -p 8000:8000 serper-proxy
```
Minimal `docker-compose.yml`:
```yaml
services:
serper-proxy:
image: edgaras0x4e/serper-proxy
ports:
- "8000:8000"
```
Verify:
```bash
curl http://localhost:8000/health
```
## Configuration
All configuration is via environment variables. None are required.
| Variable | Default | Description |
| ------------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `SERPER_ENDPOINT` | `https://google.serper.dev/search` | Serper search API URL |
| `SERPER_SCRAPE_ENDPOINT` | `https://scrape.serper.dev` | Serper scrape API URL |
| `SERPER_TIMEOUT` | `20` | Timeout in seconds for requests to Serper |
| `SERPER_DEFAULT_COUNT` | `10` | Number of search results when the request does not specify one |
| `SERPER_GL` | unset | Country code sent to Serper as `gl` (for example `us`) |
| `SERPER_HL` | unset | Interface language sent to Serper as `hl` (for example `en`) |
| `MCP_ALLOWED_HOSTS` | `serper-proxy:*,localhost:*,127.0.0.1:*` | Comma-separated `Host` values accepted by `/mcp` (DNS-rebinding protection). Must include every hostname clients use to reach `/mcp`. |
## Connecting to Open WebUI
The URLs must be reachable from the Open WebUI container: `serper-proxy:8000` works on a shared Docker network, otherwise use a routable hostname.
### Web search
**Admin Panel > Settings > Web Search**, in the **Search** section:
1. Turn on **Web Search**
2. Set **Web Search Engine** to `external`
3. Set **External Web Search URL** to `http://serper-proxy:8000/search`
4. Set **External Web Search API Key** to your Serper API key
5. Click **Save**
### Web loader
Same tab, in the **Loader** section:
1. Set **Web Loader Engine** to `external`
2. Set **External Web Loader URL** to `http://serper-proxy:8000/loader`
3. Set **External Web Loader API Key** to your Serper API key
4. Click **Save**
### MCP tools
**Admin Panel > Settings > Integrations**, in the **Tools** section (**External Tool Servers**), click **Add Connection**:
1. Set **Type** to **MCP** (Streamable HTTP)
2. Set **URL** to `http://serper-proxy:8000/mcp`
3. Fill in **ID**, which is required for MCP connections, for example `serper`
4. Set **Auth** to **Bearer** and enter your Serper API key as the **API Key**
5. Click **Save**
If Open WebUI reaches the proxy at some other hostname, add it to `MCP_ALLOWED_HOSTS`, otherwise `/mcp` returns 421.
## MCP tools
| Tool | Arguments | Returns |
| ---------------------- | ----------------------------------------------------------- | -------------------------------------------------------------- |
| `google_search` | `query`, `count` (1-100, default `SERPER_DEFAULT_COUNT`) | list of `{link, title, snippet}` |
| `google_fetch_webpage` | `url`, `start_index` (default 0), `max_length` (max 40000) | `{url, title, text, start_index, total_chars, truncated}` |
`google_fetch_webpage` pages through long documents: call it again with `start_index` advanced by `max_length` while `truncated` is `true`.
## Testing the endpoints
```bash
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your Serper API key>" \
-d '{"query": "open webui external search", "count": 5}'
curl -X POST http://localhost:8000/loader \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your Serper API key>" \
-d '{"urls": ["https://example.com/article"]}'
```
## Behavior notes
- `/loader` accepts up to 20 URLs per request. A URL that fails to scrape is skipped and logged so the rest of the batch still loads; 401/403 aborts the batch because every URL would fail the same way. Pages that return no text are skipped.
- Each `/loader` URL and each `google_fetch_webpage` call is one request to the Serper scrape API.
## Local development
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8000
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues