# ADR-032: RVF Private Network MCP Tunnel ## Status Implemented ## Context HuggingFace Chat UI enforces HTTPS-only for MCP server URLs as SSRF protection (`urlSafety.isValidUrl()`). In containerized deployments (Docker Compose), the MCP bridge runs on a private Docker network (`http://mcp-bridge:3001/mcp`) — not exposed to the internet. This creates a conflict: the security control blocks legitimate internal service communication. ## Decision Use an **RVF-inspired private tunnel pattern** — patch the URL safety validation at build time to allow HTTP for admin-configured MCP_SERVERS on the private container network. ### Architecture ``` ┌──────────────────────────────────────────────────────────────┐ │ Private Docker Network │ │ │ │ ┌──────────────┐ HTTP (private) ┌──────────────────────┐ │ │ │ Chat UI │──────────────────►│ MCP Bridge │ │ │ │ :3000 │ MCP JSON-RPC │ :3001 │ │ │ │ │ │ │ │ │ │ RVF Patch: │ /chat/completions│ ├─ /mcp (tools) │ │ │ │ Allow HTTP │──────────────────►│ ├─ /models │ │ │ │ for private │ │ ├─ /chat/completions │ │ │ │ network │ │ └─ /health │ │ │ └──────────────┘ └──────────────────────┘ │ │ │ │ │ │ └──── Not exposed to internet ────────┘ │ └──────────────────────────────────────────────────────────────┘ │ Port 3000 (only this is exposed to host) ``` ### RVF Segment Mapping | RVF Segment | Application | |-------------|-------------| | **WASM_SEG (0x10)** | Lightweight query microkernel — MCP bridge acts as the runtime | | **CRYPTO_SEG (0x0C)** | Request signing between kernel and bridge (optional) | | **META_IDX_SEG (0x0D)** | Tool registry cache in bridge `/models` endpoint | | **KERNEL_SEG (0x0E)** | Docker container as execution boundary | ### Security Model 1. **Private network only** — MCP bridge (`mcp-bridge:3001`) is only reachable within the Docker network, not from the internet 2. **Admin-configured** — `MCP_SERVERS` is set by the deployment operator in `docker-compose.yml`, not by end users 3. **IP safety preserved** — The patch relaxes protocol check (HTTP allowed) but retains all IP safety checks (no internal IP/loopback bypass for user URLs) 4. **Build-time patch** — Applied during Docker image build, not at runtime. Auditable in Dockerfile 5. **Cloud Run unaffected** — Cloud Run deployments use HTTPS URLs and don't need the patch ### Implementation **`chat-ui/patch-mcp-url-safety.sh`**: ```sh # Allow http: protocol for private network MCP sed -i 's/url.protocol !== "https:"/url.protocol !== "https:" \&\& url.protocol !== "http:"/' "$URLSAFETY_FILE" # Allow localhost for container-internal servers sed -i 's/hostname === "localhost"/false \&\& hostname === "localhost"/' "$URLSAFETY_FILE" ``` **`chat-ui/Dockerfile`**: ```dockerfile USER root COPY patch-mcp-url-safety.sh /tmp/patch-mcp-url-safety.sh RUN sh /tmp/patch-mcp-url-safety.sh && rm /tmp/patch-mcp-url-safety.sh USER 1000 ``` **`docker-compose.yml`**: ```yaml chat-ui: environment: MCP_SERVERS: '[{"name":"Tools","url":"http://mcp-bridge:3001/mcp"}]' ``` ## Alternatives Considered 1. **Caddy HTTPS sidecar** — Adds complexity (TLS certs, extra container). Rejected as over-engineered for internal comms. 2. **stdio MCP transport** — HF Chat UI doesn't support command-based MCP (only URL). Not feasible. 3. **Skip MCP, use tool-calling only** — Would lose MCP tool discovery and the tools sidebar in the UI. 4. **Fork HF Chat UI** — Maintenance burden. Build-time sed patch is simpler. ## Consequences - MCP tools work in Docker Compose without HTTPS infrastructure - Build-time patch must be updated if HF Chat UI changes `urlSafety` file naming - Cloud Run deployments are unaffected (use real HTTPS URLs) - Security posture: HTTP only allowed on private Docker network, not on public internet ## Verification ``` [MCP] Loaded 1 server(s): AI Assistant Tools Listening on http://0.0.0.0:3000 Models: gemini-2.5-pro, gemini-2.5-flash, gpt-4.1, gpt-4.1-mini, gpt-4o, gpt-4o-mini, o3-mini, o1-mini Bridge health: ok (3 tools: search, web_research, system_guide) Chat completions: working via Gemini proxy ```