@@ -85,25 +85,10 @@ async def handle_call_tool(name: str, args: dict) -> list[TextContent]:
85
85
return [TextContent (type = "text" , text = f"Called { name } " )]
86
86
87
87
88
- @pytest .fixture
89
- def server_port () -> int :
90
- """Find an available port for the test server."""
91
- with socket .socket () as s :
92
- s .bind (("127.0.0.1" , 0 ))
93
- return s .getsockname ()[1 ]
94
-
95
-
96
- @pytest .fixture
97
- def server_url (server_port : int ) -> str :
98
- """Get the URL for the test server."""
99
- return f"http://127.0.0.1:{ server_port } "
100
-
101
-
102
- def create_app (session_id = None , is_json_response_enabled = False ) -> Starlette :
88
+ def create_app (is_json_response_enabled = False ) -> Starlette :
103
89
"""Create a Starlette application for testing that matches the example server.
104
90
105
91
Args:
106
- session_id: Optional session ID to use for the server.
107
92
is_json_response_enabled: If True, use JSON responses instead of SSE streams.
108
93
"""
109
94
# Create server instance
@@ -197,20 +182,19 @@ async def run_server():
197
182
return app
198
183
199
184
200
- def run_server (port : int , session_id = None , is_json_response_enabled = False ) -> None :
185
+ def run_server (port : int , is_json_response_enabled = False ) -> None :
201
186
"""Run the test server.
202
187
203
188
Args:
204
189
port: Port to listen on.
205
- session_id: Optional session ID to use for the server.
206
190
is_json_response_enabled: If True, use JSON responses instead of SSE streams.
207
191
"""
208
192
print (
209
193
f"Starting test server on port { port } with "
210
- f"session_id= { session_id } , json_enabled={ is_json_response_enabled } "
194
+ f"json_enabled={ is_json_response_enabled } "
211
195
)
212
196
213
- app = create_app (session_id , is_json_response_enabled )
197
+ app = create_app (is_json_response_enabled )
214
198
# Configure server
215
199
config = uvicorn .Config (
216
200
app = app ,
@@ -238,22 +222,38 @@ def run_server(port: int, session_id=None, is_json_response_enabled=False) -> No
238
222
print ("Server shutdown" )
239
223
240
224
225
+ # Test fixtures - using same approach as SSE tests
241
226
@pytest .fixture
242
- def basic_server (server_port : int ) -> Generator [None , None , None ]:
243
- """Start a basic server without session ID."""
244
- # Start server process
245
- process = multiprocessing .Process (
246
- target = run_server , kwargs = {"port" : server_port }, daemon = True
227
+ def basic_server_port () -> int :
228
+ """Find an available port for the basic server."""
229
+ with socket .socket () as s :
230
+ s .bind (("127.0.0.1" , 0 ))
231
+ return s .getsockname ()[1 ]
232
+
233
+
234
+ @pytest .fixture
235
+ def json_server_port () -> int :
236
+ """Find an available port for the JSON response server."""
237
+ with socket .socket () as s :
238
+ s .bind (("127.0.0.1" , 0 ))
239
+ return s .getsockname ()[1 ]
240
+
241
+
242
+ @pytest .fixture
243
+ def basic_server (basic_server_port : int ) -> Generator [None , None , None ]:
244
+ """Start a basic server."""
245
+ proc = multiprocessing .Process (
246
+ target = run_server , kwargs = {"port" : basic_server_port }, daemon = True
247
247
)
248
- process .start ()
248
+ proc .start ()
249
249
250
- # Wait for server to start
250
+ # Wait for server to be running
251
251
max_attempts = 20
252
252
attempt = 0
253
253
while attempt < max_attempts :
254
254
try :
255
255
with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
256
- s .connect (("127.0.0.1" , server_port ))
256
+ s .connect (("127.0.0.1" , basic_server_port ))
257
257
break
258
258
except ConnectionRefusedError :
259
259
time .sleep (0.1 )
@@ -264,30 +264,29 @@ def basic_server(server_port: int) -> Generator[None, None, None]:
264
264
yield
265
265
266
266
# Clean up
267
- process . terminate ()
268
- process .join (timeout = 1 )
269
- if process .is_alive ():
270
- process . kill ( )
267
+ proc . kill ()
268
+ proc .join (timeout = 2 )
269
+ if proc .is_alive ():
270
+ print ( "server process failed to terminate" )
271
271
272
272
273
273
@pytest .fixture
274
- def json_response_server (server_port : int ) -> Generator [None , None , None ]:
274
+ def json_response_server (json_server_port : int ) -> Generator [None , None , None ]:
275
275
"""Start a server with JSON response enabled."""
276
- # Start server process with is_json_response_enabled=True
277
- process = multiprocessing .Process (
276
+ proc = multiprocessing .Process (
278
277
target = run_server ,
279
- kwargs = {"port" : server_port , "is_json_response_enabled" : True },
278
+ kwargs = {"port" : json_server_port , "is_json_response_enabled" : True },
280
279
daemon = True ,
281
280
)
282
- process .start ()
281
+ proc .start ()
283
282
284
- # Wait for server to start
283
+ # Wait for server to be running
285
284
max_attempts = 20
286
285
attempt = 0
287
286
while attempt < max_attempts :
288
287
try :
289
288
with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
290
- s .connect (("127.0.0.1" , server_port ))
289
+ s .connect (("127.0.0.1" , json_server_port ))
291
290
break
292
291
except ConnectionRefusedError :
293
292
time .sleep (0.1 )
@@ -298,30 +297,42 @@ def json_response_server(server_port: int) -> Generator[None, None, None]:
298
297
yield
299
298
300
299
# Clean up
301
- process .terminate ()
302
- process .join (timeout = 1 )
303
- if process .is_alive ():
304
- process .kill ()
300
+ proc .kill ()
301
+ proc .join (timeout = 2 )
302
+ if proc .is_alive ():
303
+ print ("server process failed to terminate" )
304
+
305
+
306
+ @pytest .fixture
307
+ def basic_server_url (basic_server_port : int ) -> str :
308
+ """Get the URL for the basic test server."""
309
+ return f"http://127.0.0.1:{ basic_server_port } "
310
+
311
+
312
+ @pytest .fixture
313
+ def json_server_url (json_server_port : int ) -> str :
314
+ """Get the URL for the JSON response test server."""
315
+ return f"http://127.0.0.1:{ json_server_port } "
305
316
306
317
307
318
# Basic request validation tests
308
- def test_accept_header_validation (basic_server , server_url ):
319
+ def test_accept_header_validation (basic_server , basic_server_url ):
309
320
"""Test that Accept header is properly validated."""
310
321
# Test without Accept header
311
322
response = requests .post (
312
- f"{ server_url } /mcp" ,
323
+ f"{ basic_server_url } /mcp" ,
313
324
headers = {"Content-Type" : "application/json" },
314
325
json = {"jsonrpc" : "2.0" , "method" : "initialize" , "id" : 1 },
315
326
)
316
327
assert response .status_code == 406
317
328
assert "Not Acceptable" in response .text
318
329
319
330
320
- def test_content_type_validation (basic_server , server_url ):
331
+ def test_content_type_validation (basic_server , basic_server_url ):
321
332
"""Test that Content-Type header is properly validated."""
322
333
# Test with incorrect Content-Type
323
334
response = requests .post (
324
- f"{ server_url } /mcp" ,
335
+ f"{ basic_server_url } /mcp" ,
325
336
headers = {
326
337
"Accept" : "application/json, text/event-stream" ,
327
338
"Content-Type" : "text/plain" ,
@@ -332,11 +343,11 @@ def test_content_type_validation(basic_server, server_url):
332
343
assert "Unsupported Media Type" in response .text
333
344
334
345
335
- def test_json_validation (basic_server , server_url ):
346
+ def test_json_validation (basic_server , basic_server_url ):
336
347
"""Test that JSON content is properly validated."""
337
348
# Test with invalid JSON
338
349
response = requests .post (
339
- f"{ server_url } /mcp" ,
350
+ f"{ basic_server_url } /mcp" ,
340
351
headers = {
341
352
"Accept" : "application/json, text/event-stream" ,
342
353
"Content-Type" : "application/json" ,
@@ -347,11 +358,11 @@ def test_json_validation(basic_server, server_url):
347
358
assert "Parse error" in response .text
348
359
349
360
350
- def test_json_parsing (basic_server , server_url ):
361
+ def test_json_parsing (basic_server , basic_server_url ):
351
362
"""Test that JSON content is properly parse."""
352
363
# Test with valid JSON but invalid JSON-RPC
353
364
response = requests .post (
354
- f"{ server_url } /mcp" ,
365
+ f"{ basic_server_url } /mcp" ,
355
366
headers = {
356
367
"Accept" : "application/json, text/event-stream" ,
357
368
"Content-Type" : "application/json" ,
@@ -362,11 +373,11 @@ def test_json_parsing(basic_server, server_url):
362
373
assert "Validation error" in response .text
363
374
364
375
365
- def test_method_not_allowed (basic_server , server_url ):
376
+ def test_method_not_allowed (basic_server , basic_server_url ):
366
377
"""Test that unsupported HTTP methods are rejected."""
367
378
# Test with unsupported method (PUT)
368
379
response = requests .put (
369
- f"{ server_url } /mcp" ,
380
+ f"{ basic_server_url } /mcp" ,
370
381
headers = {
371
382
"Accept" : "application/json, text/event-stream" ,
372
383
"Content-Type" : "application/json" ,
@@ -377,13 +388,13 @@ def test_method_not_allowed(basic_server, server_url):
377
388
assert "Method Not Allowed" in response .text
378
389
379
390
380
- def test_session_validation (basic_server , server_url ):
391
+ def test_session_validation (basic_server , basic_server_url ):
381
392
"""Test session ID validation."""
382
393
# session_id not used directly in this test
383
394
384
395
# Test without session ID
385
396
response = requests .post (
386
- f"{ server_url } /mcp" ,
397
+ f"{ basic_server_url } /mcp" ,
387
398
headers = {
388
399
"Accept" : "application/json, text/event-stream" ,
389
400
"Content-Type" : "application/json" ,
@@ -452,10 +463,10 @@ def test_streamable_http_transport_init_validation():
452
463
StreamableHTTPServerTransport (mcp_session_id = "test\n " )
453
464
454
465
455
- def test_session_termination (basic_server , server_url ):
466
+ def test_session_termination (basic_server , basic_server_url ):
456
467
"""Test session termination via DELETE and subsequent request handling."""
457
468
response = requests .post (
458
- f"{ server_url } /mcp" ,
469
+ f"{ basic_server_url } /mcp" ,
459
470
headers = {
460
471
"Accept" : "application/json, text/event-stream" ,
461
472
"Content-Type" : "application/json" ,
@@ -467,15 +478,15 @@ def test_session_termination(basic_server, server_url):
467
478
# Now terminate the session
468
479
session_id = response .headers .get (MCP_SESSION_ID_HEADER )
469
480
response = requests .delete (
470
- f"{ server_url } /mcp" ,
481
+ f"{ basic_server_url } /mcp" ,
471
482
headers = {MCP_SESSION_ID_HEADER : session_id },
472
483
)
473
484
assert response .status_code == 200
474
485
assert "Session terminated" in response .text
475
486
476
487
# Try to use the terminated session
477
488
response = requests .post (
478
- f"{ server_url } /mcp" ,
489
+ f"{ basic_server_url } /mcp" ,
479
490
headers = {
480
491
"Accept" : "application/json, text/event-stream" ,
481
492
"Content-Type" : "application/json" ,
@@ -487,9 +498,9 @@ def test_session_termination(basic_server, server_url):
487
498
assert "Session has been terminated" in response .text
488
499
489
500
490
- def test_response (basic_server , server_url ):
501
+ def test_response (basic_server , basic_server_url ):
491
502
"""Test response handling for a valid request."""
492
- mcp_url = f"{ server_url } /mcp"
503
+ mcp_url = f"{ basic_server_url } /mcp"
493
504
response = requests .post (
494
505
mcp_url ,
495
506
headers = {
@@ -518,9 +529,9 @@ def test_response(basic_server, server_url):
518
529
assert tools_response .headers .get ("Content-Type" ) == "text/event-stream"
519
530
520
531
521
- def test_json_response (json_response_server , server_url ):
532
+ def test_json_response (json_response_server , json_server_url ):
522
533
"""Test response handling when is_json_response_enabled is True."""
523
- mcp_url = f"{ server_url } /mcp"
534
+ mcp_url = f"{ json_server_url } /mcp"
524
535
response = requests .post (
525
536
mcp_url ,
526
537
headers = {
@@ -530,4 +541,4 @@ def test_json_response(json_response_server, server_url):
530
541
json = INIT_REQUEST ,
531
542
)
532
543
assert response .status_code == 200
533
- assert response .headers .get ("Content-Type" ) == "application/json"
544
+ assert response .headers .get ("Content-Type" ) == "application/json"
0 commit comments