web-search-mcp
README.md
# web-search-mcp
An MCP server that gives IBM BOB live web search via Gemini's built-in grounding,
registered as an upstream on the LiteLLM MCP Gateway.
```
BOB ──► LiteLLM proxy (MCP Gateway) ──► this server ──► LiteLLM proxy ──► Gemini (grounding)
```
LiteLLM connects to this server as an MCP client and re-exposes its tool at LiteLLM's
own `/mcp` endpoint, so BOB gains search through a config entry rather than a code change.
Instead of returning raw search results, the tool calls a Gemini model through the
LiteLLM proxy with the `googleSearchRetrieval` grounding tool. Gemini searches the web
internally and returns a synthesised answer with source citations.
One tool is exposed:
| Tool | Arguments | Returns |
|---|---|---|
| `web_search` | `query` | `{query, answer, sources[{title, url}]}` |
## 1. Credentials
Three values are required; the server refuses to start without them.
1. **Gemini API key** — from [aistudio.google.com](https://aistudio.google.com) → **Get API key**
(starts with `AIza`). Set in your LiteLLM config as `GEMINI_API_KEY`. → used by LiteLLM, not this server directly.
2. **LiteLLM API key** — the master key set in your LiteLLM proxy config. → `LITELLM_API_KEY`
3. **MCP auth token** — a shared secret you generate yourself:
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
→ `MCP_AUTH_TOKEN` (this server) and `WEB_SEARCH_MCP_TOKEN` (LiteLLM env)
## 2. Run it locally
```bash
cp .env.example .env # fill in LITELLM_BASE_URL, LITELLM_API_KEY, MCP_AUTH_TOKEN
uv sync
uv run web-search-mcp
```
In a second terminal, start the LiteLLM proxy (run from the project root):
```bash
export GEMINI_API_KEY=AIzaSy...
export WEB_SEARCH_MCP_TOKEN=<same value as MCP_AUTH_TOKEN>
uvx --from 'litellm[proxy]==1.94.1' litellm --config litellm/config.example.yaml
```
Verify the MCP server:
```bash
curl -s localhost:8000/healthz # {"status":"ok"} -- no auth needed
curl -i localhost:8000/mcp # 401, because no token was sent
```
Then inspect the tool interactively:
```bash
npx @modelcontextprotocol/inspector
```
Connect with transport **Streamable HTTP** to `http://127.0.0.1:8000/mcp`, add header
`Authorization: Bearer <MCP_AUTH_TOKEN>`, then call `web_search` from the Tools tab.
## 3. Register with LiteLLM
Merge the `mcp_servers` block from [`litellm/config.example.yaml`](litellm/config.example.yaml)
into your LiteLLM config:
```yaml
mcp_servers:
web_search:
url: "https://your-host/mcp"
transport: http
auth_type: bearer_token
auth_value: os.environ/WEB_SEARCH_MCP_TOKEN
allowed_tools: ["web_search"]
```
`web_search` should appear in LiteLLM's tool list (UI → **MCP Servers**, or through
the proxy's `/mcp` endpoint). Then ask BOB something that requires current information
and check that it calls the tool and cites URLs.
## 4. Deploy
```bash
docker build -t web-search-mcp .
docker run -p 8000:8000 --env-file .env web-search-mcp
```
The image is platform-agnostic — Code Engine, OpenShift, Kubernetes, Cloud Run. It
binds `0.0.0.0` and honours an injected `PORT`. Point `/healthz` at your platform's
liveness probe; it is intentionally unauthenticated so probes work without a token.
Supply `LITELLM_BASE_URL`, `LITELLM_API_KEY`, and `MCP_AUTH_TOKEN` as platform secrets,
never baked into the image.
## Layout
```
src/web_search_mcp/
config.py environment settings; fails fast when required values are missing
gemini_client.py LiteLLM/Gemini grounding client -- knows Gemini, not MCP
cache.py TTL/LRU cache to avoid redundant Gemini calls
server.py MCPServer, the web_search tool, /healthz, ASGI assembly
auth.py bearer-token ASGI middleware
__main__.py uvicorn entrypoint
```
Run the tests (fully mocked — no network, no API calls):
```bash
uv run pytest
```
## Notes for whoever maintains this
- **Gemini grounding returns an answer, not a list of results.** The tool response shape
is `{answer, sources}` rather than `{results[]}`. Bob reads the answer and cites the
source URLs directly.
- **`sources` may be empty.** Gemini sometimes omits grounding citations even when it
used search internally. This is not an error.
- **Error messages tell the model whether retrying can help.** A rate limit says wait;
a bad API key says an operator must intervene; a proxy timeout says retry shortly.
- **Auth middleware is pure ASGI**, not Starlette's `BaseHTTPMiddleware`, because the MCP
endpoint streams and `BaseHTTPMiddleware` buffers.
- **`streamable_http_app()` is used directly, not mounted.** It already serves at `/mcp`
and runs its own session-manager lifespan; mounting it under `/mcp` would nest the path
to `/mcp/mcp` and drop that lifespan.
- **LiteLLM version:** the proxy has a known incompatibility with `mcp>=2.0` so it cannot
be installed in the same venv as this server. Use `uvx --from 'litellm[proxy]==1.94.1'`
to run it in isolation. Version 1.95.0 has a FastAPI import bug; stay on 1.94.1 until
that is resolved upstream.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues