Skip to main content

oxe

Local web-search proxy and cache for AI agents. Exa.ai-compatible HTTP API, StreamableHTTP MCP, DuckDuckGo backend, SQLite TTL cache. ~70 MB RSS. One Python process. No API keys.

MIT Python 3.10+ uv-installable MCP

uv tool install oxe
oxe

Why oxe?

  • Don't burn your Exa/Brave/Serper free tier. Same Exa-shaped HTTP API, served from your own machine, backed by DuckDuckGo. Cached responses are shared between HTTP and MCP — second agent hits /search for python asyncio? It's instant.
  • Two surfaces, one cache. POST /search for HTTP clients; /mcp/ StreamableHTTP transport for Claude Code, Cursor, Hermes, or any MCP-aware agent. Both pull from the same SQLite TTL cache.
  • Your agents see what you explored. Click a result in the web UI; the URL is logged with exa_user_history so future agents know what's already been read.
  • Tiny footprint. ~70 MB steady-state RAM, single uvicorn worker. Runs on a Raspberry Pi.

Install

One-liner (PyPI)

uv tool install oxe

Pinned from GitHub

uv tool install "git+https://github.com/espetro/oxe@v0.1.0"

With mise

# mise.toml
[tools]
"pypi:oxe" = "latest"

From source

git clone https://github.com/espetro/oxe
cd oxe
uv tool install -e .

Run

oxe                                            # foreground
curl http://127.0.0.1:4479/health              # {"status":"ok",...}

Binds to 127.0.0.1:4479 by default. Override with OXE_PORT=8080 oxe.

Open http://127.0.0.1:4479/ for the search UI.

Endpoints

Method Path Purpose
POST /search Exa-compatible search (JSON in, JSON out)
GET /health liveness + cache stats
GET /cache/stats cache row count, hits, db size
POST /cache/invalidate wipe all cached rows
GET / server-rendered HTML search UI
GET /history click history
POST /click record a click (called by the UI)
GET /mcp/ StreamableHTTP MCP transport
GET /docs FastAPI auto-generated OpenAPI
curl -s -X POST http://127.0.0.1:4479/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"python asyncio","numResults":3,"contents":{"text":true,"highlights":true}}' \
  | jq '.results[].title'

MCP handshake

curl -s http://127.0.0.1:4479/mcp/ -X POST \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
  | head

MCP tools

  • exa_search(query, num_results=10, type="auto", contents_highlights=true, contents_text=true, include_domains=[...], exclude_domains=[...], category="") — Exa-shaped search response.
  • exa_user_history(query="", query_hash="", limit=20, since_hours=168) — recent URLs you opened from the web UI for a given query. Call this BEFORE searching if you want to avoid re-researching what you already explored.

Both tools share the same SQLite cache as the HTTP endpoint.

Configuration

All optional. Override via env vars:

Var Default Effect
OXE_PORT 4479 bind port
OXE_CACHE_DIR ~/.cache/oxe SQLite directory
OXE_TTL_DEFAULT 3600 TTL for non-empty results (s)
OXE_TTL_MAX 86400 TTL ceiling (s)
OXE_NEGATIVE_TTL 300 TTL for empty results (s)
OXE_CLICK_RETENTION_DAYS 30 how long to keep click history
OXE_LOG_LEVEL INFO log level

Exa → DuckDuckGo translation notes

DuckDuckGo does not support deep-search variants, summaries, system prompts, or output schemas. Pass-through fields are silently ignored with a server log warning. The following Exa fields are always null for DDG results because DDG doesn't expose them:

  • publishedDate
  • author
  • image

text and highlights are populated only when contents.text=true and contents.highlights=true are requested.

Architecture

┌────────────┐    POST /search    ┌──────────────────────────────┐
│  HTTP CLI  │ ─────────────────▶ │                              │
└────────────┘                    │   oxe (FastAPI + uvicorn)    │
                                  │                              │
┌────────────┐    MCP /mcp/       │  ┌────────────────────────┐  │
│  Claude /  │ ─────────────────▶ │  │  SQLite (WAL) cache    │  │
│  Hermes /  │                    │  │  + ddgs (DuckDuckGo)   │  │
│  Cursor    │                    │  └────────────────────────┘  │
└────────────┘                    │                              │
                                  │  static/app.js (vanilla JS)  │
┌────────────┐  browser           │  + <template> result cards   │
│  You, via  │ ─────▶ /  ────────▶│  + sendBeacon /click         │
│  browser   │                    └──────────────────────────────┘
└────────────┘
  • oxe/cache.py — SQLite WAL, gzip values, TTL eviction.
  • oxe/exa_compat.py — Exa request/response ↔ ddgs translation.
  • oxe/search.py — shared do_search(cache, req) used by HTTP and MCP.
  • oxe/server.py — FastAPI app, all routes, startup click pruner.
  • oxe/mcp_server.pyMCPServer with two tools.
  • oxe/ui.py + oxe/static/{app.js,ui.css} — stdlib-rendered HTML, vanilla JS.

Memory & cost

  • RSS: ~70 MB cold-start, ~27-35 MB steady-state. 200 MB ceiling.
  • Disk: ~/.cache/oxe/cache.db typically <10 MB. WAL file <5 MB.
  • Network: one DuckDuckGo HTML request per unique cache miss. Cached responses replay instantly.
  • Third-party services: none. No API keys, no telemetry.

Run as a service

systemd

# ~/.config/systemd/user/oxe.service
[Unit]
Description=oxe web-search proxy
After=network.target

[Service]
ExecStart=/home/you/.local/bin/oxe
Restart=on-failure
Environment=OXE_PORT=4479

[Install]
WantedBy=default.target
systemctl --user enable --now oxe

launchd (macOS)

<!-- ~/Library/LaunchAgents/local.oxe.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>local.oxe</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/you/.local/bin/oxe</string>
  </array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>OXE_PORT</key><string>4479</string>
  </dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/local.oxe.plist

How is this different from X?

Feature oxe ddgs direct MCP-server competitors
Exa-compatible HTTP API
MCP server
SQLite TTL cache
Click-history tool
Single process n/a
External API key
Web UI

Dependencies

Runtime: ddgs, fastapi, uvicorn, pydantic, mcp. All pulled by uv tool install oxe automatically. No system-level dependencies.

Optional host tools (not required): portless for https://*.localhost/ URLs, oxmgr / systemd / launchd for supervision.

License

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oxe-0.1.1.tar.gz (25.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oxe-0.1.1-py3-none-any.whl (23.9 kB view details)

Uploaded Python 3

File details

Details for the file oxe-0.1.1.tar.gz.

File metadata

  • Download URL: oxe-0.1.1.tar.gz
  • Upload date:
  • Size: 25.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for oxe-0.1.1.tar.gz
Algorithm Hash digest
SHA256 53b900c9fc0627c43478580f9d5f81ee2454ba83ffcfd9a572442097db507cf6
MD5 042a6cb2662e09e9cbb9ad716ee6afc8
BLAKE2b-256 2fbe793318744170a7bda3fab9fcc11dcf3c322af4c4c335251663d28dda6857

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxe-0.1.1.tar.gz:

Publisher: publish-pypi.yml on espetro/oxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oxe-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: oxe-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 23.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for oxe-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 18b61fd88ecd1c4ed6aed5029fac65e0a76b7cfde59d3837fb43f766d343c28e
MD5 e92901c4552790842980fdc09f2a783a
BLAKE2b-256 425b66a1c65f0cc90f4a6004e1b1030d600a9e27839d65ba220a8369ced29425

See more details on using hashes here.

Provenance

The following attestation bundles were made for oxe-0.1.1-py3-none-any.whl:

Publisher: publish-pypi.yml on espetro/oxe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.3

2 files

0.1.2

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page