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:
|
try:
|
||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
from mcp.server.transport_security import TransportSecuritySettings
|
||||||
from mcp.types import ToolAnnotations
|
from mcp.types import ToolAnnotations
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
@@ -1107,4 +1108,24 @@ def main() -> None:
|
|||||||
# self-contained rather than pinning clients to one replica.
|
# self-contained rather than pinning clients to one replica.
|
||||||
mcp.settings.stateless_http = True
|
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)
|
mcp.run(transport=transport)
|
||||||
@@ -23,6 +23,9 @@ services:
|
|||||||
- FIN_MCP_PORT=8000
|
- FIN_MCP_PORT=8000
|
||||||
- FIN_MCP_PATH=/mcp
|
- FIN_MCP_PATH=/mcp
|
||||||
- FIN_MCP_ALLOW_WRITES=1
|
- 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
|
# 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
|
# stays reachable only from inside the compose network. Add a mapping
|
||||||
# only if you also set FIN_API_TOKEN.
|
# only if you also set FIN_API_TOKEN.
|
||||||
|
|||||||
Reference in new issue
Block a user