feat(mcp): OMNIVOICE_MCP_ALLOWED_HOSTS — configurable host allowlist for MCP transport security (#1249) (#1250)
* feat(mcp): OMNIVOICE_MCP_ALLOWED_HOSTS env var for transport-security allowlist (#1249) Agents running in Docker containers (or on other machines) connect via a hostname like host.containers.internal, which the MCP SDK's DNS-rebinding guard rejects with 421. Add OMNIVOICE_MCP_ALLOWED_HOSTS (comma-separated host patterns) that extends both allowed_hosts and allowed_origins in create_mcp_server(). Default empty → no behavior change. Test: assert the env var extends the allowlist + origins. Docs: mcp.md notes the env var for Docker/LAN agents. * fix(changelog): move MCP_ALLOWED_HOSTS entry after Highlights per quiet style * fix(mcp): add https:// origins for HTTPS reverse proxy clients (greptile P1) * docs(mcp): add security note for remote agent connections (coderabbit)
This commit is contained in:
@@ -23,6 +23,10 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
|
||||
|
||||
- First run: the status bar (Logs, version, Sponsors) appears once you reach the studio, instead of overlaying the setup steps (#1241)
|
||||
|
||||
### Added
|
||||
|
||||
- `OMNIVOICE_MCP_ALLOWED_HOSTS` — comma-separated host patterns (e.g. `host.containers.internal:*,192.168.1.5:*`) that extend the MCP SDK's DNS-rebinding allowlist, so AI agents running in Docker containers or on other machines can reach the `/mcp` endpoint. The SDK default is localhost-only; this env var is opt-in (#1249)
|
||||
|
||||
### Docs
|
||||
|
||||
- Docker: ROCm section explains that `torch.cuda.is_available() == True` isn't proof the app is on the GPU, and notes the `--group-add` needed for `/dev/kfd` on rootless hosts (#1228)
|
||||
|
||||
@@ -114,6 +114,24 @@ def create_mcp_server():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Extend the MCP SDK's DNS-rebinding allowlist so agents on non-localhost
|
||||
# hosts (Docker's host.containers.internal, a LAN IP, a reverse proxy) can
|
||||
# reach the /mcp endpoint. The SDK default is localhost-only.
|
||||
_mcp_hosts = os.environ.get("OMNIVOICE_MCP_ALLOWED_HOSTS", "")
|
||||
if _mcp_hosts.strip():
|
||||
hosts = [h.strip() for h in _mcp_hosts.split(",") if h.strip()]
|
||||
try:
|
||||
mcp.settings.transport_security.allowed_hosts.extend(hosts)
|
||||
# Also extend origins for both http and https (browser-based MCP
|
||||
# clients behind a proxy send an Origin header — agent clients
|
||||
# typically don't, but a reverse proxy may use either scheme).
|
||||
origins = [
|
||||
f"{scheme}://{h}" for h in hosts for scheme in ("http", "https")
|
||||
]
|
||||
mcp.settings.transport_security.allowed_origins.extend(origins)
|
||||
except Exception as e:
|
||||
logger.warning("OMNIVOICE_MCP_ALLOWED_HOSTS not applied (%s)", e)
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
def _api_base() -> str:
|
||||
|
||||
@@ -30,6 +30,14 @@ To bind this agent to a specific voice, send an
|
||||
`X-OmniVoice-Client-Id` header (e.g. `claude-code`). See
|
||||
[per-agent voices](#per-agent-voices).
|
||||
|
||||
**Agents in Docker or on another machine:** the MCP SDK rejects non-localhost
|
||||
Host headers by default (DNS-rebinding guard). Set
|
||||
`OMNIVOICE_MCP_ALLOWED_HOSTS` to a comma-separated list of host patterns the
|
||||
agent connects from (e.g. `host.containers.internal:*,192.168.1.50:*`).
|
||||
Keep this on a trusted LAN or behind TLS (Tailscale Serve, a reverse proxy
|
||||
with HTTPS) — the MCP transport is not authenticated, so don't expose it on
|
||||
the open internet.
|
||||
|
||||
### stdio (clients that only speak stdio)
|
||||
|
||||
Use the bundled shim — it proxies stdio ↔ the mounted HTTP endpoint. Drop
|
||||
|
||||
@@ -103,3 +103,17 @@ def test_decode_ref_audio_rejects_garbage_without_raising():
|
||||
def test_sniff_audio_ext_matches_magic_bytes(raw, ext):
|
||||
from mcp_server import _sniff_audio_ext
|
||||
assert _sniff_audio_ext(raw) == ext
|
||||
|
||||
|
||||
def test_mcp_allowed_hosts_env_extends_allowlist(monkeypatch):
|
||||
"""OMNIVOICE_MCP_ALLOWED_HOSTS must extend the transport-security allowlist."""
|
||||
from mcp_server import create_mcp_server
|
||||
|
||||
monkeypatch.setenv("OMNIVOICE_MCP_ALLOWED_HOSTS", "host.containers.internal:*,10.0.0.1:*")
|
||||
server = create_mcp_server()
|
||||
allowed = server.settings.transport_security.allowed_hosts
|
||||
assert "host.containers.internal:*" in allowed
|
||||
assert "10.0.0.1:*" in allowed
|
||||
origins = server.settings.transport_security.allowed_origins
|
||||
assert "http://host.containers.internal:*" in origins
|
||||
assert "https://host.containers.internal:*" in origins
|
||||
|
||||
Reference in New Issue
Block a user