22import multiprocessing
33import socket
44import time
5+ import os
6+ import signal
7+ import atexit
8+ import sys
9+ import threading
10+ import coverage
511from typing import AsyncGenerator , Generator
612from mcp .client .session import ClientSession
713from mcp .client .sse import sse_client
@@ -32,6 +38,40 @@ def server_url(server_port: int) -> str:
3238
3339
3440def run_server (server_port : int ) -> None :
41+ # Initialize coverage for subprocesses
42+ cov = None
43+ if "COVERAGE_PROCESS_START" in os .environ :
44+ cov = coverage .Coverage (source = ["fastapi_mcp" ])
45+ cov .start ()
46+
47+ # Create a function to save coverage data at exit
48+ def cleanup ():
49+ if cov :
50+ cov .stop ()
51+ cov .save ()
52+
53+ # Register multiple cleanup mechanisms to ensure coverage data is saved
54+ atexit .register (cleanup )
55+
56+ # Setup signal handler for clean termination
57+ def handle_signal (signum , frame ):
58+ cleanup ()
59+ sys .exit (0 )
60+
61+ signal .signal (signal .SIGTERM , handle_signal )
62+
63+ # Backup thread to ensure coverage is written if process is terminated abruptly
64+ def periodic_save ():
65+ while True :
66+ time .sleep (1.0 )
67+ if cov :
68+ cov .save ()
69+
70+ save_thread = threading .Thread (target = periodic_save )
71+ save_thread .daemon = True
72+ save_thread .start ()
73+
74+ # Configure the server
3575 fastapi = make_simple_fastapi_app ()
3676 mcp = FastApiMCP (
3777 fastapi ,
@@ -40,16 +80,26 @@ def run_server(server_port: int) -> None:
4080 )
4181 mcp .mount ()
4282
83+ # Start the server
4384 server = uvicorn .Server (config = uvicorn .Config (app = fastapi , host = HOST , port = server_port , log_level = "error" ))
4485 server .run ()
4586
4687 # Give server time to start
4788 while not server .started :
4889 time .sleep (0.5 )
4990
91+ # Ensure coverage is saved if exiting the normal way
92+ if cov :
93+ cov .stop ()
94+ cov .save ()
95+
5096
5197@pytest .fixture ()
5298def server (server_port : int ) -> Generator [None , None , None ]:
99+ # Ensure COVERAGE_PROCESS_START is set in the environment for subprocesses
100+ coverage_rc = os .path .abspath (".coveragerc" )
101+ os .environ ["COVERAGE_PROCESS_START" ] = coverage_rc
102+
53103 proc = multiprocessing .Process (target = run_server , kwargs = {"server_port" : server_port }, daemon = True )
54104 proc .start ()
55105
@@ -69,11 +119,18 @@ def server(server_port: int) -> Generator[None, None, None]:
69119
70120 yield
71121
72- # Signal the server to stop
73- proc .kill ()
74- proc .join (timeout = 2 )
122+ # Signal the server to stop - added graceful shutdown before kill
123+ try :
124+ proc .terminate ()
125+ proc .join (timeout = 2 )
126+ except (OSError , AttributeError ):
127+ pass
128+
75129 if proc .is_alive ():
76- raise RuntimeError ("server process failed to terminate" )
130+ proc .kill ()
131+ proc .join (timeout = 2 )
132+ if proc .is_alive ():
133+ raise RuntimeError ("server process failed to terminate" )
77134
78135
79136@pytest .fixture ()
0 commit comments