Summary
With WHATSAPP_MCP_TRANSPORT=http and WHATSAPP_MCP_HOST=0.0.0.0, the server starts and logs that it is listening on 0.0.0.0:8000 — but rejects every request that isn't addressed to loopback:
WARNING Invalid Host header: whatsapp-mcp.homelab.svc.cluster.local:8000 transport_security.py:71
INFO: 10.52.0.67:38536 - "GET /mcp HTTP/1.1" 421 Misdirected Request
Since remote transport is the reason this fork exists over upstream's stdio-only server, this makes the HTTP transport unusable for its main use case: reaching the server by any name other than localhost.
Seen on v0.6.0 (7f518e2) with mcp 1.29.1, running in Kubernetes with a client in another pod.
Cause
The MCP SDK auto-enables DNS-rebinding protection when FastMCP is constructed with the default host, pinning allowed_hosts to loopback (mcp/server/fastmcp/server.py):
if transport_security is None and host in ("127.0.0.1", "localhost", "::1"):
transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"],
allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"],
)
main.py constructs the server at import time, before any env var is read:
# main.py:61
mcp = FastMCP("whatsapp")
and only applies the host afterwards, in __main__:
mcp.settings.host = resolve_host(os.getenv("WHATSAPP_MCP_HOST"))
mcp.settings.port = resolve_port(os.getenv("WHATSAPP_MCP_PORT"))
Mutating settings.host after the fact does not revisit the allowlist, so a server that ends up bound to 0.0.0.0 still carries the loopback-only policy the constructor chose. run() reads self.settings.transport_security lazily, so the value set at construction is the one enforced.
The deferred env-var handling is deliberate (the comment above line 61 notes it keeps imports side-effect free), so the fix isn't to move construction — it's to update the security settings alongside host and port.
Reproduce
docker run --rm -e WHATSAPP_MCP_TRANSPORT=http -e WHATSAPP_MCP_HOST=0.0.0.0 -p 8000:8000 <image>
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: example.internal:8000' \
-H 'Accept: text/event-stream' http://127.0.0.1:8000/mcp
# 421
Any Host header other than localhost/127.0.0.1/[::1] reproduces it — a container name, a Kubernetes service DNS name, a LAN hostname, a reverse-proxy Host.
Suggested fix
Mirror what __main__ already does for host and port. Roughly:
if transport != "stdio":
mcp.settings.host = resolve_host(os.getenv("WHATSAPP_MCP_HOST"))
mcp.settings.port = resolve_port(os.getenv("WHATSAPP_MCP_PORT"))
# The SDK picked a loopback-only allowlist at construction time, because
# FastMCP() defaulted to 127.0.0.1. Re-derive it now that the real host
# is known, or the server 421s every non-loopback caller.
allowed = [h.strip() for h in os.getenv("WHATSAPP_MCP_ALLOWED_HOSTS", "").split(",") if h.strip()]
if allowed:
mcp.settings.transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=allowed,
)
elif mcp.settings.host not in ("127.0.0.1", "localhost", "::1"):
mcp.settings.transport_security = TransportSecuritySettings(
enable_dns_rebinding_protection=False,
)
Keeping an explicit WHATSAPP_MCP_ALLOWED_HOSTS is worth it: an operator who has deliberately bound to 0.0.0.0 can still keep rebinding protection on, rather than choosing between "unreachable" and "protection off". Entries accept the SDK's host:* wildcard form.
Whatever shape it takes, it's worth documenting next to WHATSAPP_MCP_HOST — the failure is quiet from the server side (a WARNING line and a 421) and looks like a client or networking problem, not a policy decision.
Workaround
For anyone hitting this before a fix lands: sitecustomize.py on the Python path, which the interpreter imports before main.py runs, so the constructor is patched without touching upstream source:
import os
def _install() -> None:
raw = os.environ.get("MCP_ALLOWED_HOSTS", "").strip()
if not raw:
return
hosts = [h.strip() for h in raw.split(",") if h.strip()]
try:
from mcp.server.fastmcp.server import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
except ImportError:
return
original_init = FastMCP.__init__
def __init__(self, *args, **kwargs):
if kwargs.get("transport_security") is None:
kwargs["transport_security"] = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=hosts,
)
original_init(self, *args, **kwargs)
FastMCP.__init__ = __init__
_install()
Then MCP_ALLOWED_HOSTS="whatsapp-mcp.homelab.svc.cluster.local:*,localhost:*". Verified: allowed hosts complete an MCP initialize handshake, disallowed hosts still get 421, so rebinding protection stays meaningful.
Happy to send a PR if the suggested shape looks right.
Summary
With
WHATSAPP_MCP_TRANSPORT=httpandWHATSAPP_MCP_HOST=0.0.0.0, the server starts and logs that it is listening on0.0.0.0:8000— but rejects every request that isn't addressed to loopback:Since remote transport is the reason this fork exists over upstream's stdio-only server, this makes the HTTP transport unusable for its main use case: reaching the server by any name other than
localhost.Seen on
v0.6.0(7f518e2) withmcp1.29.1, running in Kubernetes with a client in another pod.Cause
The MCP SDK auto-enables DNS-rebinding protection when
FastMCPis constructed with the default host, pinningallowed_hoststo loopback (mcp/server/fastmcp/server.py):main.pyconstructs the server at import time, before any env var is read:and only applies the host afterwards, in
__main__:Mutating
settings.hostafter the fact does not revisit the allowlist, so a server that ends up bound to0.0.0.0still carries the loopback-only policy the constructor chose.run()readsself.settings.transport_securitylazily, so the value set at construction is the one enforced.The deferred env-var handling is deliberate (the comment above line 61 notes it keeps imports side-effect free), so the fix isn't to move construction — it's to update the security settings alongside host and port.
Reproduce
Any Host header other than
localhost/127.0.0.1/[::1]reproduces it — a container name, a Kubernetes service DNS name, a LAN hostname, a reverse-proxyHost.Suggested fix
Mirror what
__main__already does for host and port. Roughly:Keeping an explicit
WHATSAPP_MCP_ALLOWED_HOSTSis worth it: an operator who has deliberately bound to0.0.0.0can still keep rebinding protection on, rather than choosing between "unreachable" and "protection off". Entries accept the SDK'shost:*wildcard form.Whatever shape it takes, it's worth documenting next to
WHATSAPP_MCP_HOST— the failure is quiet from the server side (aWARNINGline and a 421) and looks like a client or networking problem, not a policy decision.Workaround
For anyone hitting this before a fix lands:
sitecustomize.pyon the Python path, which the interpreter imports beforemain.pyruns, so the constructor is patched without touching upstream source:Then
MCP_ALLOWED_HOSTS="whatsapp-mcp.homelab.svc.cluster.local:*,localhost:*". Verified: allowed hosts complete an MCPinitializehandshake, disallowed hosts still get 421, so rebinding protection stays meaningful.Happy to send a PR if the suggested shape looks right.