Skip to content

Commit 9ac127a

Browse files
committed
Extract HTTP bind settings from MCP server construction.
1 parent 711edae commit 9ac127a

3 files changed

Lines changed: 99 additions & 25 deletions

File tree

config/runtime.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Process-level runtime wiring shared by all tool registrations."""
2+
import os
23
from dataclasses import dataclass
34
from typing import Any, Literal, Optional
45

@@ -12,6 +13,42 @@
1213

1314
Transport = Literal["stdio", "streamable-http"]
1415

16+
DEFAULT_HTTP_HOST = "127.0.0.1"
17+
DEFAULT_HTTP_PORT = 8000
18+
DEFAULT_HTTP_PATH = "/mcp"
19+
20+
21+
@dataclass(frozen=True)
22+
class HttpBindSettings:
23+
"""Listen settings used only by the streamable-http transport."""
24+
25+
host: str = DEFAULT_HTTP_HOST
26+
port: int = DEFAULT_HTTP_PORT
27+
streamable_http_path: str = DEFAULT_HTTP_PATH
28+
29+
30+
def resolve_http_bind_settings() -> HttpBindSettings:
31+
"""
32+
Resolve FastMCP bind settings from the environment.
33+
34+
Cloud Run injects PORT; prefer FASTMCP_PORT when set, else PORT, else 8000.
35+
"""
36+
host = os.getenv("FASTMCP_HOST", DEFAULT_HTTP_HOST).strip() or DEFAULT_HTTP_HOST
37+
port_raw = (
38+
os.getenv("FASTMCP_PORT")
39+
or os.getenv("PORT")
40+
or str(DEFAULT_HTTP_PORT)
41+
).strip() or str(DEFAULT_HTTP_PORT)
42+
streamable_http_path = (
43+
os.getenv("FASTMCP_STREAMABLE_HTTP_PATH", DEFAULT_HTTP_PATH).strip()
44+
or DEFAULT_HTTP_PATH
45+
)
46+
return HttpBindSettings(
47+
host=host,
48+
port=int(port_raw),
49+
streamable_http_path=streamable_http_path,
50+
)
51+
1552

1653
@dataclass(frozen=True)
1754
class AppRuntime:

main.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from config.auth import run_streamable_http
1111
from config.perfecto import SECURITY_TOKEN_FILE_ENV_NAME, SECURITY_TOKEN_ENV_NAME, PERFECTO_CLOUD_NAME_ENV_NAME, \
1212
GITHUB
13-
from config.runtime import build_runtime
13+
from config.runtime import build_runtime, resolve_http_bind_settings
1414
from config.token import PerfectoToken, PerfectoTokenError
1515
from config.version import __version__, __executable__, __bundle__, __uvx__, get_version
1616
from server import register_tools
@@ -112,20 +112,6 @@ def build_mcp_server(
112112
transport name (``stdio`` or ``streamable-http``).
113113
"""
114114
init_telemetry("perfecto-mcp", __version__)
115-
host = "127.0.0.1"
116-
port = 8000
117-
streamable_http_path = "/mcp"
118-
if transport == "http":
119-
host = os.getenv("FASTMCP_HOST", "127.0.0.1").strip() or "127.0.0.1"
120-
# Cloud Run injects PORT; prefer FASTMCP_PORT when set, else PORT, else 8000.
121-
port_raw = (
122-
os.getenv("FASTMCP_PORT")
123-
or os.getenv("PORT")
124-
or "8000"
125-
).strip() or "8000"
126-
port = int(port_raw)
127-
streamable_http_path = os.getenv("FASTMCP_STREAMABLE_HTTP_PATH", "/mcp").strip() or "/mcp"
128-
129115
# docker and stdio share process-lifetime credentials; http uses Bearer per request.
130116
wire_transport = to_wire_transport(transport)
131117
app_runtime = build_runtime(
@@ -136,15 +122,19 @@ def build_mcp_server(
136122
# Perfecto MCP Server
137123
138124
"""
139-
mcp = FastMCP(
140-
"perfecto-mcp",
141-
instructions=instructions,
142-
log_level=cast(LOG_LEVELS, log_level),
143-
host=host,
144-
port=port,
145-
streamable_http_path=streamable_http_path,
146-
stateless_http=False,
147-
)
125+
mcp_kwargs: dict = {
126+
"instructions": instructions,
127+
"log_level": cast(LOG_LEVELS, log_level),
128+
}
129+
if transport == "http":
130+
bind = resolve_http_bind_settings()
131+
mcp_kwargs.update(
132+
host=bind.host,
133+
port=bind.port,
134+
streamable_http_path=bind.streamable_http_path,
135+
stateless_http=False,
136+
)
137+
mcp = FastMCP("perfecto-mcp", **mcp_kwargs)
148138
register_tools(mcp, app_runtime)
149139
return mcp, wire_transport
150140

tests/test_main_transport.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import main
44
from config.auth import HttpAuthProvider, StdioAuthProvider
5-
from config.runtime import AppRuntime
5+
from config.runtime import AppRuntime, resolve_http_bind_settings
66

77

88
class _DummyFastMCP:
@@ -47,6 +47,38 @@ def test_maps_logical_transports(self):
4747
assert main.to_wire_transport("docker") == "stdio"
4848

4949

50+
class TestResolveHttpBindSettings:
51+
def test_defaults(self, monkeypatch):
52+
monkeypatch.delenv("FASTMCP_HOST", raising=False)
53+
monkeypatch.delenv("FASTMCP_PORT", raising=False)
54+
monkeypatch.delenv("PORT", raising=False)
55+
monkeypatch.delenv("FASTMCP_STREAMABLE_HTTP_PATH", raising=False)
56+
57+
bind = resolve_http_bind_settings()
58+
59+
assert bind.host == "127.0.0.1"
60+
assert bind.port == 8000
61+
assert bind.streamable_http_path == "/mcp"
62+
63+
def test_env_overrides_and_cloud_run_port_fallback(self, monkeypatch):
64+
monkeypatch.setenv("FASTMCP_HOST", "0.0.0.0")
65+
monkeypatch.setenv("FASTMCP_STREAMABLE_HTTP_PATH", "/custom-mcp")
66+
monkeypatch.delenv("FASTMCP_PORT", raising=False)
67+
monkeypatch.setenv("PORT", "8080")
68+
69+
bind = resolve_http_bind_settings()
70+
71+
assert bind.host == "0.0.0.0"
72+
assert bind.port == 8080
73+
assert bind.streamable_http_path == "/custom-mcp"
74+
75+
def test_fastmcp_port_wins_over_port(self, monkeypatch):
76+
monkeypatch.setenv("FASTMCP_PORT", "8012")
77+
monkeypatch.setenv("PORT", "8080")
78+
79+
assert resolve_http_bind_settings().port == 8012
80+
81+
5082
class TestBuildMcpServerHttp:
5183
def test_http_uses_env_settings_and_stateful_http(self, monkeypatch):
5284
_patch_mcp_server_dependencies(monkeypatch)
@@ -73,6 +105,21 @@ def test_http_falls_back_to_cloud_run_port(self, monkeypatch):
73105

74106
assert mcp.kwargs["port"] == 8080
75107

108+
def test_stdio_and_docker_omit_http_bind_kwargs(self, monkeypatch):
109+
_patch_mcp_server_dependencies(monkeypatch)
110+
monkeypatch.setenv("FASTMCP_HOST", "0.0.0.0")
111+
monkeypatch.setenv("FASTMCP_PORT", "8012")
112+
monkeypatch.setenv("FASTMCP_STREAMABLE_HTTP_PATH", "/custom-mcp")
113+
114+
mcp_stdio, _ = main.build_mcp_server(transport="stdio")
115+
mcp_docker, _ = main.build_mcp_server(transport="docker")
116+
117+
for mcp in (mcp_stdio, mcp_docker):
118+
assert "host" not in mcp.kwargs
119+
assert "port" not in mcp.kwargs
120+
assert "streamable_http_path" not in mcp.kwargs
121+
assert "stateless_http" not in mcp.kwargs
122+
76123

77124
class TestBuildMcpServerTransportMapping:
78125
def test_transport_mapping_keeps_docker_stdio_and_http_streamable(self, monkeypatch):

0 commit comments

Comments
 (0)