1
- # test_sse.py
2
1
import re
3
2
import multiprocessing
4
3
import socket
21
20
from mcp .client .sse import sse_client
22
21
from mcp .server import Server
23
22
from 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
+ )
25
31
26
32
SERVER_NAME = "test_server_for_SSE"
27
33
34
+
28
35
@pytest .fixture
29
36
def server_port () -> int :
30
37
with socket .socket () as s :
31
- s .bind ((' 127.0.0.1' , 0 ))
38
+ s .bind ((" 127.0.0.1" , 0 ))
32
39
return s .getsockname ()[1 ]
33
40
41
+
34
42
@pytest .fixture
35
43
def server_url (server_port : int ) -> str :
36
44
return f"http://127.0.0.1:{ server_port } "
37
45
46
+
38
47
# Test server implementation
39
48
class TestServer (Server ):
40
49
def __init__ (self ):
@@ -45,15 +54,19 @@ async def handle_read_resource(uri: AnyUrl) -> str | bytes:
45
54
if uri .scheme == "foobar" :
46
55
return f"Read { uri .host } "
47
56
# 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
+ )
49
62
50
63
@self .list_tools ()
51
64
async def handle_list_tools ():
52
65
return [
53
66
Tool (
54
67
name = "test_tool" ,
55
68
description = "A test tool" ,
56
- inputSchema = {"type" : "object" , "properties" : {}}
69
+ inputSchema = {"type" : "object" , "properties" : {}},
57
70
)
58
71
]
59
72
@@ -62,9 +75,8 @@ async def handle_call_tool(name: str, args: dict):
62
75
return [TextContent (type = "text" , text = f"Called { name } " )]
63
76
64
77
65
-
66
78
# Test fixtures
67
- def make_server_app ()-> Starlette :
79
+ def make_server_app () -> Starlette :
68
80
"""Create test Starlette app with SSE transport"""
69
81
sse = SseServerTransport ("/messages/" )
70
82
server = TestServer ()
@@ -74,80 +86,97 @@ async def handle_sse(request):
74
86
request .scope , request .receive , request ._send
75
87
) as streams :
76
88
await server .run (
77
- streams [0 ],
78
- streams [1 ],
79
- server .create_initialization_options ()
89
+ streams [0 ], streams [1 ], server .create_initialization_options ()
80
90
)
81
91
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
+ )
86
98
87
99
return app
88
100
101
+
89
102
@pytest .fixture (autouse = True )
90
103
def space_around_test ():
91
104
time .sleep (0.1 )
92
105
yield
93
106
time .sleep (0.1 )
94
107
108
+
95
109
def run_server (server_port : int ):
96
110
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 } " )
99
117
server .run ()
100
118
101
119
# Give server time to start
102
120
while not server .started :
103
- print (' waiting for server to start' )
121
+ print (" waiting for server to start" )
104
122
time .sleep (0.5 )
105
123
124
+
106
125
@pytest .fixture ()
107
126
def 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" )
110
131
proc .start ()
111
132
112
133
# Wait for server to be running
113
134
max_attempts = 20
114
135
attempt = 0
115
- print (' waiting for server to start' )
136
+ print (" waiting for server to start" )
116
137
while attempt < max_attempts :
117
138
try :
118
139
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 ))
120
141
break
121
142
except ConnectionRefusedError :
122
143
time .sleep (0.1 )
123
144
attempt += 1
124
145
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
+ )
126
149
127
150
yield
128
151
129
- print (' killing server' )
152
+ print (" killing server" )
130
153
# Signal the server to stop
131
154
proc .kill ()
132
155
proc .join (timeout = 2 )
133
156
if proc .is_alive ():
134
157
print ("server process failed to terminate" )
135
158
159
+
136
160
@pytest .fixture ()
137
161
async def http_client (server , server_url ) -> AsyncGenerator [httpx .AsyncClient , None ]:
138
162
"""Create test client"""
139
163
async with httpx .AsyncClient (base_url = server_url ) as client :
140
164
yield client
141
165
166
+
142
167
# Tests
143
168
@pytest .mark .anyio
144
169
async def test_raw_sse_connection (http_client : httpx .AsyncClient ):
145
170
"""Test the SSE connection establishment simply with an HTTP client."""
146
171
async with anyio .create_task_group () as tg :
172
+
147
173
async def connection_test ():
148
174
async with http_client .stream ("GET" , "/sse" ) as response :
149
175
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
+ )
151
180
152
181
line_number = 0
153
182
async for line in response .aiter_lines ():
@@ -177,23 +206,32 @@ async def test_sse_client_basic_connection(server, server_url):
177
206
ping_result = await session .send_ping ()
178
207
assert isinstance (ping_result , EmptyResult )
179
208
209
+
180
210
@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 ]:
182
214
async with sse_client (server_url + "/sse" ) as streams :
183
215
async with ClientSession (* streams ) as session :
184
216
await session .initialize ()
185
217
yield session
186
218
219
+
187
220
@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
+ ):
189
224
session = initialized_sse_client_session
190
225
response = await session .read_resource (uri = AnyUrl ("foobar://should-work" ))
191
226
assert len (response .contents ) == 1
192
227
assert isinstance (response .contents [0 ], TextResourceContents )
193
228
assert response .contents [0 ].text == "Read should-work"
194
229
230
+
195
231
@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
+ ):
197
235
session = initialized_sse_client_session
198
236
with pytest .raises (McpError , match = "OOPS! no resource with that URI was found" ):
199
237
await session .read_resource (uri = AnyUrl ("xxx://will-not-work" ))
0 commit comments