From 5bd337dc905d6a98cf239a1bd9f5a4feb851350e Mon Sep 17 00:00:00 2001 From: Brian Glass Date: Sun, 16 Aug 2026 11:57:45 -0400 Subject: [PATCH] Reject GET on /mcp instead of holding an unused SSE stream open Neither MCP tool (get_day, search_saints) ever pushes unsolicited messages to a client, so the streamable-http transport's optional GET/SSE channel serves no purpose here. The SDK holds that stream open indefinitely waiting for a server-initiated message that never comes, and Cloud Run only closes it at the 20s request timeout -- request logs showed a sustained flood of GET /mcp requests (up ~50x since Aug 11, steady ~190/hour) each billed for a full 20s of held-open compute, the largest driver behind this month's Cloud Run bill. Rejecting GET before it reaches the MCP app costs single-digit milliseconds instead; POST (actual tool calls) is unaffected. --- orthocal/asgi.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/orthocal/asgi.py b/orthocal/asgi.py index 0165d5f..b78060e 100644 --- a/orthocal/asgi.py +++ b/orthocal/asgi.py @@ -51,8 +51,35 @@ ) +async def _reject_get(send): + """Neither of orthocal's MCP tools ever needs to push an unsolicited + message to a client, so the streamable-http transport's optional GET/SSE + channel serves no purpose here -- but the SDK holds it open indefinitely + waiting for a server-initiated message that will never come, and Cloud + Run only closes it at the request timeout (20s). That turned into the + single largest cost driver on this service: a flood of GET requests each + billed for a full 20s of held-open compute. Rejecting GET here, before it + reaches the MCP app, costs a few milliseconds instead.""" + + await send({ + 'type': 'http.response.start', + 'status': 405, + 'headers': [ + (b'allow', b'POST, DELETE'), + (b'content-type', b'text/plain'), + ], + }) + await send({ + 'type': 'http.response.body', + 'body': b'Method Not Allowed', + }) + + async def application(scope, receive, send): - if scope['type'] == 'lifespan' or (scope['type'] == 'http' and scope['path'].startswith('/mcp')): + is_mcp_path = scope['type'] == 'http' and scope['path'].startswith('/mcp') + if is_mcp_path and scope['method'] == 'GET': + await _reject_get(send) + elif scope['type'] == 'lifespan' or is_mcp_path: await mcp_application(scope, receive, send) else: await django_application(scope, receive, send)