Skip to content

Commit 6f2139e

Browse files
authored
Merge pull request chigwell#161 from noxlovette/main
Allowed Hosts Configuration
2 parents c8f43ce + 6abf812 commit 6f2139e

4 files changed

Lines changed: 66 additions & 0 deletions

File tree

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ TELEGRAM_SESSION_NAME=telegram_session
4848
# server CLI roots in those cases. Default is deny-all.
4949
# TELEGRAM_ALLOW_SERVER_ROOTS_FALLBACK=1
5050

51+
# --- HTTP/SSE transport security (optional) ---
52+
# Only relevant when MCP_TRANSPORT=http or sse. If the server is reachable via
53+
# a domain (e.g. behind a reverse proxy) rather than only 127.0.0.1/localhost,
54+
# set MCP_ALLOWED_HOSTS to enable DNS-rebinding protection and allow that Host
55+
# header. Comma-separated; supports a ":*" suffix to allow any port.
56+
# MCP_ALLOWED_HOSTS=mcp.example.com
57+
# MCP_ALLOWED_ORIGINS=https://mcp.example.com
58+
5159
# --- Proxy (optional) ---
5260
# Route Telegram traffic through a proxy. Set TELEGRAM_PROXY_TYPE to enable.
5361
# Supported types: socks5, socks4, http, mtproxy.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,13 @@ For `http` and `sse`, the server binds `MCP_HOST`:`MCP_PORT` (default
214214
`127.0.0.1:8765`); the streamable HTTP endpoint is `/mcp`, the SSE endpoint is
215215
`/sse`.
216216

217+
If the server is reachable via a domain (e.g. behind a reverse proxy) rather
218+
than only `127.0.0.1`/`localhost`, set `MCP_ALLOWED_HOSTS` (and optionally
219+
`MCP_ALLOWED_ORIGINS`) to enable DNS-rebinding protection and allow that Host
220+
header, e.g. `MCP_ALLOWED_HOSTS=mcp.example.com`. Comma-separated; supports a
221+
`:*` suffix to allow any port. Left unset, DNS-rebinding protection stays off
222+
(the historical default).
223+
217224
Prefer `http` when more than one MCP client (or many coding-agent sessions)
218225
will use the server: a single long-lived process holds one Telegram
219226
connection, instead of every client spawning its own Telethon session —

telegram_mcp/runner.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ async def _connect_authorized_client(label, client) -> None:
5555
)
5656

5757

58+
def _configure_transport_security() -> None:
59+
"""Wire MCP_ALLOWED_HOSTS/MCP_ALLOWED_ORIGINS into FastMCP's DNS-rebinding
60+
protection, e.g. when the server sits behind a reverse proxy on a public
61+
domain instead of only being reached via 127.0.0.1/localhost.
62+
"""
63+
raw_hosts = os.getenv("MCP_ALLOWED_HOSTS", "")
64+
allowed_hosts = [h.strip() for h in raw_hosts.split(",") if h.strip()]
65+
if not allowed_hosts:
66+
return
67+
68+
from mcp.server.transport_security import TransportSecuritySettings
69+
70+
raw_origins = os.getenv("MCP_ALLOWED_ORIGINS", "")
71+
allowed_origins = [o.strip() for o in raw_origins.split(",") if o.strip()]
72+
73+
mcp.settings.transport_security = TransportSecuritySettings(
74+
enable_dns_rebinding_protection=True,
75+
allowed_hosts=allowed_hosts,
76+
allowed_origins=allowed_origins,
77+
)
78+
79+
5880
async def _serve(transport: str) -> None:
5981
"""Run the MCP server on the selected transport.
6082
@@ -68,6 +90,7 @@ async def _serve(transport: str) -> None:
6890
if transport in ("http", "sse"):
6991
mcp.settings.host = os.getenv("MCP_HOST", "127.0.0.1")
7092
mcp.settings.port = int(os.getenv("MCP_PORT", "8765"))
93+
_configure_transport_security()
7194
if transport == "http":
7295
await mcp.run_streamable_http_async()
7396
else:

tests/test_runner.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class _FakeSettings:
4444
def __init__(self):
4545
self.host = None
4646
self.port = None
47+
self.transport_security = None
4748

4849

4950
class _FakeMcp:
@@ -99,3 +100,30 @@ async def test_serve_http_uses_default_host_and_port(monkeypatch):
99100
assert fake.ran == "http"
100101
assert fake.settings.host == "127.0.0.1"
101102
assert fake.settings.port == 8765
103+
104+
105+
@pytest.mark.asyncio
106+
async def test_serve_http_leaves_transport_security_unset_by_default(monkeypatch):
107+
fake = _FakeMcp()
108+
monkeypatch.setattr(runner, "mcp", fake)
109+
monkeypatch.delenv("MCP_ALLOWED_HOSTS", raising=False)
110+
monkeypatch.delenv("MCP_ALLOWED_ORIGINS", raising=False)
111+
112+
await runner._serve("http")
113+
114+
assert fake.settings.transport_security is None
115+
116+
117+
@pytest.mark.asyncio
118+
async def test_serve_http_configures_allowed_hosts(monkeypatch):
119+
fake = _FakeMcp()
120+
monkeypatch.setattr(runner, "mcp", fake)
121+
monkeypatch.setenv("MCP_ALLOWED_HOSTS", "mcp.example.com, localhost:8765")
122+
monkeypatch.setenv("MCP_ALLOWED_ORIGINS", "https://mcp.example.com")
123+
124+
await runner._serve("http")
125+
126+
security = fake.settings.transport_security
127+
assert security.enable_dns_rebinding_protection is True
128+
assert security.allowed_hosts == ["mcp.example.com", "localhost:8765"]
129+
assert security.allowed_origins == ["https://mcp.example.com"]

0 commit comments

Comments
 (0)