8080 search_programs_resource ,
8181)
8282from mcp_nixos .tools .darwin .darwin_tools import register_darwin_tools
83- from mcp_nixos .tools .home_manager_tools import ( # noqa: F401
84- home_manager_info ,
85- home_manager_search ,
86- home_manager_stats ,
87- register_home_manager_tools ,
88- )
89- from mcp_nixos .tools .nixos_tools import nixos_info , nixos_search , nixos_stats , register_nixos_tools # noqa: F401
83+ from mcp_nixos .tools .home_manager_tools import register_home_manager_tools
84+ from mcp_nixos .tools .nixos_tools import register_nixos_tools
9085from mcp_nixos .utils .helpers import create_wildcard_query # noqa: F401
9186
9287# Load environment variables from .env file
@@ -206,6 +201,44 @@ def run_precache():
206201async def app_lifespan (mcp_server : FastMCP ):
207202 logger .info ("Initializing MCP-NixOS server components" )
208203
204+ # Import state persistence
205+ from mcp_nixos .utils .state_persistence import get_state_persistence
206+
207+ # Create state tracking with initial value
208+ state_persistence = get_state_persistence ()
209+ state_persistence .load_state ()
210+
211+ # Track connection count across reconnections
212+ connection_count = state_persistence .increment_counter ("connection_count" )
213+ logger .info (f"This is connection #{ connection_count } since server installation" )
214+
215+ # Create synchronization for MCP protocol initialization
216+ protocol_initialized = asyncio .Event ()
217+ app_ready = asyncio .Event ()
218+
219+ # Track initialization state in context
220+ lifespan_context = {
221+ "nixos_context" : nixos_context ,
222+ "home_manager_context" : home_manager_context ,
223+ "darwin_context" : darwin_context ,
224+ "is_ready" : False ,
225+ "initialization_time" : time .time (),
226+ "connection_count" : connection_count ,
227+ }
228+
229+ # Handle MCP protocol handshake
230+ # FastMCP doesn't expose a public API for modifying initialize behavior,
231+ # but it handles the initialize/initialized protocol automatically.
232+ # We'll use protocol_initialized.set() when we detect the first connection.
233+
234+ # We'll mark the initialization as complete as soon as app is ready
235+ logger .info ("Setting protocol initialization events" )
236+ protocol_initialized .set ()
237+
238+ # This will trigger waiting for connection
239+ logger .info ("App is ready for requests" )
240+ lifespan_context ["is_ready" ] = True
241+
209242 # Start loading Home Manager data in background thread
210243 # This way the server can start up immediately without blocking
211244 logger .info ("Starting background loading of Home Manager data..." )
@@ -228,6 +261,20 @@ async def app_lifespan(mcp_server: FastMCP):
228261 # Don't wait for the data to be fully loaded
229262 logger .info ("Server will continue startup while Home Manager and Darwin data loads in background" )
230263
264+ # Mark app as ready for requests
265+ logger .info ("App is ready for requests, waiting for MCP protocol initialization" )
266+ app_ready .set ()
267+
268+ # Wait for MCP protocol initialization (with timeout)
269+ try :
270+ await asyncio .wait_for (protocol_initialized .wait (), timeout = 5.0 )
271+ logger .info ("MCP protocol initialization complete" )
272+ lifespan_context ["is_ready" ] = True
273+ except asyncio .TimeoutError :
274+ logger .warning ("Timeout waiting for MCP initialize request. Server will proceed anyway." )
275+ # Still mark as ready to avoid hanging
276+ lifespan_context ["is_ready" ] = True
277+
231278 # Add prompt to guide assistants on using the MCP tools
232279 @mcp_server .prompt ()
233280 def mcp_nixos_prompt ():
@@ -652,12 +699,15 @@ def mcp_nixos_prompt():
652699 """
653700
654701 try :
702+ # Save the final state before yielding control to server
703+ from mcp_nixos .utils .state_persistence import get_state_persistence
704+
705+ state_persistence = get_state_persistence ()
706+ state_persistence .set_state ("last_startup_time" , time .time ())
707+ state_persistence .save_state ()
708+
655709 # We yield our contexts that will be accessible in all handlers
656- yield {
657- "nixos_context" : nixos_context ,
658- "home_manager_context" : home_manager_context ,
659- "darwin_context" : darwin_context ,
660- }
710+ yield lifespan_context
661711 except Exception as e :
662712 logger .error (f"Error in server lifespan: { e } " )
663713 raise
@@ -668,6 +718,25 @@ def mcp_nixos_prompt():
668718 # Track start time for overall shutdown duration
669719 shutdown_start = time .time ()
670720
721+ # Save final state before shutdown
722+ try :
723+ from mcp_nixos .utils .state_persistence import get_state_persistence
724+
725+ state_persistence = get_state_persistence ()
726+ state_persistence .set_state ("last_shutdown_time" , time .time ())
727+ state_persistence .set_state ("shutdown_reason" , "normal" )
728+
729+ # Calculate uptime if we have an initialization time
730+ if lifespan_context .get ("initialization_time" ):
731+ uptime = time .time () - lifespan_context ["initialization_time" ]
732+ state_persistence .set_state ("last_uptime" , uptime )
733+ logger .info (f"Server uptime: { uptime :.2f} s" )
734+
735+ # Save state to disk
736+ state_persistence .save_state ()
737+ except Exception as e :
738+ logger .error (f"Error saving state during shutdown: { e } " )
739+
671740 # Create coroutines for shutdown operations
672741 shutdown_coroutines = []
673742
@@ -710,8 +779,22 @@ def mcp_nixos_prompt():
710779 logger .debug ("All context shutdowns completed" )
711780 except asyncio .TimeoutError :
712781 logger .warning ("Some shutdown operations timed out and were terminated" )
782+ # Record abnormal shutdown in state
783+ try :
784+ state_persistence = get_state_persistence ()
785+ state_persistence .set_state ("shutdown_reason" , "timeout" )
786+ state_persistence .save_state ()
787+ except Exception :
788+ pass # Avoid cascading errors
713789 except Exception as e :
714790 logger .error (f"Error during concurrent shutdown operations: { e } " )
791+ # Record error in state
792+ try :
793+ state_persistence = get_state_persistence ()
794+ state_persistence .set_state ("shutdown_reason" , f"error: { str (e )} " )
795+ state_persistence .save_state ()
796+ except Exception :
797+ pass # Avoid cascading errors
715798
716799 # Log shutdown duration
717800 shutdown_duration = time .time () - shutdown_start
0 commit comments