@@ -48,6 +48,20 @@ def http_server_url(http_server_port: int) -> str:
4848 return f"http://127.0.0.1:{ http_server_port } "
4949
5050
51+ @pytest .fixture
52+ def stateless_http_server_port () -> int :
53+ """Get a free port for testing the stateless StreamableHTTP server."""
54+ with socket .socket () as s :
55+ s .bind (("127.0.0.1" , 0 ))
56+ return s .getsockname ()[1 ]
57+
58+
59+ @pytest .fixture
60+ def stateless_http_server_url (stateless_http_server_port : int ) -> str :
61+ """Get the stateless StreamableHTTP server URL for testing."""
62+ return f"http://127.0.0.1:{ stateless_http_server_port } "
63+
64+
5165# Create a function to make the FastMCP server app
5266def make_fastmcp_app ():
5367 """Create a FastMCP server without auth settings."""
@@ -83,6 +97,23 @@ def echo(message: str) -> str:
8397 return mcp , app
8498
8599
100+ def make_fastmcp_stateless_http_app ():
101+ """Create a FastMCP server with stateless StreamableHTTP transport."""
102+ from starlette .applications import Starlette
103+
104+ mcp = FastMCP (name = "StatelessServer" , stateless_http = True )
105+
106+ # Add a simple tool
107+ @mcp .tool (description = "A simple echo tool" )
108+ def echo (message : str ) -> str :
109+ return f"Echo: { message } "
110+
111+ # Create the StreamableHTTP app
112+ app : Starlette = mcp .streamable_http_app ()
113+
114+ return mcp , app
115+
116+
86117def run_server (server_port : int ) -> None :
87118 """Run the server."""
88119 _ , app = make_fastmcp_app ()
@@ -107,6 +138,18 @@ def run_streamable_http_server(server_port: int) -> None:
107138 server .run ()
108139
109140
141+ def run_stateless_http_server (server_port : int ) -> None :
142+ """Run the stateless StreamableHTTP server."""
143+ _ , app = make_fastmcp_stateless_http_app ()
144+ server = uvicorn .Server (
145+ config = uvicorn .Config (
146+ app = app , host = "127.0.0.1" , port = server_port , log_level = "error"
147+ )
148+ )
149+ print (f"Starting stateless StreamableHTTP server on port { server_port } " )
150+ server .run ()
151+
152+
110153@pytest .fixture ()
111154def server (server_port : int ) -> Generator [None , None , None ]:
112155 """Start the server in a separate process and clean up after the test."""
@@ -173,6 +216,45 @@ def streamable_http_server(http_server_port: int) -> Generator[None, None, None]
173216 print ("StreamableHTTP server process failed to terminate" )
174217
175218
219+ @pytest .fixture ()
220+ def stateless_http_server (
221+ stateless_http_server_port : int ,
222+ ) -> Generator [None , None , None ]:
223+ """Start the stateless StreamableHTTP server in a separate process."""
224+ proc = multiprocessing .Process (
225+ target = run_stateless_http_server ,
226+ args = (stateless_http_server_port ,),
227+ daemon = True ,
228+ )
229+ print ("Starting stateless StreamableHTTP server process" )
230+ proc .start ()
231+
232+ # Wait for server to be running
233+ max_attempts = 20
234+ attempt = 0
235+ print ("Waiting for stateless StreamableHTTP server to start" )
236+ while attempt < max_attempts :
237+ try :
238+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
239+ s .connect (("127.0.0.1" , stateless_http_server_port ))
240+ break
241+ except ConnectionRefusedError :
242+ time .sleep (0.1 )
243+ attempt += 1
244+ else :
245+ raise RuntimeError (
246+ f"Stateless server failed to start after { max_attempts } attempts"
247+ )
248+
249+ yield
250+
251+ print ("Killing stateless StreamableHTTP server" )
252+ proc .kill ()
253+ proc .join (timeout = 2 )
254+ if proc .is_alive ():
255+ print ("Stateless StreamableHTTP server process failed to terminate" )
256+
257+
176258@pytest .mark .anyio
177259async def test_fastmcp_without_auth (server : None , server_url : str ) -> None :
178260 """Test that FastMCP works when auth settings are not provided."""
@@ -214,3 +296,30 @@ async def test_fastmcp_streamable_http(
214296 assert len (tool_result .content ) == 1
215297 assert isinstance (tool_result .content [0 ], TextContent )
216298 assert tool_result .content [0 ].text == "Echo: hello"
299+
300+
301+ @pytest .mark .anyio
302+ async def test_fastmcp_stateless_streamable_http (
303+ stateless_http_server : None , stateless_http_server_url : str
304+ ) -> None :
305+ """Test that FastMCP works with stateless StreamableHTTP transport."""
306+ # Connect to the server using StreamableHTTP
307+ async with streamablehttp_client (stateless_http_server_url + "/mcp" ) as (
308+ read_stream ,
309+ write_stream ,
310+ _ ,
311+ ):
312+ async with ClientSession (read_stream , write_stream ) as session :
313+ result = await session .initialize ()
314+ assert isinstance (result , InitializeResult )
315+ assert result .serverInfo .name == "StatelessServer"
316+ tool_result = await session .call_tool ("echo" , {"message" : "hello" })
317+ assert len (tool_result .content ) == 1
318+ assert isinstance (tool_result .content [0 ], TextContent )
319+ assert tool_result .content [0 ].text == "Echo: hello"
320+
321+ for i in range (3 ):
322+ tool_result = await session .call_tool ("echo" , {"message" : f"test_{ i } " })
323+ assert len (tool_result .content ) == 1
324+ assert isinstance (tool_result .content [0 ], TextContent )
325+ assert tool_result .content [0 ].text == f"Echo: test_{ i } "
0 commit comments