1010
1111from mcp .server .fastmcp .server import FastMCP
1212from mcp .server .transport_security import TransportSecuritySettings
13+ from starlette .requests import Request
14+ from starlette .responses import Response
15+ from starlette .types import ASGIApp , Receive , Scope , Send
1316
1417from mcp_paradex import __version__
1518from mcp_paradex .utils .config import config
2326logger = logging .getLogger ("mcp-paradex" )
2427
2528
29+ class RejectGetMiddleware :
30+ """
31+ Rejects GET requests with 405 in stateless HTTP mode.
32+
33+ In stateless mode (Lambda), GET /mcp would open a persistent SSE stream that
34+ Lambda cannot hold open. Returning 405 causes MCP clients to fall back to
35+ POST-only mode, which works correctly with Lambda's request/response model.
36+ """
37+
38+ def __init__ (self , app : ASGIApp , mcp_path : str = "/mcp" ) -> None :
39+ self .app = app
40+ self .mcp_path = mcp_path
41+
42+ async def __call__ (self , scope : Scope , receive : Receive , send : Send ) -> None :
43+ if scope ["type" ] == "http" and scope ["method" ] == "GET" and scope ["path" ] == self .mcp_path :
44+ response = Response (
45+ content = "Method Not Allowed: GET is not supported in stateless mode" ,
46+ status_code = 405 ,
47+ headers = {"Allow" : "POST" },
48+ )
49+ await response (scope , receive , send )
50+ return
51+ await self .app (scope , receive , send )
52+
53+
2654def create_server () -> FastMCP :
2755 """
2856 Create and configure the FastMCP server instance.
@@ -85,6 +113,8 @@ def run_cli() -> None:
85113
86114 try :
87115 if args .transport == "streamable-http" :
116+ import uvicorn
117+
88118 server .settings .port = args .port
89119 server .settings .stateless_http = args .stateless
90120 server .settings .host = "0.0.0.0" # bind all interfaces for container deployments
@@ -94,7 +124,29 @@ def run_cli() -> None:
94124 server .settings .transport_security = TransportSecuritySettings (
95125 enable_dns_rebinding_protection = False
96126 )
97- server .run (transport = args .transport )
127+ starlette_app = server .streamable_http_app ()
128+ if args .stateless :
129+ # Wrap with middleware that rejects GET requests.
130+ # In stateless mode GET /mcp would open a persistent SSE stream
131+ # that Lambda cannot hold — 405 makes clients fall back to POST-only.
132+ starlette_app = RejectGetMiddleware (
133+ starlette_app , mcp_path = server .settings .streamable_http_path
134+ )
135+
136+ import anyio
137+
138+ async def _serve () -> None :
139+ config = uvicorn .Config (
140+ starlette_app ,
141+ host = server .settings .host ,
142+ port = server .settings .port ,
143+ log_level = server .settings .log_level .lower (),
144+ )
145+ await uvicorn .Server (config ).serve ()
146+
147+ anyio .run (_serve )
148+ else :
149+ server .run (transport = args .transport )
98150 except KeyboardInterrupt :
99151 logger .info ("Server stopped by user" )
100152 except Exception as e :
0 commit comments