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)
|
- 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
|
### 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)
|
- 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:
|
except Exception:
|
||||||
pass
|
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 ─────────────────────────────────────────────────────────
|
# ── Helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _api_base() -> str:
|
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
|
`X-OmniVoice-Client-Id` header (e.g. `claude-code`). See
|
||||||
[per-agent voices](#per-agent-voices).
|
[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)
|
### stdio (clients that only speak stdio)
|
||||||
|
|
||||||
Use the bundled shim — it proxies stdio ↔ the mounted HTTP endpoint. Drop
|
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):
|
def test_sniff_audio_ext_matches_magic_bytes(raw, ext):
|
||||||
from mcp_server import _sniff_audio_ext
|
from mcp_server import _sniff_audio_ext
|
||||||
assert _sniff_audio_ext(raw) == 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