From ade2b3ff465bca28985429d836d019abcef92b6d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 24 Jun 2026 17:15:40 -0400 Subject: [PATCH] fix: bracket IPv6 hosts in server startup log URL (#4372) --- .../fastmcp/server/mixins/transport.py | 16 ++++++++++++++- tests/server/test_transport.py | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/server/test_transport.py diff --git a/fastmcp_slim/fastmcp/server/mixins/transport.py b/fastmcp_slim/fastmcp/server/mixins/transport.py index 5f9743ca6..4908d22a0 100644 --- a/fastmcp_slim/fastmcp/server/mixins/transport.py +++ b/fastmcp_slim/fastmcp/server/mixins/transport.py @@ -35,6 +35,19 @@ if TYPE_CHECKING: logger = get_logger(__name__) +def _format_host_for_url(host: str) -> str: + """Format a host for inclusion in a URL, bracketing IPv6 addresses. + + A bare IPv6 address like ``::1`` must be wrapped in brackets when placed + before a ``:port`` suffix, otherwise the result (``http://::1:8000``) is an + invalid URL. Hostnames and IPv4 addresses are returned unchanged, as are + addresses that are already bracketed. + """ + if ":" in host and not host.startswith("["): + return f"[{host}]" + return host + + class TransportMixin: """Mixin providing transport-related methods for FastMCP. @@ -301,8 +314,9 @@ class TransportMixin: server = uvicorn.Server(config) path = getattr(app.state, "path", "").lstrip("/") mode = " (stateless)" if stateless_http else "" + display_host = _format_host_for_url(host) logger.info( - f"Starting MCP server {self.name!r} with transport {transport!r}{mode} on http://{host}:{port}/{path}" + f"Starting MCP server {self.name!r} with transport {transport!r}{mode} on http://{display_host}:{port}/{path}" ) if sockets is not None: diff --git a/tests/server/test_transport.py b/tests/server/test_transport.py new file mode 100644 index 000000000..7baa87078 --- /dev/null +++ b/tests/server/test_transport.py @@ -0,0 +1,20 @@ +import pytest + +from fastmcp.server.mixins.transport import _format_host_for_url + + +@pytest.mark.parametrize( + "host, expected", + [ + ("127.0.0.1", "127.0.0.1"), + ("localhost", "localhost"), + ("0.0.0.0", "0.0.0.0"), + ("::1", "[::1]"), + ("::", "[::]"), + ("fe80::1", "[fe80::1]"), + ("[::1]", "[::1]"), + ], +) +def test_format_host_for_url(host: str, expected: str): + """IPv6 hosts are bracketed for use in a URL; everything else is unchanged.""" + assert _format_host_for_url(host) == expected