From 6d7c8a03653cd28d96ae75a62c0ceb03074b479e Mon Sep 17 00:00:00 2001 From: Brian Glass Date: Mon, 3 Aug 2026 18:55:36 -0400 Subject: [PATCH] Fix production 421 "Invalid Host header" on /mcp orthocal/asgi.py called mcp.streamable_http_app() with no arguments, so the SDK defaulted host='127.0.0.1' and auto-enabled DNS-rebinding protection restricted to localhost -- fine in dev, but it rejected every real request in production, since Cloud Run's own hostname (forwarded as the Host header by the Firebase Hosting proxy in front of it) isn't on that allowlist. Confirmed via Cloud Run logs showing "Invalid Host header: orthocal-6czswbhara-uc.a.run.app" on every /mcp request since the merge. DNS-rebinding protection defends a server bound to localhost against a malicious webpage's JS reaching it through the browser; it doesn't apply to a public HTTPS API with no localhost-only trust boundary, so disabling it explicitly is the correct fix. Verified locally by replaying the exact Host header from the production error logs. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Hf6j2xXQXywHVh3HAVRxB3 --- orthocal/asgi.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/orthocal/asgi.py b/orthocal/asgi.py index e5c1238..060ebf5 100644 --- a/orthocal/asgi.py +++ b/orthocal/asgi.py @@ -13,6 +13,7 @@ from django.conf import settings from django.core.asgi import get_asgi_application +from mcp.server.transport_security import TransportSecuritySettings django_application = get_asgi_application() @@ -24,7 +25,19 @@ # owns its own /mcp route and lifespan (which starts/stops its session # manager's background task). Django's ASGIHandler doesn't implement the # lifespan protocol at all, so lifespan scope is only ever handled here. -mcp_application = mcp.streamable_http_app() +# +# transport_security must be set explicitly: with no host argument, the SDK +# defaults host to '127.0.0.1' and auto-enables DNS-rebinding protection +# restricted to localhost -- fine in local dev, but it rejects every real +# request in production (Cloud Run's own hostname as the Host header, +# forwarded by the Firebase Hosting proxy in front of it, isn't on that +# allowlist). DNS-rebinding protection defends a server bound to localhost +# against a malicious webpage's JS reaching it through the browser; it +# doesn't apply to a public HTTPS API with no localhost-only trust boundary, +# so disabling it here is the correct fix, not a workaround. +mcp_application = mcp.streamable_http_app( + transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False), +) async def application(scope, receive, send):