oxe 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
oxe-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 espetro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
oxe-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.4
2
+ Name: oxe
3
+ Version: 0.1.0
4
+ Summary: Local web-search proxy and cache for AI agents. Exa-compatible HTTP API + MCP, DuckDuckGo backend, SQLite TTL.
5
+ Author: espetro
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/espetro/oxe
8
+ Project-URL: Repository, https://github.com/espetro/oxe
9
+ Project-URL: Issues, https://github.com/espetro/oxe/issues
10
+ Keywords: duckduckgo,duckduckgo-search,exa,exa-search,mcp,model-context-protocol,web-search,search-api,cache,proxy,ai-agents,llm-tools,claude
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Internet :: WWW/HTTP
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: ddgs>=9.16.0
26
+ Requires-Dist: fastapi>=0.110
27
+ Requires-Dist: uvicorn>=0.27
28
+ Requires-Dist: pydantic>=2.0
29
+ Requires-Dist: mcp>=2.0
30
+ Dynamic: license-file
31
+
32
+ # oxe
33
+
34
+ **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.
35
+
36
+ [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
37
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
38
+ [![uv-installable](https://img.shields.io/badge/uv-tool%20install-purple.svg)](https://docs.astral.sh/uv/)
39
+ [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io/)
40
+
41
+ ```bash
42
+ uv tool install oxe
43
+ oxe
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Why oxe?
49
+
50
+ - **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.
51
+ - **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.
52
+ - **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.
53
+ - **Tiny footprint.** ~70 MB steady-state RAM, single `uvicorn` worker. Runs on a Raspberry Pi.
54
+
55
+ ## Install
56
+
57
+ ### One-liner (PyPI)
58
+
59
+ ```bash
60
+ uv tool install oxe
61
+ ```
62
+
63
+ ### Pinned from GitHub
64
+
65
+ ```bash
66
+ uv tool install "git+https://github.com/espetro/oxe@v0.1.0"
67
+ ```
68
+
69
+ ### With [mise](https://mise.jdx.dev/)
70
+
71
+ ```toml
72
+ # mise.toml
73
+ [tools]
74
+ "pypi:oxe" = "latest"
75
+ ```
76
+
77
+ ### From source
78
+
79
+ ```bash
80
+ git clone https://github.com/espetro/oxe
81
+ cd oxe
82
+ uv tool install -e .
83
+ ```
84
+
85
+ ## Run
86
+
87
+ ```bash
88
+ oxe # foreground
89
+ curl http://127.0.0.1:4479/health # {"status":"ok",...}
90
+ ```
91
+
92
+ Binds to `127.0.0.1:4479` by default. Override with `OXE_PORT=8080 oxe`.
93
+
94
+ Open `http://127.0.0.1:4479/` for the search UI.
95
+
96
+ ## Endpoints
97
+
98
+ | Method | Path | Purpose |
99
+ |---|---|---|
100
+ | `POST` | `/search` | Exa-compatible search (JSON in, JSON out) |
101
+ | `GET` | `/health` | liveness + cache stats |
102
+ | `GET` | `/cache/stats` | cache row count, hits, db size |
103
+ | `POST` | `/cache/invalidate` | wipe all cached rows |
104
+ | `GET` | `/` | server-rendered HTML search UI |
105
+ | `GET` | `/history` | click history |
106
+ | `POST` | `/click` | record a click (called by the UI) |
107
+ | `GET` | `/mcp/` | StreamableHTTP MCP transport |
108
+ | `GET` | `/docs` | FastAPI auto-generated OpenAPI |
109
+
110
+ ### Quick search
111
+
112
+ ```bash
113
+ curl -s -X POST http://127.0.0.1:4479/search \
114
+ -H 'Content-Type: application/json' \
115
+ -d '{"query":"python asyncio","numResults":3,"contents":{"text":true,"highlights":true}}' \
116
+ | jq '.results[].title'
117
+ ```
118
+
119
+ ### MCP handshake
120
+
121
+ ```bash
122
+ curl -s http://127.0.0.1:4479/mcp/ -X POST \
123
+ -H 'Content-Type: application/json' \
124
+ -H 'Accept: application/json, text/event-stream' \
125
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
126
+ | head
127
+ ```
128
+
129
+ ## MCP tools
130
+
131
+ - **`exa_search(query, num_results=10, type="auto", contents_highlights=true, contents_text=true, include_domains=[...], exclude_domains=[...], category="")`** — Exa-shaped search response.
132
+ - **`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.
133
+
134
+ Both tools share the same SQLite cache as the HTTP endpoint.
135
+
136
+ ## Configuration
137
+
138
+ All optional. Override via env vars:
139
+
140
+ | Var | Default | Effect |
141
+ |---|---|---|
142
+ | `OXE_PORT` | `4479` | bind port |
143
+ | `OXE_CACHE_DIR` | `~/.cache/oxe` | SQLite directory |
144
+ | `OXE_TTL_DEFAULT` | `3600` | TTL for non-empty results (s) |
145
+ | `OXE_TTL_MAX` | `86400` | TTL ceiling (s) |
146
+ | `OXE_NEGATIVE_TTL` | `300` | TTL for empty results (s) |
147
+ | `OXE_CLICK_RETENTION_DAYS` | `30` | how long to keep click history |
148
+ | `OXE_LOG_LEVEL` | `INFO` | log level |
149
+
150
+ ## Exa → DuckDuckGo translation notes
151
+
152
+ DuckDuckGo does not support deep-search variants, summaries, system
153
+ prompts, or output schemas. Pass-through fields are silently ignored
154
+ with a server log warning. The following Exa fields are always
155
+ `null` for DDG results because DDG doesn't expose them:
156
+
157
+ - `publishedDate`
158
+ - `author`
159
+ - `image`
160
+
161
+ `text` and `highlights` are populated only when `contents.text=true`
162
+ and `contents.highlights=true` are requested.
163
+
164
+ ## Architecture
165
+
166
+ ```
167
+ ┌────────────┐ POST /search ┌──────────────────────────────┐
168
+ │ HTTP CLI │ ─────────────────▶ │ │
169
+ └────────────┘ │ oxe (FastAPI + uvicorn) │
170
+ │ │
171
+ ┌────────────┐ MCP /mcp/ │ ┌────────────────────────┐ │
172
+ │ Claude / │ ─────────────────▶ │ │ SQLite (WAL) cache │ │
173
+ │ Hermes / │ │ │ + ddgs (DuckDuckGo) │ │
174
+ │ Cursor │ │ └────────────────────────┘ │
175
+ └────────────┘ │ │
176
+ │ static/app.js (vanilla JS) │
177
+ ┌────────────┐ browser │ + <template> result cards │
178
+ │ You, via │ ─────▶ / ────────▶│ + sendBeacon /click │
179
+ │ browser │ └──────────────────────────────┘
180
+ └────────────┘
181
+ ```
182
+
183
+ - `oxe/cache.py` — SQLite WAL, gzip values, TTL eviction.
184
+ - `oxe/exa_compat.py` — Exa request/response ↔ `ddgs` translation.
185
+ - `oxe/search.py` — shared `do_search(cache, req)` used by HTTP and MCP.
186
+ - `oxe/server.py` — FastAPI app, all routes, startup click pruner.
187
+ - `oxe/mcp_server.py` — `MCPServer` with two tools.
188
+ - `oxe/ui.py` + `oxe/static/{app.js,ui.css}` — stdlib-rendered HTML, vanilla JS.
189
+
190
+ ## Memory & cost
191
+
192
+ - **RSS**: ~70 MB cold-start, ~27-35 MB steady-state. 200 MB ceiling.
193
+ - **Disk**: `~/.cache/oxe/cache.db` typically <10 MB. WAL file `<5 MB`.
194
+ - **Network**: one DuckDuckGo HTML request per unique cache miss. Cached responses replay instantly.
195
+ - **Third-party services**: none. No API keys, no telemetry.
196
+
197
+ ## Run as a service
198
+
199
+ ### systemd
200
+
201
+ ```ini
202
+ # ~/.config/systemd/user/oxe.service
203
+ [Unit]
204
+ Description=oxe web-search proxy
205
+ After=network.target
206
+
207
+ [Service]
208
+ ExecStart=/home/you/.local/bin/oxe
209
+ Restart=on-failure
210
+ Environment=OXE_PORT=4479
211
+
212
+ [Install]
213
+ WantedBy=default.target
214
+ ```
215
+
216
+ ```bash
217
+ systemctl --user enable --now oxe
218
+ ```
219
+
220
+ ### launchd (macOS)
221
+
222
+ ```xml
223
+ <!-- ~/Library/LaunchAgents/local.oxe.plist -->
224
+ <?xml version="1.0" encoding="UTF-8"?>
225
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
226
+ <plist version="1.0">
227
+ <dict>
228
+ <key>Label</key><string>local.oxe</string>
229
+ <key>ProgramArguments</key>
230
+ <array>
231
+ <string>/Users/you/.local/bin/oxe</string>
232
+ </array>
233
+ <key>EnvironmentVariables</key>
234
+ <dict>
235
+ <key>OXE_PORT</key><string>4479</string>
236
+ </dict>
237
+ <key>RunAtLoad</key><true/>
238
+ <key>KeepAlive</key><true/>
239
+ </dict>
240
+ </plist>
241
+ ```
242
+
243
+ ```bash
244
+ launchctl load ~/Library/LaunchAgents/local.oxe.plist
245
+ ```
246
+
247
+ ## How is this different from X?
248
+
249
+ | Feature | oxe | `ddgs` direct | MCP-server competitors |
250
+ |---|---|---|---|
251
+ | Exa-compatible HTTP API | ✅ | ❌ | ❌ |
252
+ | MCP server | ✅ | ❌ | ✅ |
253
+ | SQLite TTL cache | ✅ | ❌ | ❌ |
254
+ | Click-history tool | ✅ | ❌ | ❌ |
255
+ | Single process | ✅ | n/a | ✅ |
256
+ | External API key | ❌ | ❌ | ❌ |
257
+ | Web UI | ✅ | ❌ | ❌ |
258
+
259
+ ## Dependencies
260
+
261
+ Runtime: `ddgs`, `fastapi`, `uvicorn`, `pydantic`, `mcp`. All pulled by `uv tool install oxe` automatically. No system-level dependencies.
262
+
263
+ Optional host tools (not required): `portless` for `https://*.localhost/` URLs, `oxmgr` / `systemd` / `launchd` for supervision.
264
+
265
+ ## License
266
+
267
+ [MIT](LICENSE).
oxe-0.1.0/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # oxe
2
+
3
+ **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.
4
+
5
+ [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
+ [![uv-installable](https://img.shields.io/badge/uv-tool%20install-purple.svg)](https://docs.astral.sh/uv/)
8
+ [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io/)
9
+
10
+ ```bash
11
+ uv tool install oxe
12
+ oxe
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Why oxe?
18
+
19
+ - **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.
20
+ - **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.
21
+ - **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.
22
+ - **Tiny footprint.** ~70 MB steady-state RAM, single `uvicorn` worker. Runs on a Raspberry Pi.
23
+
24
+ ## Install
25
+
26
+ ### One-liner (PyPI)
27
+
28
+ ```bash
29
+ uv tool install oxe
30
+ ```
31
+
32
+ ### Pinned from GitHub
33
+
34
+ ```bash
35
+ uv tool install "git+https://github.com/espetro/oxe@v0.1.0"
36
+ ```
37
+
38
+ ### With [mise](https://mise.jdx.dev/)
39
+
40
+ ```toml
41
+ # mise.toml
42
+ [tools]
43
+ "pypi:oxe" = "latest"
44
+ ```
45
+
46
+ ### From source
47
+
48
+ ```bash
49
+ git clone https://github.com/espetro/oxe
50
+ cd oxe
51
+ uv tool install -e .
52
+ ```
53
+
54
+ ## Run
55
+
56
+ ```bash
57
+ oxe # foreground
58
+ curl http://127.0.0.1:4479/health # {"status":"ok",...}
59
+ ```
60
+
61
+ Binds to `127.0.0.1:4479` by default. Override with `OXE_PORT=8080 oxe`.
62
+
63
+ Open `http://127.0.0.1:4479/` for the search UI.
64
+
65
+ ## Endpoints
66
+
67
+ | Method | Path | Purpose |
68
+ |---|---|---|
69
+ | `POST` | `/search` | Exa-compatible search (JSON in, JSON out) |
70
+ | `GET` | `/health` | liveness + cache stats |
71
+ | `GET` | `/cache/stats` | cache row count, hits, db size |
72
+ | `POST` | `/cache/invalidate` | wipe all cached rows |
73
+ | `GET` | `/` | server-rendered HTML search UI |
74
+ | `GET` | `/history` | click history |
75
+ | `POST` | `/click` | record a click (called by the UI) |
76
+ | `GET` | `/mcp/` | StreamableHTTP MCP transport |
77
+ | `GET` | `/docs` | FastAPI auto-generated OpenAPI |
78
+
79
+ ### Quick search
80
+
81
+ ```bash
82
+ curl -s -X POST http://127.0.0.1:4479/search \
83
+ -H 'Content-Type: application/json' \
84
+ -d '{"query":"python asyncio","numResults":3,"contents":{"text":true,"highlights":true}}' \
85
+ | jq '.results[].title'
86
+ ```
87
+
88
+ ### MCP handshake
89
+
90
+ ```bash
91
+ curl -s http://127.0.0.1:4479/mcp/ -X POST \
92
+ -H 'Content-Type: application/json' \
93
+ -H 'Accept: application/json, text/event-stream' \
94
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
95
+ | head
96
+ ```
97
+
98
+ ## MCP tools
99
+
100
+ - **`exa_search(query, num_results=10, type="auto", contents_highlights=true, contents_text=true, include_domains=[...], exclude_domains=[...], category="")`** — Exa-shaped search response.
101
+ - **`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.
102
+
103
+ Both tools share the same SQLite cache as the HTTP endpoint.
104
+
105
+ ## Configuration
106
+
107
+ All optional. Override via env vars:
108
+
109
+ | Var | Default | Effect |
110
+ |---|---|---|
111
+ | `OXE_PORT` | `4479` | bind port |
112
+ | `OXE_CACHE_DIR` | `~/.cache/oxe` | SQLite directory |
113
+ | `OXE_TTL_DEFAULT` | `3600` | TTL for non-empty results (s) |
114
+ | `OXE_TTL_MAX` | `86400` | TTL ceiling (s) |
115
+ | `OXE_NEGATIVE_TTL` | `300` | TTL for empty results (s) |
116
+ | `OXE_CLICK_RETENTION_DAYS` | `30` | how long to keep click history |
117
+ | `OXE_LOG_LEVEL` | `INFO` | log level |
118
+
119
+ ## Exa → DuckDuckGo translation notes
120
+
121
+ DuckDuckGo does not support deep-search variants, summaries, system
122
+ prompts, or output schemas. Pass-through fields are silently ignored
123
+ with a server log warning. The following Exa fields are always
124
+ `null` for DDG results because DDG doesn't expose them:
125
+
126
+ - `publishedDate`
127
+ - `author`
128
+ - `image`
129
+
130
+ `text` and `highlights` are populated only when `contents.text=true`
131
+ and `contents.highlights=true` are requested.
132
+
133
+ ## Architecture
134
+
135
+ ```
136
+ ┌────────────┐ POST /search ┌──────────────────────────────┐
137
+ │ HTTP CLI │ ─────────────────▶ │ │
138
+ └────────────┘ │ oxe (FastAPI + uvicorn) │
139
+ │ │
140
+ ┌────────────┐ MCP /mcp/ │ ┌────────────────────────┐ │
141
+ │ Claude / │ ─────────────────▶ │ │ SQLite (WAL) cache │ │
142
+ │ Hermes / │ │ │ + ddgs (DuckDuckGo) │ │
143
+ │ Cursor │ │ └────────────────────────┘ │
144
+ └────────────┘ │ │
145
+ │ static/app.js (vanilla JS) │
146
+ ┌────────────┐ browser │ + <template> result cards │
147
+ │ You, via │ ─────▶ / ────────▶│ + sendBeacon /click │
148
+ │ browser │ └──────────────────────────────┘
149
+ └────────────┘
150
+ ```
151
+
152
+ - `oxe/cache.py` — SQLite WAL, gzip values, TTL eviction.
153
+ - `oxe/exa_compat.py` — Exa request/response ↔ `ddgs` translation.
154
+ - `oxe/search.py` — shared `do_search(cache, req)` used by HTTP and MCP.
155
+ - `oxe/server.py` — FastAPI app, all routes, startup click pruner.
156
+ - `oxe/mcp_server.py` — `MCPServer` with two tools.
157
+ - `oxe/ui.py` + `oxe/static/{app.js,ui.css}` — stdlib-rendered HTML, vanilla JS.
158
+
159
+ ## Memory & cost
160
+
161
+ - **RSS**: ~70 MB cold-start, ~27-35 MB steady-state. 200 MB ceiling.
162
+ - **Disk**: `~/.cache/oxe/cache.db` typically <10 MB. WAL file `<5 MB`.
163
+ - **Network**: one DuckDuckGo HTML request per unique cache miss. Cached responses replay instantly.
164
+ - **Third-party services**: none. No API keys, no telemetry.
165
+
166
+ ## Run as a service
167
+
168
+ ### systemd
169
+
170
+ ```ini
171
+ # ~/.config/systemd/user/oxe.service
172
+ [Unit]
173
+ Description=oxe web-search proxy
174
+ After=network.target
175
+
176
+ [Service]
177
+ ExecStart=/home/you/.local/bin/oxe
178
+ Restart=on-failure
179
+ Environment=OXE_PORT=4479
180
+
181
+ [Install]
182
+ WantedBy=default.target
183
+ ```
184
+
185
+ ```bash
186
+ systemctl --user enable --now oxe
187
+ ```
188
+
189
+ ### launchd (macOS)
190
+
191
+ ```xml
192
+ <!-- ~/Library/LaunchAgents/local.oxe.plist -->
193
+ <?xml version="1.0" encoding="UTF-8"?>
194
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
195
+ <plist version="1.0">
196
+ <dict>
197
+ <key>Label</key><string>local.oxe</string>
198
+ <key>ProgramArguments</key>
199
+ <array>
200
+ <string>/Users/you/.local/bin/oxe</string>
201
+ </array>
202
+ <key>EnvironmentVariables</key>
203
+ <dict>
204
+ <key>OXE_PORT</key><string>4479</string>
205
+ </dict>
206
+ <key>RunAtLoad</key><true/>
207
+ <key>KeepAlive</key><true/>
208
+ </dict>
209
+ </plist>
210
+ ```
211
+
212
+ ```bash
213
+ launchctl load ~/Library/LaunchAgents/local.oxe.plist
214
+ ```
215
+
216
+ ## How is this different from X?
217
+
218
+ | Feature | oxe | `ddgs` direct | MCP-server competitors |
219
+ |---|---|---|---|
220
+ | Exa-compatible HTTP API | ✅ | ❌ | ❌ |
221
+ | MCP server | ✅ | ❌ | ✅ |
222
+ | SQLite TTL cache | ✅ | ❌ | ❌ |
223
+ | Click-history tool | ✅ | ❌ | ❌ |
224
+ | Single process | ✅ | n/a | ✅ |
225
+ | External API key | ❌ | ❌ | ❌ |
226
+ | Web UI | ✅ | ❌ | ❌ |
227
+
228
+ ## Dependencies
229
+
230
+ Runtime: `ddgs`, `fastapi`, `uvicorn`, `pydantic`, `mcp`. All pulled by `uv tool install oxe` automatically. No system-level dependencies.
231
+
232
+ Optional host tools (not required): `portless` for `https://*.localhost/` URLs, `oxmgr` / `systemd` / `launchd` for supervision.
233
+
234
+ ## License
235
+
236
+ [MIT](LICENSE).
@@ -0,0 +1,3 @@
1
+ """oxe: Exa-compatible web-search proxy and MCP server backed by DuckDuckGo."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,19 @@
1
+ import os
2
+
3
+ import uvicorn
4
+
5
+ from .server import app
6
+
7
+
8
+ def main() -> None:
9
+ uvicorn.run(
10
+ app,
11
+ host="127.0.0.1",
12
+ port=int(os.getenv("OXE_PORT", "4479")),
13
+ workers=1,
14
+ log_level=os.getenv("OXE_LOG_LEVEL", "info").lower(),
15
+ )
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()