fix(mcp): trust the deployed hostname under streamable-http
FastMCP enables DNS rebinding protection inside __init__ when it is built with
a loopback bind address, and create_server() takes the default 127.0.0.1:
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]:*"],
)
main() reassigns settings.host afterwards, which never revisits that decision,
so the allow-list stayed loopback-only. Any client reaching the server under a
real hostname got 421 Misdirected Request with "Invalid Host header". It reads
as a hang rather than a rejection, because clients retry until they time out --
bifrost reported fin as simply disconnected after exhausting its retries.
Rebuild the settings after the host is known. Protection stays on; the loopback
patterns are kept and FIN_MCP_ALLOWED_HOSTS adds to them (comma separated,
"host:*" allows any port). Unlisted hosts are still refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTuguQqazhoWUsh2GkPg9a
This commit is contained in:
1 parent
631bdbb13f
commit
43c90dcd43
2 files changed
+24
No files matched your search
@@ -19,6 +19,7 @@ from typing import Any, Literal
|
||||
|
||||
try:
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from mcp.types import ToolAnnotations
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
@@ -1107,4 +1108,24 @@ def main() -> None:
|
||||
# self-contained rather than pinning clients to one replica.
|
||||
mcp.settings.stateless_http = True
|
||||
|
||||
# FastMCP turns on DNS rebinding protection during __init__ when it is
|
||||
# built with a loopback bind address, and create_server() takes the
|
||||
# default 127.0.0.1. Reassigning settings.host above does not revisit
|
||||
# that decision, so the allow-list stays loopback-only and any client
|
||||
# reaching us under a real hostname gets a 421 with "Invalid Host
|
||||
# header" -- which reads as a hang, because clients retry until they
|
||||
# time out. Rebuild the settings here so the deployed hostname is
|
||||
# trusted. FIN_MCP_ALLOWED_HOSTS is a comma-separated list and accepts
|
||||
# "host:*" to allow any port.
|
||||
allowed_hosts = ["127.0.0.1:*", "localhost:*", "[::1]:*"]
|
||||
allowed_hosts += [
|
||||
host.strip()
|
||||
for host in os.getenv("FIN_MCP_ALLOWED_HOSTS", "").split(",")
|
||||
if host.strip()
|
||||
]
|
||||
mcp.settings.transport_security = TransportSecuritySettings(
|
||||
enable_dns_rebinding_protection=True,
|
||||
allowed_hosts=allowed_hosts,
|
||||
)
|
||||
|
||||
mcp.run(transport=transport)
|
||||
@@ -23,6 +23,9 @@ services:
|
||||
- FIN_MCP_PORT=8000
|
||||
- FIN_MCP_PATH=/mcp
|
||||
- FIN_MCP_ALLOW_WRITES=1
|
||||
# Clients reach this service as "mcp" on the compose network, and FastMCP
|
||||
# rejects any Host it was not told to trust with a 421.
|
||||
- FIN_MCP_ALLOWED_HOSTS=mcp:*
|
||||
# No ports mapping on purpose: the API has no auth, so the MCP surface
|
||||
# stays reachable only from inside the compose network. Add a mapping
|
||||
# only if you also set FIN_API_TOKEN.
|
||||
|
||||
Reference in new issue
Block a user