Skip to content

Commit 58acdbb

Browse files
committed
fix(mcp): pass bind host to streamable_http_app and close caller-owned http client
- runtime.py: pass host=server_config.host to streamable_http_app(). Without it the v2 SDK defaults host to 127.0.0.1 and auto-applies a localhost-only DNS rebinding allowlist when transport_security is None, causing HTTP 421 on every non-localhost request for servers bound to 0.0.0.0. - client.py: enter the caller-provided httpx.AsyncClient into the exit stack. The v2 SDK does not close a caller-provided client, so it leaked on every connection attempt. Entered before the transport so LIFO teardown fires the terminate_on_close DELETE while the client is still open. - test_runtime_coverage.py: set _transport_security on object.__new__ servers that bypass __init__, now required after dropping the getattr fallback. Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
1 parent 7a72951 commit 58acdbb

3 files changed

Lines changed: 14 additions & 1 deletion

File tree

cpex/framework/external/mcp/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,11 @@ def _tls_httpx_client_factory(
376376
for attempt in range(max_retries):
377377
try:
378378
http_client = _tls_httpx_client_factory()
379+
# The v2 SDK does not close a caller-provided http_client, so we own its
380+
# lifecycle. Enter it into the exit stack first (before the transport) so
381+
# LIFO teardown closes the transport — firing the terminate_on_close DELETE
382+
# while the client is still open — and then closes the client itself.
383+
await self._exit_stack.enter_async_context(http_client)
379384
streamable_client = streamable_http_client(
380385
uri, http_client=http_client, terminate_on_close=True
381386
)

cpex/framework/external/mcp/server/runtime.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,10 @@ async def run_streamable_http_async(self) -> None:
387387
>>> server.server_config.port
388388
9000
389389
"""
390-
starlette_app = self.streamable_http_app(transport_security=self._transport_security)
390+
starlette_app = self.streamable_http_app(
391+
transport_security=self._transport_security,
392+
host=self.server_config.host,
393+
)
391394

392395
# Add health check endpoint to main app
393396
# Third-Party

tests/unit/cpex/framework/external/mcp/server/test_runtime_coverage.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ async def test_with_uds(self, tmp_path, monkeypatch):
117117
server = object.__new__(runtime.SSLCapableMCPServer)
118118
server.server_config = config
119119
server.settings = SimpleNamespace(host="127.0.0.1", port=8000, log_level="info")
120+
server._transport_security = None
120121
server.streamable_http_app = lambda **kwargs: SimpleNamespace(routes=[])
121122

122123
monkeypatch.setattr(runtime.SSLCapableMCPServer, "_get_ssl_config", lambda self: {})
@@ -154,6 +155,7 @@ async def test_no_ssl(self, monkeypatch):
154155
server = object.__new__(runtime.SSLCapableMCPServer)
155156
server.server_config = config
156157
server.settings = SimpleNamespace(host="127.0.0.1", port=8000, log_level="info")
158+
server._transport_security = None
157159
server.streamable_http_app = lambda **kwargs: SimpleNamespace(routes=[])
158160

159161
monkeypatch.setattr(runtime.SSLCapableMCPServer, "_get_ssl_config", lambda self: {})
@@ -183,6 +185,7 @@ async def test_metrics_disabled(self, monkeypatch):
183185

184186
routes_added = []
185187
app = SimpleNamespace(routes=routes_added)
188+
server._transport_security = None
186189
server.streamable_http_app = lambda **kwargs: app
187190

188191
monkeypatch.setattr(runtime.SSLCapableMCPServer, "_get_ssl_config", lambda self: {})
@@ -284,6 +287,7 @@ async def test_metrics_enabled_executes_routes(self, monkeypatch):
284287
server = object.__new__(runtime.SSLCapableMCPServer)
285288
server.server_config = config
286289
server.settings = SimpleNamespace(host="127.0.0.1", port=8000, log_level="INFO")
290+
server._transport_security = None
287291
server.streamable_http_app = lambda **kwargs: SimpleNamespace(routes=[])
288292

289293
monkeypatch.setenv("ENABLE_METRICS", "true")
@@ -315,6 +319,7 @@ async def test_metrics_disabled_executes_route(self, monkeypatch):
315319
server = object.__new__(runtime.SSLCapableMCPServer)
316320
server.server_config = config
317321
server.settings = SimpleNamespace(host="127.0.0.1", port=8000, log_level="INFO")
322+
server._transport_security = None
318323
server.streamable_http_app = lambda **kwargs: SimpleNamespace(routes=[])
319324

320325
monkeypatch.setenv("ENABLE_METRICS", "false")

0 commit comments

Comments
 (0)