import argparse import logging import os import sys import uvicorn from mcp.server.fastmcp import FastMCP def run_mcp_server( mcp: FastMCP, default_port: int = 8000, default_host: str = "127.0.0.1" ): """Standardize the startup process for ChemGraph MCP servers. Supports 'stdio' (default) and 'sse' (HTTP) transports. Ensures logging is correctly routed to stderr to avoid corrupting stdio transport. Parameters ---------- mcp : FastMCP MCP server instance to run. default_port : int, optional Default port for streamable HTTP transport. default_host : str, optional Default host for streamable HTTP transport. """ parser = argparse.ArgumentParser(description=f"Run {mcp.name} MCP Server") parser.add_argument( "--transport", choices=["stdio", "streamable_http"], default="stdio", help="Transport protocol to use: 'stdio' (default) or 'streamable_http' (HTTP/SSE).", ) parser.add_argument( "--port", type=int, default=default_port, help=f"Port for streamable_http transport (default: {default_port})", ) parser.add_argument( "--host", type=str, default=default_host, help=f"Host for streamable_http transport (default: {default_host})", ) args = parser.parse_args() # Configure logging to write to stderr. # This is CRITICAL for stdio transport mode, as stdout is used for communication. logger = logging.getLogger() logger.setLevel(logging.INFO) formatter = logging.Formatter( "%(asctime)s - %(levelname)s - %(name)s - %(message)s" ) # Always log to stderr stderr_handler = logging.StreamHandler(sys.stderr) stderr_handler.setFormatter(formatter) logger.addHandler(stderr_handler) # If CHEMGRAPH_LOG_DIR is set, also log to a file log_dir = os.environ.get("CHEMGRAPH_LOG_DIR") if log_dir: try: os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, f"mcp_server_{mcp.name}.log") file_handler = logging.FileHandler(log_file) file_handler.setFormatter(formatter) logger.addHandler(file_handler) logging.info(f"Logging to file: {log_file}") except Exception as e: logging.error(f"Failed to setup file logging to {log_dir}: {e}") if args.transport == "streamable_http": logging.info( "Starting %s on %s:%s via streamable_http transport...", mcp.name, args.host, args.port, ) # FastMCP.streamable_http_app() returns a Starlette/FastAPI-compatible app app = mcp.streamable_http_app() uvicorn.run(app, host=args.host, port=args.port) else: logging.info("Starting %s via stdio transport...", mcp.name) # FastMCP.run(transport='stdio') handles the stdio loop mcp.run(transport="stdio") # --------------------------------------------------------------------------- # Parsl HPC configuration loader # --------------------------------------------------------------------------- def load_parsl_config(system_name: str): """Dynamically import and return a Parsl config for the given HPC system. Parameters ---------- system_name : str Target system name. Supported: ``polaris``, ``aurora``. Returns ------- parsl.config.Config Parsl configuration object for the requested system. Raises ------ ValueError If the system name is not recognised. """ system_name = system_name.lower() run_dir = os.getcwd() logging.info("Initializing Parsl for system: %s", system_name) if system_name == "polaris": from chemgraph.hpc_configs.polaris_parsl import get_polaris_config return get_polaris_config(run_dir=run_dir) elif system_name == "aurora": from chemgraph.hpc_configs.aurora_parsl import get_aurora_config return get_aurora_config(run_dir=run_dir) else: raise ValueError( f"Unknown system specified: '{system_name}'. Supported: polaris, aurora" )