1- # test_sse.py
21import re
32import multiprocessing
43import socket
2120from mcp .client .sse import sse_client
2221from mcp .server import Server
2322from mcp .server .sse import SseServerTransport
24- from mcp .types import EmptyResult , ErrorData , InitializeResult , TextContent , TextResourceContents , Tool
23+ from mcp .types import (
24+ EmptyResult ,
25+ ErrorData ,
26+ InitializeResult ,
27+ TextContent ,
28+ TextResourceContents ,
29+ Tool ,
30+ )
2531
2632SERVER_NAME = "test_server_for_SSE"
2733
34+
2835@pytest .fixture
2936def server_port () -> int :
3037 with socket .socket () as s :
31- s .bind ((' 127.0.0.1' , 0 ))
38+ s .bind ((" 127.0.0.1" , 0 ))
3239 return s .getsockname ()[1 ]
3340
41+
3442@pytest .fixture
3543def server_url (server_port : int ) -> str :
3644 return f"http://127.0.0.1:{ server_port } "
3745
46+
3847# Test server implementation
3948class TestServer (Server ):
4049 def __init__ (self ):
@@ -45,15 +54,19 @@ async def handle_read_resource(uri: AnyUrl) -> str | bytes:
4554 if uri .scheme == "foobar" :
4655 return f"Read { uri .host } "
4756 # TODO: make this an error
48- raise McpError (error = ErrorData (code = 404 , message = "OOPS! no resource with that URI was found" ))
57+ raise McpError (
58+ error = ErrorData (
59+ code = 404 , message = "OOPS! no resource with that URI was found"
60+ )
61+ )
4962
5063 @self .list_tools ()
5164 async def handle_list_tools ():
5265 return [
5366 Tool (
5467 name = "test_tool" ,
5568 description = "A test tool" ,
56- inputSchema = {"type" : "object" , "properties" : {}}
69+ inputSchema = {"type" : "object" , "properties" : {}},
5770 )
5871 ]
5972
@@ -62,9 +75,8 @@ async def handle_call_tool(name: str, args: dict):
6275 return [TextContent (type = "text" , text = f"Called { name } " )]
6376
6477
65-
6678# Test fixtures
67- def make_server_app ()-> Starlette :
79+ def make_server_app () -> Starlette :
6880 """Create test Starlette app with SSE transport"""
6981 sse = SseServerTransport ("/messages/" )
7082 server = TestServer ()
@@ -74,80 +86,97 @@ async def handle_sse(request):
7486 request .scope , request .receive , request ._send
7587 ) as streams :
7688 await server .run (
77- streams [0 ],
78- streams [1 ],
79- server .create_initialization_options ()
89+ streams [0 ], streams [1 ], server .create_initialization_options ()
8090 )
8191
82- app = Starlette (routes = [
83- Route ("/sse" , endpoint = handle_sse ),
84- Mount ("/messages/" , app = sse .handle_post_message ),
85- ])
92+ app = Starlette (
93+ routes = [
94+ Route ("/sse" , endpoint = handle_sse ),
95+ Mount ("/messages/" , app = sse .handle_post_message ),
96+ ]
97+ )
8698
8799 return app
88100
101+
89102@pytest .fixture (autouse = True )
90103def space_around_test ():
91104 time .sleep (0.1 )
92105 yield
93106 time .sleep (0.1 )
94107
108+
95109def run_server (server_port : int ):
96110 app = make_server_app ()
97- server = uvicorn .Server (config = uvicorn .Config (app = app , host = "127.0.0.1" , port = server_port , log_level = "error" ))
98- print (f'starting server on { server_port } ' )
111+ server = uvicorn .Server (
112+ config = uvicorn .Config (
113+ app = app , host = "127.0.0.1" , port = server_port , log_level = "error"
114+ )
115+ )
116+ print (f"starting server on { server_port } " )
99117 server .run ()
100118
101119 # Give server time to start
102120 while not server .started :
103- print (' waiting for server to start' )
121+ print (" waiting for server to start" )
104122 time .sleep (0.5 )
105123
124+
106125@pytest .fixture ()
107126def server (server_port : int ):
108- proc = multiprocessing .Process (target = run_server , kwargs = {"server_port" : server_port }, daemon = True )
109- print ('starting process' )
127+ proc = multiprocessing .Process (
128+ target = run_server , kwargs = {"server_port" : server_port }, daemon = True
129+ )
130+ print ("starting process" )
110131 proc .start ()
111132
112133 # Wait for server to be running
113134 max_attempts = 20
114135 attempt = 0
115- print (' waiting for server to start' )
136+ print (" waiting for server to start" )
116137 while attempt < max_attempts :
117138 try :
118139 with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
119- s .connect ((' 127.0.0.1' , server_port ))
140+ s .connect ((" 127.0.0.1" , server_port ))
120141 break
121142 except ConnectionRefusedError :
122143 time .sleep (0.1 )
123144 attempt += 1
124145 else :
125- raise RuntimeError ("Server failed to start after {} attempts" .format (max_attempts ))
146+ raise RuntimeError (
147+ "Server failed to start after {} attempts" .format (max_attempts )
148+ )
126149
127150 yield
128151
129- print (' killing server' )
152+ print (" killing server" )
130153 # Signal the server to stop
131154 proc .kill ()
132155 proc .join (timeout = 2 )
133156 if proc .is_alive ():
134157 print ("server process failed to terminate" )
135158
159+
136160@pytest .fixture ()
137161async def http_client (server , server_url ) -> AsyncGenerator [httpx .AsyncClient , None ]:
138162 """Create test client"""
139163 async with httpx .AsyncClient (base_url = server_url ) as client :
140164 yield client
141165
166+
142167# Tests
143168@pytest .mark .anyio
144169async def test_raw_sse_connection (http_client : httpx .AsyncClient ):
145170 """Test the SSE connection establishment simply with an HTTP client."""
146171 async with anyio .create_task_group () as tg :
172+
147173 async def connection_test ():
148174 async with http_client .stream ("GET" , "/sse" ) as response :
149175 assert response .status_code == 200
150- assert response .headers ["content-type" ] == "text/event-stream; charset=utf-8"
176+ assert (
177+ response .headers ["content-type" ]
178+ == "text/event-stream; charset=utf-8"
179+ )
151180
152181 line_number = 0
153182 async for line in response .aiter_lines ():
@@ -177,23 +206,32 @@ async def test_sse_client_basic_connection(server, server_url):
177206 ping_result = await session .send_ping ()
178207 assert isinstance (ping_result , EmptyResult )
179208
209+
180210@pytest .fixture
181- async def initialized_sse_client_session (server , server_url : str ) -> AsyncGenerator [ClientSession , None ]:
211+ async def initialized_sse_client_session (
212+ server , server_url : str
213+ ) -> AsyncGenerator [ClientSession , None ]:
182214 async with sse_client (server_url + "/sse" ) as streams :
183215 async with ClientSession (* streams ) as session :
184216 await session .initialize ()
185217 yield session
186218
219+
187220@pytest .mark .anyio
188- async def test_sse_client_happy_request_and_response (initialized_sse_client_session : ClientSession ):
221+ async def test_sse_client_happy_request_and_response (
222+ initialized_sse_client_session : ClientSession ,
223+ ):
189224 session = initialized_sse_client_session
190225 response = await session .read_resource (uri = AnyUrl ("foobar://should-work" ))
191226 assert len (response .contents ) == 1
192227 assert isinstance (response .contents [0 ], TextResourceContents )
193228 assert response .contents [0 ].text == "Read should-work"
194229
230+
195231@pytest .mark .anyio
196- async def test_sse_client_exception_handling (initialized_sse_client_session : ClientSession ):
232+ async def test_sse_client_exception_handling (
233+ initialized_sse_client_session : ClientSession ,
234+ ):
197235 session = initialized_sse_client_session
198236 with pytest .raises (McpError , match = "OOPS! no resource with that URI was found" ):
199237 await session .read_resource (uri = AnyUrl ("xxx://will-not-work" ))
0 commit comments