A single MCP gateway endpoint that aggregates web/doc/media/browser-automation tools for any agent on your LAN. Built so adding a capability once makes it available to every connected agent — no per-agent installs, no skill drift, no version churn across agents.
Lives on a Proxmox LXC. Exposes one URL. Any MCP-capable agent connects and gets the full toolset.
| Tool | Backed by | What it does |
|---|---|---|
search_web |
SearXNG | Federated meta-search, 30+ engines, no rate limits |
fetch_page |
Crawl4AI (Playwright) | JS-rendered page → clean markdown |
convert_document |
Markitdown | PDF/DOCX/PPTX/XLSX/HTML/image-OCR → markdown |
extract_video |
yt-dlp | Metadata + auto-transcript from YouTube/podcasts/1000+ sites |
download_media |
yt-dlp + Node | Download the real audio/video file (base64). Genuine best stream — no low-quality format 18 fallback |
pdf_extract_text |
Stirling-PDF | Plain text from PDF |
transcribe_audio |
whisper.cpp + Vulkan | GPU STT (AMD 5700 XT). All standard models tiny→large-v3-turbo |
synthesize_speech |
OmniVoice (CPU) | TTS in 600+ languages, optional voice cloning from a 3-10 s sample |
cf_* (17 tools) |
whit3rabbit/camoufox-mcp | Stealth Firefox automation. Use when normal browsers get bot-blocked |
ping |
— | Health check / hot-reload smoke test |
The three media tools split by what they hand back: extract_video → metadata/transcript,
transcribe_audio → text, download_media → the actual file (base64). download_media uses the
container's Node JS runtime so yt-dlp sees full formats (avoids YouTube's degraded format 18).
Speech goes both directions: transcribe_audio for speech→text, synthesize_speech for text→speech.
The asymmetry is deliberate and worth knowing before you plan around it — STT is GPU-accelerated and
faster than real time, TTS runs on CPU and is slower than real time. See the TTS section below.
Browser tools split by job:
fetch_page→ fast Crawl4AI fetch with JS rendering, the 90% casecf_browse→ Camoufox-backed when Crawl4AI gets caught (Cloudflare Turnstile, DataDome, fingerprint walls)cf_browse_session_*→ multi-step flows (click load-more, fill forms, navigate auth)
One FastMCP gateway, multiple backing services on the same Docker network:
agents ── HTTP/MCP ──▶ tb-gateway ──┬──▶ tb-searxng
├──▶ tb-crawl4ai
├──▶ tb-stirling
├──▶ tb-whisper (AMD GPU passthrough)
├──▶ tb-tts (CPU)
└──▶ camoufox-mcp (subprocess, mounted via FastMCP)
Three patterns that earn their keep:
FastMCP mount(create_proxy(...)) — third-party MCP servers (Camoufox-mcp) are spawned as STDIO subprocesses inside the gateway. Their tools surface through our single endpoint with a namespace prefix. We don't reimplement them; we expose them.
Eager init at startup — the gateway calls list_tools() on itself in-process before binding the HTTP server. Forces the proxy subprocess to spawn and register its tools so the first external client connection already sees the full catalog. Without this, the proxy populates lazily and clients miss tools on their initial enumeration.
Health watcher — background task polls a proxy tool every 30s. On two consecutive failures (camoufox subprocess crashed from an unhandledRejection), os._exit(1) triggers Docker's restart: unless-stopped to respawn the gateway. ~10s self-heal per crash.
Prereqs:
- A Proxmox host (or any Docker host)
- An LXC / VM with at least 4 GB RAM, 8 GB+ if running Whisper
- Docker + docker compose v2
- For Whisper GPU: an AMD GPU passed through with
/dev/dri/card*+/dev/dri/renderD128(or skip Whisper)
Steps:
# Clone
git clone <this-repo> toolbox
cd toolbox
# Start the stack (first build ~10-15 min — Whisper builds whisper.cpp with Vulkan,
# Camoufox downloads a 300 MB patched Firefox on first cf_browse call)
docker compose up -d --build
# Wait for gateway to log "Uvicorn running" — eager init takes ~10s
docker compose logs -f gatewayWire it into your agent:
# Claude Code (user scope, available across all projects)
claude mcp add --transport http --scope user toolbox http://<host-ip>:8000/mcp
# Other MCP clients: configure with HTTP transport pointing at http://<host-ip>:8000/mcpThese cost us a lot of rebuilds. Documented so you don't repeat them.
-
pnpm 10 silently breaks native modules.
better-sqlite3(camoufox-mcp transitive dep) needsnode-gypto build its.nodebinding. pnpm 10 blocks lifecycle scripts by default → no postinstall → no compiled binding → cf_browse fails at runtime with "Could not locate the bindings file." Pin pnpm 9. Already done in the Dockerfile; don't bump it without retesting. -
Pin
camoufox-mcp-serverto a known version.@latestcauses transitive-dep drift between rebuilds. We pin2.0.7. -
Don't use
npxto spawn camoufox-mcp from the FastMCP proxy. npx's package cache fragments across container restarts and you'll hit ERR_MODULE_NOT_FOUND. Use the binary that pnpm puts in/usr/local/bin/camoufox-mcp-server. -
No volume mount on
/root/.cache/camoufox. Camoufox-mcp tries tormdirits cache subdir at startup as a cleanup step. Docker volume mount points can't be rmdir'd → EBUSY → unhandledRejection → subprocess death. Let the Firefox binary live in the container writable layer instead; container restart + LXC reboot preserve it. Onlydocker compose up --buildwipes it (and triggers a one-time 300 MB re-fetch). -
Camoufox needs Xvfb. "Virtual headless" mode uses a fake X display for fingerprint-resistance tricks.
apt install xvfbis required even though no display is connected. -
whisper.cpp + Vulkan needs the full SPIR-V chain.
glslc(only in Ubuntu 24.04+ main),spirv-headers,glslang-dev,libshaderc-dev. Useubuntu:24.04as the builder base. Match runtime base for glibc (whisper-cli built on 24.04 needsGLIBC_2.38/GLIBCXX_3.4.32). -
Constrain
cmake --build -jparallelism. Whisper's Vulkan headers are template-heavy — eachcc1pluspeaks at ~3 GB. All-cores parallel will OOM in an 8 GB LXC.-j4keeps it under the cap. Even at-j4this compile can OOM if the other containers are running and eating RAM — so don't trigger a whisper rebuild unless you mean to. The sneaky way to trigger it accidentally isdocker compose up --build gateway: becausegateway depends_on whisper, that rebuilds whisper too. To redeploy only a gateway code change, usedocker compose build gateway && docker compose up -d --no-deps gateway(see Operations). -
TTS is CPU-only because the GPU is AMD. OmniVoice is a PyTorch model with CUDA / Apple MPS / Intel XPU backends and no ROCm path. Don't try to point it at the AMD card — the reason whisper works on that GPU is that whisper.cpp ships hand-written Vulkan compute shaders, which is a completely different approach. The upside is that OmniVoice's dependency list has no
flash-attnand novllm, which is what makes a plain CPU build possible at all; if a future TTS model pins either, it won't run here. -
First call to
cf_browseafter adocker compose up --buildmay fail with "Version information not found at .../version.json" — Firefox download race. Retry once; subsequent calls work.
synthesize_speech wraps OmniVoice — Apache-2.0, 600+
languages, zero-shot voice cloning from a short reference clip.
It runs on CPU, and that is a deliberate trade. OmniVoice ships CUDA, Apple MPS and Intel XPU backends; there is no ROCm path, and this stack's GPU is AMD. The options were "no TTS in the toolbox" or "TTS that is slower than real time." The second is more useful, because what agents actually ask for is narration and voiceover — batch jobs where a few minutes of wall clock is irrelevant. What it is not suitable for is anything interactive or streaming.
Measured on an 8-core CPU allocation, at the default 32 diffusion steps:
| Mode | Real-time factor | 60 s of speech takes |
|---|---|---|
Default voice, steps=16 |
~2.2x | ~2 min |
Default voice, steps=32 |
~4.5x | ~4.5 min |
Cloned voice, steps=32 |
~7x | ~7 min |
Two things that matter more than the raw numbers:
Batch the whole script in one call. Per-call overhead dominates short requests — a 3.5 s line measured 13x real time while a 40 s passage measured 6.9x. Same model, same settings. Synthesising line by line wastes most of the compute.
Pass reference_text when cloning. Without it the model transcribes the reference clip with
Whisper first, which on CPU costs more than the synthesis. With it, cloning is a single pass.
The model is ~3 GB and downloads on first use into the tts-models volume. A cold start (download
included) is ~35 s; afterwards the model stays resident and warm loads are ~1.5 s. Requests are
serialised — the model is not re-entrant, and parallel requests on a CPU box would only contend for
the same cores.
For voice cloning, use a 3-10 second reference in the same language as the text you want spoken. A cross-language reference works, but the output carries the reference language's accent.
# Tail any service
docker compose logs -f gateway # toolbox gateway logs
docker compose logs -f whisper # Whisper inference logs
docker compose logs -f crawl4ai # browser pool
# Restart one service (other tools stay up)
docker compose restart gateway
# Pull latest base images (Stirling/SearXNG/Crawl4AI)
docker compose pull && docker compose up -d
# Rebuild gateway after a code/dep change.
# IMPORTANT: build then recreate with --no-deps. Do NOT use `up --build gateway`:
# gateway `depends_on: whisper`, so `up --build` ALSO rebuilds the whisper image,
# whose whisper.cpp+Vulkan compile OOM-kills cc1plus in an 8 GB LXC (see gotcha #7).
# The gateway image only needs the new server.py copied — a ~3 s rebuild.
docker compose build gateway # builds gateway only, never touches deps
docker compose up -d --no-deps gateway # recreates only the gateway container
# Verify all tools are live
docker exec tb-gateway python -c "
import asyncio
from fastmcp import Client
async def main():
async with Client('http://localhost:8000/mcp') as c:
print(len(await c.list_tools()), 'tools')
asyncio.run(main())
"The scalable pattern. When a capability gap repeats across agents:
- If it needs a new container: add to
docker-compose.ymlon thetoolboxnetwork. - If it's a Python dep: add to
gateway/requirements.txt. - Add a
@mcp.toolfunction togateway/server.py. Write the docstring for a small model — first line is the summary an agent reads to decide whether to call it. docker compose build gateway && docker compose up -d --no-deps gateway(~3 s rebuild + ~5-10 sec downtime, well within MCP reconnect window). Notup --build gateway— that drags in a whisper Vulkan recompile that OOMs the LXC (see Operations note + gotcha #7).- Connected MCP clients pick up the new tool via
tools/list_changednotification or on next refresh. For Claude Code:/reload-plugins.
For wrapping an external MCP server (like we did with Camoufox), mount it through FastMCP's create_proxy() instead of reimplementing. See gateway/server.py for the pattern.
Verified end-to-end:
- LXC
onboot: 1(Proxmox), Docker enabled inside, all containersrestart: unless-stopped - Gateway eager-init repopulates all tools at startup
- Firefox binary persists in container writable layer
- LXC reboot → all tools live in ~10-15 sec, no manual intervention
For wiring an agent to actually USE this toolbox effectively (tool selection rules, workflow recipes, gotchas to tell the agent about), see AGENT_HANDOFF.md. It's written so any MCP-capable agent — Claude Code, Hermes-style harnesses, custom local-model agents — can be onboarded with one document.
MIT