@@ -223,17 +223,32 @@ def request(method: str, path: str, *, json_data=None, **kw):
223
223
224
224
# ───────────────────────────── Cleanup logic ─────────────────────────────
225
225
_supergw_proc : subprocess .Popen | None = None
226
+ _supergw_log_file = None
226
227
227
228
228
229
def cleanup ():
229
230
log_section ("Cleanup" , "🧹" )
230
- global _supergw_proc
231
+ global _supergw_proc , _supergw_log_file
232
+
233
+ # Clean up the supergateway process
231
234
if _supergw_proc and _supergw_proc .poll () is None :
235
+ logging .info ("🔄 Terminating supergateway process (PID: %d)" , _supergw_proc .pid )
232
236
_supergw_proc .terminate ()
233
237
try :
234
238
_supergw_proc .wait (timeout = 5 )
239
+ logging .info ("✅ Supergateway process terminated cleanly" )
235
240
except subprocess .TimeoutExpired :
241
+ logging .warning ("⚠️ Supergateway didn't terminate in time, killing it" )
236
242
_supergw_proc .kill ()
243
+ _supergw_proc .wait ()
244
+
245
+ # Close log file if open
246
+ if _supergw_log_file :
247
+ _supergw_log_file .close ()
248
+ _supergw_log_file = None
249
+
250
+ # Stop docker container
251
+ logging .info ("🐋 Stopping Docker container" )
237
252
subprocess .run (MAKE_DOCKER_STOP , stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
238
253
logging .info ("✅ Cleanup complete" )
239
254
@@ -270,49 +285,137 @@ def step_4_docker_run():
270
285
full = f"https://localhost:{ PORT_GATEWAY } { ep } "
271
286
need_auth = os .getenv ("AUTH_REQUIRED" , "true" ).lower () == "true"
272
287
headers = auth_headers if (ep == "/version" or need_auth ) else None
288
+ logging .info ("🔍 Waiting for endpoint %s (auth: %s)" , ep , bool (headers ))
273
289
if not wait_http_ok (full , 45 , headers = headers ):
274
290
raise RuntimeError (f"Gateway endpoint { ep } not ready" )
291
+ logging .info ("✅ Endpoint %s is ready" , ep )
275
292
276
293
logging .info ("✅ Gateway /health /ready /version all OK" )
277
294
278
295
279
296
def step_5_start_time_server (restart = False ):
280
- global _supergw_proc
297
+ global _supergw_proc , _supergw_log_file
298
+
299
+ # Check if npx is available
300
+ try :
301
+ npx_version = subprocess .check_output (["npx" , "--version" ], text = True , stderr = subprocess .DEVNULL ).strip ()
302
+ logging .info ("🔍 Found npx version: %s" , npx_version )
303
+ except (subprocess .CalledProcessError , FileNotFoundError ):
304
+ raise RuntimeError ("npx not found. Please install Node.js and npm." )
305
+
306
+ # Check if uvx is available
307
+ try :
308
+ uvx_check = subprocess .run (["uvx" , "--version" ], capture_output = True , text = True )
309
+ if uvx_check .returncode == 0 :
310
+ logging .info ("🔍 Found uvx version: %s" , uvx_check .stdout .strip ())
311
+ else :
312
+ logging .warning ("⚠️ uvx not found or not working. This may cause issues." )
313
+ except FileNotFoundError :
314
+ logging .warning ("⚠️ uvx not found. Please install uv (pip install uv) if the time server fails." )
315
+
281
316
if port_open (PORT_TIME_SERVER ):
282
317
if restart :
283
318
logging .info ("🔄 Restarting process on port %d" , PORT_TIME_SERVER )
284
319
try :
285
320
pid = int (subprocess .check_output (["lsof" , "-ti" , f"TCP:{ PORT_TIME_SERVER } " ], text = True ).strip ())
321
+ logging .info ("🔍 Found existing process PID: %d" , pid )
286
322
os .kill (pid , signal .SIGTERM )
287
323
time .sleep (2 )
288
324
except Exception as e :
289
325
logging .warning ("Could not stop existing server: %s" , e )
290
326
else :
291
327
logging .info ("ℹ️ Re-using MCP-Time-Server on port %d" , PORT_TIME_SERVER )
328
+ return
329
+
292
330
if not port_open (PORT_TIME_SERVER ):
293
331
log_section ("Launching MCP-Time-Server" , "⏰" )
294
- _supergw_proc = subprocess .Popen (SUPERGW_CMD , stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
295
- for _ in range (20 ):
332
+ logging .info ("🚀 Command: %s" , " " .join (shlex .quote (c ) for c in SUPERGW_CMD ))
333
+
334
+ # Create a log file for the time server output
335
+ log_filename = f"supergateway_{ int (time .time ())} .log"
336
+ _supergw_log_file = open (log_filename , "w" )
337
+ logging .info ("📝 Logging supergateway output to: %s" , log_filename )
338
+
339
+ # Start the process with output capture
340
+ _supergw_proc = subprocess .Popen (
341
+ SUPERGW_CMD ,
342
+ stdout = _supergw_log_file ,
343
+ stderr = subprocess .STDOUT ,
344
+ text = True ,
345
+ bufsize = 1
346
+ )
347
+
348
+ logging .info ("🔍 Started supergateway process with PID: %d" , _supergw_proc .pid )
349
+
350
+ # Wait for the server to start
351
+ start_time = time .time ()
352
+ timeout = 30
353
+ check_interval = 0.5
354
+
355
+ while time .time () - start_time < timeout :
356
+ # Check if process is still running
357
+ exit_code = _supergw_proc .poll ()
358
+ if exit_code is not None :
359
+ # Process exited, read the log file
360
+ _supergw_log_file .close ()
361
+ with open (log_filename , "r" ) as f :
362
+ output = f .read ()
363
+ logging .error ("❌ Time-Server process exited with code %d" , exit_code )
364
+ logging .error ("📋 Process output:\n %s" , output )
365
+ raise RuntimeError (f"Time-Server exited with code { exit_code } . Check the logs above." )
366
+
367
+ # Check if port is open
296
368
if port_open (PORT_TIME_SERVER ):
297
- break
298
- if _supergw_proc .poll () is not None :
299
- raise RuntimeError ("Time-Server exited" )
300
- time .sleep (1 )
369
+ elapsed = time .time () - start_time
370
+ logging .info ("✅ Time-Server is listening on port %d (took %.1fs)" , PORT_TIME_SERVER , elapsed )
371
+
372
+ # Give it a moment to fully initialize
373
+ time .sleep (1 )
374
+
375
+ # Double-check it's still running
376
+ if _supergw_proc .poll () is None :
377
+ return
378
+ else :
379
+ raise RuntimeError ("Time-Server started but then immediately exited" )
380
+
381
+ # Log progress
382
+ if int (time .time () - start_time ) % 5 == 0 :
383
+ logging .info ("⏳ Still waiting for Time-Server to start... (%.0fs elapsed)" , time .time () - start_time )
384
+
385
+ time .sleep (check_interval )
386
+
387
+ # Timeout reached
388
+ if _supergw_proc .poll () is None :
389
+ _supergw_proc .terminate ()
390
+ _supergw_proc .wait ()
391
+
392
+ _supergw_log_file .close ()
393
+ with open (log_filename , "r" ) as f :
394
+ output = f .read ()
395
+ logging .error ("📋 Process output:\n %s" , output )
396
+ raise RuntimeError (f"Time-Server failed to start within { timeout } s" )
301
397
302
398
303
399
def step_6_register_gateway () -> int :
304
400
log_section ("Registering gateway" , "🛂" )
305
401
payload = {"name" : "smoketest_time_server" , "url" : f"http://localhost:{ PORT_TIME_SERVER } /sse" }
402
+ logging .info ("📤 Registering gateway with payload: %s" , json .dumps (payload , indent = 2 ))
403
+
306
404
r = request ("POST" , "/gateways" , json_data = payload )
307
405
if r .status_code in (200 , 201 ):
308
406
gid = r .json ()["id" ]
309
407
logging .info ("✅ Gateway ID %s registered" , gid )
310
408
return gid
311
409
# 409 conflict → find existing
312
410
if r .status_code == 409 :
313
- gw = next (g for g in request ("GET" , "/gateways" ).json () if g ["name" ] == payload ["name" ])
314
- logging .info ("ℹ️ Gateway already present - using ID %s" , gw ["id" ])
315
- return gw ["id" ]
411
+ logging .info ("⚠️ Gateway already exists, fetching existing one" )
412
+ gateways = request ("GET" , "/gateways" ).json ()
413
+ gw = next ((g for g in gateways if g ["name" ] == payload ["name" ]), None )
414
+ if gw :
415
+ logging .info ("ℹ️ Gateway already present - using ID %s" , gw ["id" ])
416
+ return gw ["id" ]
417
+ else :
418
+ raise RuntimeError ("Gateway conflict but not found in list" )
316
419
# other error
317
420
msg = r .text
318
421
try :
@@ -323,42 +426,88 @@ def step_6_register_gateway() -> int:
323
426
324
427
325
428
def step_7_verify_tools ():
326
- names = [t ["name" ] for t in request ("GET" , "/tools" ).json ()]
327
- assert f"smoketest-time-server{ settings .gateway_tool_name_separator } get-current-time" in names , f"smoketest-time-server{ settings .gateway_tool_name_separator } get-current-time absent"
328
- logging .info ("✅ Tool visible in /tools" )
429
+ logging .info ("🔍 Fetching tool list" )
430
+ tools = request ("GET" , "/tools" ).json ()
431
+ tool_names = [t ["name" ] for t in tools ]
432
+
433
+ expected_tool = f"smoketest-time-server{ settings .gateway_tool_name_separator } get-current-time"
434
+ logging .info ("📋 Found %d tools total" , len (tool_names ))
435
+ logging .debug ("📋 All tools: %s" , json .dumps (tool_names , indent = 2 ))
436
+
437
+ if expected_tool not in tool_names :
438
+ # Log similar tools to help debug
439
+ similar = [t for t in tool_names if "time" in t .lower () or "smoketest" in t .lower ()]
440
+ if similar :
441
+ logging .error ("❌ Expected tool not found. Similar tools: %s" , similar )
442
+ raise AssertionError (f"{ expected_tool } not found in tools list" )
443
+
444
+ logging .info ("✅ Tool '%s' visible in /tools" , expected_tool )
329
445
330
446
331
447
def step_8_invoke_tool ():
332
- body = {"jsonrpc" : "2.0" , "id" : 1 , "method" : "smoketest-time-server-get-current-time" , "params" : {"timezone" : "Europe/Dublin" }}
448
+ log_section ("Invoking remote tool" , "🔧" )
449
+ body = {
450
+ "jsonrpc" : "2.0" ,
451
+ "id" : 1 ,
452
+ "method" : f"smoketest-time-server{ settings .gateway_tool_name_separator } get-current-time" ,
453
+ "params" : {"timezone" : "Europe/Dublin" }
454
+ }
455
+ logging .info ("📤 RPC request: %s" , json .dumps (body , indent = 2 ))
456
+
333
457
j = request ("POST" , "/rpc" , json_data = body ).json ()
458
+ logging .info ("📥 RPC response: %s" , json .dumps (j , indent = 2 ))
334
459
335
460
if "error" in j :
336
- raise RuntimeError (j [ " error" ] )
461
+ raise RuntimeError (f"RPC error: { j [ ' error' ] } " )
337
462
338
463
result = j .get ("result" , j )
339
464
if "content" not in result :
340
- raise RuntimeError ("Missing 'content' in tool response" )
465
+ raise RuntimeError (f"Missing 'content' in tool response. Got: { result } " )
466
+
467
+ content = result ["content" ]
468
+ if not content or not isinstance (content , list ):
469
+ raise RuntimeError (f"Invalid content format. Expected list, got: { type (content )} " )
470
+
471
+ text = content [0 ].get ("text" , "" )
472
+ if not text :
473
+ raise RuntimeError (f"No text in content. Content: { content } " )
341
474
342
- text = result ["content" ][0 ]["text" ]
343
- assert "datetime" in text , "datetime missing"
344
- logging .info ("✅ Tool invocation returned time" )
475
+ if "datetime" not in text :
476
+ raise RuntimeError (f"Expected 'datetime' in response, got: { text } " )
477
+
478
+ logging .info ("✅ Tool invocation returned time: %s" , text [:100 ])
345
479
346
480
347
481
def step_9_version_health ():
348
- health = request ("GET" , "/health" ).json ()["status" ].lower ()
482
+ log_section ("Final health check" , "🏥" )
483
+
484
+ health_resp = request ("GET" , "/health" ).json ()
485
+ logging .info ("📥 Health response: %s" , json .dumps (health_resp , indent = 2 ))
486
+ health = health_resp .get ("status" , "" ).lower ()
349
487
assert health in ("ok" , "healthy" ), f"Unexpected health status: { health } "
350
- ver = request ("GET" , "/version" ).json ()["app" ]["name" ]
488
+
489
+ ver_resp = request ("GET" , "/version" ).json ()
490
+ logging .info ("📥 Version response: %s" , json .dumps (ver_resp , indent = 2 ))
491
+ ver = ver_resp .get ("app" , {}).get ("name" , "Unknown" )
351
492
logging .info ("✅ Health OK - app %s" , ver )
352
493
353
494
354
495
def step_10_cleanup_gateway (gid : int | None = None ):
496
+ log_section ("Cleanup gateway registration" , "🧹" )
497
+
355
498
if gid is None :
356
499
logging .warning ("🧹 No gateway ID; nothing to delete" )
357
500
return
358
501
502
+ logging .info ("🗑️ Deleting gateway ID: %s" , gid )
359
503
request ("DELETE" , f"/gateways/{ gid } " )
360
- assert all (g ["id" ] != gid for g in request ("GET" , "/gateways" ).json ())
361
- logging .info ("✅ Gateway deleted" )
504
+
505
+ # Verify it's gone
506
+ gateways = request ("GET" , "/gateways" ).json ()
507
+ if any (g ["id" ] == gid for g in gateways ):
508
+ raise RuntimeError (f"Gateway { gid } still exists after deletion" )
509
+
510
+ logging .info ("✅ Gateway deleted successfully" )
362
511
363
512
364
513
# ───────────────────────────── Step registry ─────────────────────────────
@@ -391,7 +540,7 @@ def main():
391
540
392
541
logging .basicConfig (
393
542
level = logging .DEBUG if args .verbose else logging .INFO ,
394
- format = "%(asctime)s %(levelname)-7s %(message)s" ,
543
+ format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ,
395
544
datefmt = "%H:%M:%S" ,
396
545
)
397
546
@@ -414,6 +563,13 @@ def main():
414
563
failed = False
415
564
416
565
try :
566
+ logging .info ("🚀 Starting MCP Gateway smoke test" )
567
+ logging .info ("📋 Environment:" )
568
+ logging .info (" - Gateway port: %d" , PORT_GATEWAY )
569
+ logging .info (" - Time server port: %d" , PORT_TIME_SERVER )
570
+ logging .info (" - Docker container: %s" , DOCKER_CONTAINER )
571
+ logging .info (" - Selected steps: %s" , [s [0 ] for s in sel ])
572
+
417
573
for no , (name , fn ) in enumerate (sel , 1 ):
418
574
logging .info ("\n 🔸 Step %s/%s - %s" , no , len (sel ), name )
419
575
if name == "start_time_server" :
@@ -431,9 +587,17 @@ def main():
431
587
except Exception as e :
432
588
failed = True
433
589
logging .error ("❌ Failure: %s" , e , exc_info = args .verbose )
590
+ logging .error ("\n 💡 Troubleshooting tips:" )
591
+ logging .error (" - Check if npx is installed: npx --version" )
592
+ logging .error (" - Check if uvx is installed: uvx --version" )
593
+ logging .error (" - Check if port %d is already in use: lsof -i :%d" , PORT_TIME_SERVER , PORT_TIME_SERVER )
594
+ logging .error (" - Look for supergateway_*.log files for detailed output" )
595
+ logging .error (" - Try running with -v for verbose output" )
434
596
435
597
if not failed :
436
598
cleanup ()
599
+ else :
600
+ logging .warning ("⚠️ Skipping cleanup due to failure. Run with --cleanup-only to clean up manually." )
437
601
438
602
439
603
if __name__ == "__main__" :
0 commit comments