Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 28 additions & 19 deletions kicad_mcp/context.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,64 @@
"""
Lifespan context management for KiCad MCP Server.
"""

from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import AsyncIterator, Dict, Any
import logging # Import logging
import os # Added for PID
import logging # Import logging
import os # Added for PID

from mcp.server.fastmcp import FastMCP

# Get PID for logging
# _PID = os.getpid()


@dataclass
class KiCadAppContext:
"""Type-safe context for KiCad MCP server."""

kicad_modules_available: bool

# Optional cache for expensive operations
cache: Dict[str, Any]


@asynccontextmanager
async def kicad_lifespan(server: FastMCP, kicad_modules_available: bool = False) -> AsyncIterator[KiCadAppContext]:
async def kicad_lifespan(
server: FastMCP, kicad_modules_available: bool = False
) -> AsyncIterator[KiCadAppContext]:
"""Manage KiCad MCP server lifecycle with type-safe context.

This function handles:
1. Initializing shared resources when the server starts
2. Providing a typed context object to all request handlers
3. Properly cleaning up resources when the server shuts down

Args:
server: The FastMCP server instance
kicad_modules_available: Flag indicating if Python modules were found (passed from create_server)

Yields:
KiCadAppContext: A typed context object shared across all handlers
"""
logging.info(f"Starting KiCad MCP server initialization")

# Resources initialization - Python path setup removed
# print("Setting up KiCad Python modules")
# kicad_modules_available = setup_kicad_python_path() # Now passed as arg
logging.info(f"KiCad Python module availability: {kicad_modules_available} (Setup logic removed)")

logging.info(
f"KiCad Python module availability: {kicad_modules_available} (Setup logic removed)"
)

# Create in-memory cache for expensive operations
cache: Dict[str, Any] = {}

# Initialize any other resources that need cleanup later
created_temp_dirs = [] # Assuming this is managed elsewhere or not needed for now
created_temp_dirs = [] # Assuming this is managed elsewhere or not needed for now

try:
# --- Removed Python module preloading section ---
# --- Removed Python module preloading section ---
# if kicad_modules_available:
# try:
# print("Preloading KiCad Python modules")
Expand All @@ -61,25 +69,26 @@ async def kicad_lifespan(server: FastMCP, kicad_modules_available: bool = False)
# Yield the context to the server - server runs during this time
logging.info(f"KiCad MCP server initialization complete")
yield KiCadAppContext(
kicad_modules_available=kicad_modules_available, # Pass the flag through
cache=cache
kicad_modules_available=kicad_modules_available, # Pass the flag through
cache=cache,
)
finally:
# Clean up resources when server shuts down
logging.info(f"Shutting down KiCad MCP server")

# Clear the cache
if cache:
logging.info(f"Clearing cache with {len(cache)} entries")
cache.clear()

# Clean up any temporary directories
import shutil

for temp_dir in created_temp_dirs:
try:
logging.info(f"Removing temporary directory: {temp_dir}")
shutil.rmtree(temp_dir, ignore_errors=True)
except Exception as e:
logging.error(f"Error cleaning up temporary directory {temp_dir}: {str(e)}")

logging.info(f"KiCad MCP server shutdown complete")
56 changes: 32 additions & 24 deletions kicad_mcp/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
MCP server creation and configuration.
"""

import atexit
import os
import signal
Expand Down Expand Up @@ -45,38 +46,41 @@
# Store server instance for clean shutdown
_server_instance = None


def add_cleanup_handler(handler: Callable) -> None:
"""Register a function to be called during cleanup.

Args:
handler: Function to call during cleanup
"""
cleanup_handlers.append(handler)


def run_cleanup_handlers() -> None:
"""Run all registered cleanup handlers."""
logging.info(f"Running cleanup handlers...")

global _shutting_down

# Prevent running cleanup handlers multiple times
if _shutting_down:
return

_shutting_down = True
logging.info(f"Running cleanup handlers...")

for handler in cleanup_handlers:
try:
handler()
logging.info(f"Cleanup handler {handler.__name__} completed successfully")
except Exception as e:
logging.error(f"Error in cleanup handler {handler.__name__}: {str(e)}", exc_info=True)


def shutdown_server():
"""Properly shutdown the server if it exists."""
global _server_instance

if _server_instance:
try:
logging.info(f"Shutting down KiCad MCP server")
Expand All @@ -88,22 +92,23 @@ def shutdown_server():

def register_signal_handlers(server: FastMCP) -> None:
"""Register handlers for system signals to ensure clean shutdown.

Args:
server: The FastMCP server instance
"""

def handle_exit_signal(signum, frame):
logging.info(f"Received signal {signum}, initiating shutdown...")

# Run cleanup first
run_cleanup_handlers()

# Then shutdown server
shutdown_server()

# Exit without waiting for stdio processes which might be blocking
os._exit(0)

# Register for common termination signals
for sig in (signal.SIGINT, signal.SIGTERM):
try:
Expand All @@ -120,21 +125,25 @@ def create_server() -> FastMCP:

# Try to set up KiCad Python path - Removed
# kicad_modules_available = setup_kicad_python_path()
kicad_modules_available = False # Set to False as we removed the setup logic
kicad_modules_available = False # Set to False as we removed the setup logic

# if kicad_modules_available:
# print("KiCad Python modules successfully configured")
# else:
# Always print this now, as we rely on CLI
logging.info(f"KiCad Python module setup removed; relying on kicad-cli for external operations.")
logging.info(
f"KiCad Python module setup removed; relying on kicad-cli for external operations."
)

# Build a lifespan callable with the kwarg baked in (FastMCP 2.x dropped lifespan_kwargs)
lifespan_factory = functools.partial(kicad_lifespan, kicad_modules_available=kicad_modules_available)
lifespan_factory = functools.partial(
kicad_lifespan, kicad_modules_available=kicad_modules_available
)

# Initialize FastMCP server
mcp = FastMCP("KiCad", lifespan=lifespan_factory)
logging.info(f"Created FastMCP server instance with lifespan management")

# Register resources
logging.info(f"Registering resources...")
register_project_resources(mcp)
Expand All @@ -143,7 +152,7 @@ def create_server() -> FastMCP:
register_bom_resources(mcp)
register_netlist_resources(mcp)
register_pattern_resources(mcp)

# Register tools
logging.info(f"Registering tools...")
register_project_tools(mcp)
Expand All @@ -153,7 +162,7 @@ def create_server() -> FastMCP:
register_bom_tools(mcp)
register_netlist_tools(mcp)
register_pattern_tools(mcp)

# Register prompts
logging.info(f"Registering prompts...")
register_prompts(mcp)
Expand All @@ -164,7 +173,7 @@ def create_server() -> FastMCP:
# Register signal handlers and cleanup
register_signal_handlers(mcp)
atexit.register(run_cleanup_handlers)

# Add specific cleanup handlers
add_cleanup_handler(lambda: logging.info(f"KiCad MCP server shutdown complete"))

Expand All @@ -173,20 +182,20 @@ def cleanup_temp_dirs():
"""Clean up any temporary directories created by the server."""
import shutil
from kicad_mcp.utils.temp_dir_manager import get_temp_dirs

temp_dirs = get_temp_dirs()
logging.info(f"Cleaning up {len(temp_dirs)} temporary directories")

for temp_dir in temp_dirs:
try:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
logging.info(f"Removed temporary directory: {temp_dir}")
except Exception as e:
logging.error(f"Error cleaning up temporary directory {temp_dir}: {str(e)}")

add_cleanup_handler(cleanup_temp_dirs)

logging.info(f"Server initialization complete")
return mcp

Expand All @@ -205,18 +214,17 @@ def cleanup_handler() -> None:
def setup_logging() -> None:
"""Configure logging for the server."""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)


def main() -> None:
"""Start the KiCad MCP server (blocking)."""
setup_logging()
logging.info("Starting KiCad MCP server...")

server = create_server()

try:
server.run() # FastMCP manages its own event loop
except KeyboardInterrupt:
Expand Down
Loading
Loading