Skip to content

mcp/server API

dnallm.mcp.server

DNALLM MCP Server Implementation.

This module implements the main MCP (Model Context Protocol) server using the FastMCP framework with Server-Sent Events (SSE) support for real-time DNA sequence prediction.

The server provides a comprehensive set of tools for DNA sequence analysis, including: - Single sequence prediction with specific models - Batch processing of multiple sequences - Multi-model prediction and comparison - Real-time streaming predictions with progress updates - Model management and health monitoring

Architecture

The server is built on top of the FastMCP framework, which provides MCP protocol implementation with multiple transport options (stdio, SSE, HTTP). The server manages DNA large language models through a ModelManager and handles configuration through a ConfigManager.

Transport Protocols
  • stdio: Standard input/output for CLI tools
  • streamable-http: HTTP-based streaming protocol (recommended for remote connections, per MCP spec 2025-11-25)
  • sse: Server-Sent Events for real-time web applications (legacy, deprecated in MCP spec 2025-11-25 but retained for backward compatibility)
Example

Basic server initialization:

server = DNALLMMCPServer("config/server_config.yaml")
await server.initialize()
server.start_server(host="127.0.0.1", port=8000,
                    transport="streamable-http")
Note

This server requires proper configuration files and model setup before initialization. See the configuration documentation for details.

Classes

DNALLMMCPServer

DNALLMMCPServer(config_path)

DNALLM MCP Server implementation using FastMCP framework with SSE support.

This class provides a comprehensive MCP server for DNA large language model inference and analysis. It supports multiple transport protocols and provides real-time streaming capabilities for DNA sequence prediction tasks.

The server manages multiple DNA large language models and provides various prediction modes including single sequence prediction, batch processing, and multi-model comparison. All operations support progress reporting through streaming transports for real-time user feedback.

Attributes:

Name Type Description
config_path str

Path to the main server configuration file

config_manager MCPConfigManager

Handles configuration management

model_manager ModelManager

Manages model loading and prediction

app FastMCP | None

Main FastMCP application instance

sse_app

SSE application instance (unused, FastMCP handles SSE internally)

_initialized bool

Server initialization status flag

Example

Initialize and start the server:

# Create server instance
server = DNALLMMCPServer("config/mcp_server_config.yaml")

# Initialize asynchronously
await server.initialize()

# Start with Streamable HTTP transport (recommended)
server.start_server(host="0.0.0.0", port=8000,
                   transport="streamable-http")

# Start with SSE transport (legacy, backward compatible)
server.start_server(host="0.0.0.0", port=8000,
                   transport="sse")
Note

The server must be initialized before starting. Configuration files must contain valid model and server settings.

Initialize the MCP server instance.

Sets up the server with configuration and model managers, but does not load models or start the server. Call initialize() and start_server() separately for complete setup.

Parameters:

Name Type Description Default
config_path str

Absolute or relative path to the main MCP server configuration file. This file should contain server settings, model configurations, and transport options.

required

Raises:

Type Description
FileNotFoundError

If the configuration file doesn't exist

ConfigurationError

If the configuration file is invalid

Example
server = DNALLMMCPServer("/path/to/config.yaml")
Note

The configuration directory and filename are extracted separately to support the MCPConfigManager's directory-based configuration loading strategy.

Source code in dnallm/mcp/server.py
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def __init__(self, config_path: str) -> None:
    """Initialize the MCP server instance.

    Sets up the server with configuration and model managers, but does not
    load models or start the server. Call initialize() and start_server()
    separately for complete setup.

    Args:
        config_path (str): Absolute or relative path to the main MCP
            server configuration file. This file should contain server
            settings, model configurations, and transport options.

    Raises:
        FileNotFoundError: If the configuration file doesn't exist
        ConfigurationError: If the configuration file is invalid

    Example:
        ```python
        server = DNALLMMCPServer("/path/to/config.yaml")
        ```

    Note:
        The configuration directory and filename are extracted
        separately to support the MCPConfigManager's directory-based
        configuration loading strategy.
    """
    self.config_path = config_path
    # Extract directory and filename from config file path for
    # ConfigManager. MCPConfigManager requires separate directory and
    # filename parameters
    config_path_obj = Path(config_path)
    config_dir = config_path_obj.parent
    config_filename = config_path_obj.name
    # Initialize core components
    self.config_manager = MCPConfigManager(str(config_dir), config_filename)
    self.model_manager = ModelManager(self.config_manager)

    # FastMCP application instances
    self.app: FastMCP | None = None  # Main MCP application
    self.sse_app = None  # Not used - FastMCP handles SSE internally

    # Server state tracking
    self._initialized = False  # Prevents double initialization

    # WR-01 (261003-ij4): dna_interpret offloads its captum work to the
    # default executor; this lock is acquired INSIDE the
    # executor-submitted closure (the CR-01 / 261003-hhj pattern), so a
    # tool-timeout cancellation abandons only the await — the orphaned
    # thread keeps the flight, a client retry queues behind it, and at
    # most one interpretation runs per process at any instant,
    # preventing unbounded orphan stacking via timeout→retry loops.
    # Deliberately NOT _infer_thread_lock: DNAInterpret uses no
    # DataLoader (no fork-unsafe window) and attributions must not
    # queue behind minutes-long predicts.
    self._interpret_thread_lock = threading.Lock()
Methods:
get_server_info
get_server_info()

Get server information.

Source code in dnallm/mcp/server.py
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
def get_server_info(self) -> dict[str, Any]:
    """Get server information."""
    server_config = self.config_manager.get_server_config()
    if not server_config:
        return {"error": "Server configuration not loaded"}

    return {
        "name": server_config.mcp.name,
        "version": server_config.mcp.version,
        "description": server_config.mcp.description,
        "host": server_config.server.host,
        "port": server_config.server.port,
        "loaded_models": self.model_manager.get_loaded_models(),
        "enabled_models": self.config_manager.get_enabled_models(),
        "initialized": self._initialized,
    }
initialize async
initialize()

Initialize the server and load all enabled models.

This method performs the complete server initialization process: 1. Checks if already initialized (idempotent operation) 2. Loads and validates server configuration 3. Creates the FastMCP application instance 4. Registers all MCP tools 5. Loads all enabled DNA large language models

The initialization is asynchronous because model loading can be time-consuming, especially for large transformer models.

Raises:

Type Description
RuntimeError

If server configuration cannot be loaded

ModelLoadError

If critical models fail to load

ConfigurationError

If configuration is invalid

Example
server = DNALLMMCPServer("config.yaml")
await server.initialize()  # Required before starting
Note

This method is idempotent - calling it multiple times has no additional effect after the first successful initialization.

Source code in dnallm/mcp/server.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
async def initialize(self) -> None:
    """Initialize the server and load all enabled models.

    This method performs the complete server initialization process:
    1. Checks if already initialized (idempotent operation)
    2. Loads and validates server configuration
    3. Creates the FastMCP application instance
    4. Registers all MCP tools
    5. Loads all enabled DNA large language models

    The initialization is asynchronous because model loading can be
    time-consuming, especially for large transformer models.

    Raises:
        RuntimeError: If server configuration cannot be loaded
        ModelLoadError: If critical models fail to load
        ConfigurationError: If configuration is invalid

    Example:
        ```python
        server = DNALLMMCPServer("config.yaml")
        await server.initialize()  # Required before starting
        ```

    Note:
        This method is idempotent - calling it multiple times has no
        additional effect after the first successful initialization.
    """
    # Check for duplicate initialization
    if self._initialized:
        logger.info("Server already initialized")
        return

    logger.info("Initializing DNALLM MCP Server...")

    # Load and validate server configuration
    server_config = self.config_manager.get_server_config()
    if not server_config:
        raise RuntimeError("Failed to load server configuration")

    # Load timeout and logging configuration
    timeout_config = self.config_manager.get_timeout_config()
    self._tool_timeout_seconds = timeout_config.get("tool_timeout_seconds", 30)

    logging_config = self.config_manager.get_logging_config()
    self._log_format = logging_config.get("log_format", "text")

    # Create FastMCP application with configuration
    self.app = FastMCP(
        name=server_config.mcp.name,
        instructions=server_config.mcp.description,
    )

    # Register all available MCP tools
    self._register_tools()

    # Load all enabled models asynchronously
    await self.model_manager.load_all_enabled_models()

    # SSE transport is built into FastMCP framework
    # No need for separate SSE application setup

    # Mark server as initialized
    self._initialized = True
    logger.info("DNALLM MCP Server initialized successfully")
shutdown async
shutdown()

Shutdown the server and cleanup resources.

Source code in dnallm/mcp/server.py
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
async def shutdown(self) -> None:
    """Shutdown the server and cleanup resources."""
    logger.info("Shutting down DNALLM MCP Server...")

    # Unload all models
    unloaded_count = self.model_manager.unload_all_models()
    logger.info(f"Unloaded {unloaded_count} models during shutdown")

    self._initialized = False
    logger.info("DNALLM MCP Server shutdown complete")
start_server
start_server(host=None, port=None, transport='stdio')

Start the MCP server with the specified transport protocol.

This method starts the server using one of the supported transport protocols. The server must be initialized before calling this method. The transport protocol determines how the server communicates with clients:

  • stdio: Standard input/output for CLI tools and automation
  • streamable-http: HTTP-based streaming for REST API integration (recommended per MCP spec 2025-11-25)
  • sse: Server-Sent Events (legacy, deprecated in MCP spec 2025-11-25 but retained for backward compatibility)

Session management for the streamable-http transport is handled entirely by the FastMCP SDK via StreamableHTTPSessionManager. Callers do not need to create, track, or clean up MCP-Session-Id headers manually.

Parameters:

Name Type Description Default
host str | None

Host address to bind the server to. When None (the default), the value is resolved per the documented chain: transport-specific YAML (streamable_http on the streamable-http transport) > server YAML > DEFAULT_BIND_HOST ("127.0.0.1"). An explicitly-passed value always takes precedence over every YAML block (Phase 12 REV-11 fix — previously YAML silently overrode the CLI flags). Use "0.0.0.0" to bind all interfaces.

None
port int | None

Port number to bind the server to. Resolved with the same precedence chain when None, falling back to DEFAULT_BIND_PORT (8000). Only used for HTTP-based transports.

None
transport str

Transport protocol to use. Choices: "stdio", "streamable-http", "sse". Defaults to "stdio".

'stdio'

Raises:

Type Description
RuntimeError

If server is not initialized before starting

OSError

If port is already in use or host is invalid

ConfigurationError

If transport configuration is invalid

Example
# Start with Streamable HTTP (recommended)
server.start_server(
    host="0.0.0.0",
    port=8000,
    transport="streamable-http"
)

# Start with SSE (legacy, backward compatible)
server.start_server(
    host="0.0.0.0",
    port=8000,
    transport="sse"
)

# Start with stdio for CLI tools
server.start_server(transport="stdio")
Note

This method is blocking and will run until the server is stopped. For SSE and HTTP transports, uvicorn handles graceful shutdown on SIGINT/SIGTERM signals.

Source code in dnallm/mcp/server.py
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
def start_server(
    self,
    host: str | None = None,
    port: int | None = None,
    transport: str = "stdio",
) -> None:
    """Start the MCP server with the specified transport protocol.

    This method starts the server using one of the supported transport
    protocols. The server must be initialized before calling this method.
    The transport protocol determines how the server communicates with
    clients:

    - stdio: Standard input/output for CLI tools and automation
    - streamable-http: HTTP-based streaming for REST API integration
      (recommended per MCP spec 2025-11-25)
    - sse: Server-Sent Events (legacy, deprecated in MCP spec 2025-11-25
      but retained for backward compatibility)

    Session management for the ``streamable-http`` transport is handled
    entirely by the FastMCP SDK via ``StreamableHTTPSessionManager``.
    Callers do not need to create, track, or clean up ``MCP-Session-Id``
    headers manually.

    Args:
        host (str | None, optional): Host address to bind the server to.
            When ``None`` (the default), the value is resolved per the
            documented chain: transport-specific YAML (``streamable_http``
            on the streamable-http transport) > ``server`` YAML >
            ``DEFAULT_BIND_HOST`` ("127.0.0.1"). An explicitly-passed
            value always takes precedence over every YAML block
            (Phase 12 REV-11 fix — previously YAML silently overrode
            the CLI flags). Use "0.0.0.0" to bind all interfaces.
        port (int | None, optional): Port number to bind the server to.
            Resolved with the same precedence chain when ``None``,
            falling back to ``DEFAULT_BIND_PORT`` (8000). Only used for
            HTTP-based transports.
        transport (str, optional): Transport protocol to use.
            Choices: "stdio", "streamable-http", "sse".
            Defaults to "stdio".

    Raises:
        RuntimeError: If server is not initialized before starting
        OSError: If port is already in use or host is invalid
        ConfigurationError: If transport configuration is invalid

    Example:
        ```python
        # Start with Streamable HTTP (recommended)
        server.start_server(
            host="0.0.0.0",
            port=8000,
            transport="streamable-http"
        )

        # Start with SSE (legacy, backward compatible)
        server.start_server(
            host="0.0.0.0",
            port=8000,
            transport="sse"
        )

        # Start with stdio for CLI tools
        server.start_server(transport="stdio")
        ```

    Note:
        This method is blocking and will run until the server is stopped.
        For SSE and HTTP transports, uvicorn handles graceful shutdown
        on SIGINT/SIGTERM signals.
    """
    # Validate server initialization state
    if not self._initialized:
        raise RuntimeError("Server not initialized. Call initialize() first.")

    # Resolve the bind address ONCE (Phase 12 REV-11): CLI-explicit
    # values win over YAML on both HTTP transports; both starters below
    # receive the final values. The old unconditional YAML override here
    # silently discarded the CLI flags, and the streamable-http starter
    # then always overrode them again with the streamable_http block.
    host, port = self._resolve_bind_address(host, port, transport)

    logger.info(f"Starting DNALLM MCP Server on {host}:{port} with {transport} transport")

    # Validate transport before dispatching
    valid_transports = ("stdio", "sse", "streamable-http")
    if transport not in valid_transports:
        raise ValueError(
            f"Invalid transport: {transport!r}. Must be one of: {valid_transports}"
        )

    # Dispatch to appropriate transport handler
    if transport == "sse":
        self._start_sse_server(host, port)
    elif transport == "streamable-http":
        self._start_http_server(host, port)
    else:
        # Default to stdio transport
        self._start_stdio_server()

Functions:

initialize_mcp_server async

initialize_mcp_server(config_path)

Initialize the MCP server asynchronously.

This is a convenience function that creates and initializes a DNALLMMCPServer instance. It's designed to be called from asyncio.run() or other async contexts.

Parameters:

Name Type Description Default
config_path str

Path to the server configuration file

required

Returns:

Name Type Description
DNALLMMCPServer DNALLMMCPServer

Fully initialized server instance ready to start

Raises:

Type Description
ConfigurationError

If configuration is invalid

ModelLoadError

If model loading fails

RuntimeError

If initialization fails

Example
server = await initialize_mcp_server("config.yaml")
server.start_server(transport="sse")
Note

This function combines server creation and initialization for convenience in async entry points.

Source code in dnallm/mcp/server.py
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
async def initialize_mcp_server(config_path: str) -> DNALLMMCPServer:
    """Initialize the MCP server asynchronously.

    This is a convenience function that creates and initializes a
    DNALLMMCPServer instance. It's designed to be called from
    asyncio.run() or other async contexts.

    Args:
        config_path (str): Path to the server configuration file

    Returns:
        DNALLMMCPServer: Fully initialized server instance ready to start

    Raises:
        ConfigurationError: If configuration is invalid
        ModelLoadError: If model loading fails
        RuntimeError: If initialization fails

    Example:
        ```python
        server = await initialize_mcp_server("config.yaml")
        server.start_server(transport="sse")
        ```

    Note:
        This function combines server creation and initialization
        for convenience in async entry points.
    """
    server = DNALLMMCPServer(str(config_path))
    await server.initialize()
    return server

main

main()

Main entry point for the DNALLM MCP server CLI.

This function provides a command-line interface for starting the DNALLM MCP server with various configuration options. It handles argument parsing, configuration validation, server initialization, and graceful error handling.

The CLI supports multiple transport protocols and comprehensive configuration options for production deployment. It includes proper error handling and logging for troubleshooting.

Command Line Arguments

--config: Path to server configuration file --host: Host to bind HTTP/SSE transports to; when omitted the value resolves from the YAML config (streamable_http block on that transport, else the server block), falling back to 127.0.0.1 --port: Port to bind HTTP/SSE transports to; same resolution chain as --host, falling back to 8000 --transport: Protocol (stdio/sse/streamable-http, default: stdio) --log-level: Logging verbosity (DEBUG/INFO/WARNING/ERROR/CRITICAL) --version: Display version information

Example Usage
# Start with SSE transport
python server.py --config config.yaml --transport sse --port 8000

# Start with stdio (default)
python server.py --config config.yaml

# Start with debug logging
python server.py --config config.yaml --log-level DEBUG
Exit Codes

0: Successful execution 1: Configuration file not found or server error

Note

The server runs in blocking mode. Use Ctrl+C to stop gracefully. For SSE/HTTP transports, uvicorn handles signal processing automatically.

Source code in dnallm/mcp/server.py
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
def main():
    """Main entry point for the DNALLM MCP server CLI.

    This function provides a command-line interface for starting the DNALLM
    MCP server with various configuration options. It handles argument parsing,
    configuration validation, server initialization, and graceful error
    handling.

    The CLI supports multiple transport protocols and comprehensive
    configuration
    options for production deployment. It includes proper error handling and
    logging for troubleshooting.

    Command Line Arguments:
        --config: Path to server configuration file
        --host: Host to bind HTTP/SSE transports to; when omitted the value
            resolves from the YAML config (streamable_http block on that
            transport, else the server block), falling back to 127.0.0.1
        --port: Port to bind HTTP/SSE transports to; same resolution chain
            as --host, falling back to 8000
        --transport: Protocol (stdio/sse/streamable-http, default: stdio)
        --log-level: Logging verbosity (DEBUG/INFO/WARNING/ERROR/CRITICAL)
        --version: Display version information

    Example Usage:
        ```bash
        # Start with SSE transport
        python server.py --config config.yaml --transport sse --port 8000

        # Start with stdio (default)
        python server.py --config config.yaml

        # Start with debug logging
        python server.py --config config.yaml --log-level DEBUG
        ```

    Exit Codes:
        0: Successful execution
        1: Configuration file not found or server error

    Note:
        The server runs in blocking mode. Use Ctrl+C to stop gracefully.
        For SSE/HTTP transports, uvicorn handles signal processing
        automatically.
    """
    import asyncio
    import argparse
    import sys
    from pathlib import Path

    parser = argparse.ArgumentParser(
        description="Start the DNALLM MCP (Model Context Protocol) server",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  dnallm-mcp-server --config dnallm/mcp/configs/mcp_server_config.yaml
  dnallm-mcp-server --config dnallm/mcp/configs/mcp_server_config_2.yaml \
      --transport sse --port 8000
  dnallm-mcp-server --config dnallm/mcp/configs/mcp_server_config.yaml \
      --host 127.0.0.1 --port 9000
        """,
    )

    parser.add_argument(
        "--config",
        "-c",
        type=str,
        default="dnallm/mcp/configs/mcp_server_config.yaml",
        help="Path to MCP server configuration file (default: %(default)s)",
    )

    parser.add_argument(
        "--host",
        type=str,
        default=None,
        help=(
            "Host to bind HTTP/SSE transports to. Resolution order when "
            "omitted: transport-specific YAML (streamable_http block) > "
            "server YAML > 127.0.0.1. An explicit flag always wins over "
            "the YAML config."
        ),
    )

    parser.add_argument(
        "--port",
        type=int,
        default=None,
        help=(
            "Port to bind HTTP/SSE transports to. Resolution order when "
            "omitted: transport-specific YAML (streamable_http block) > "
            "server YAML > 8000. An explicit flag always wins over the "
            "YAML config."
        ),
    )

    parser.add_argument(
        "--log-level",
        type=str,
        choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
        default="INFO",
        help="Logging level (default: %(default)s)",
    )

    parser.add_argument(
        "--transport",
        type=str,
        choices=["stdio", "sse", "streamable-http"],
        default="stdio",
        help=(
            "Transport protocol (streamable-http=recommended per MCP "
            "2025-11-25, sse=legacy, stdio=default) (default: %(default)s)"
        ),
    )

    parser.add_argument("--version", action="version", version="DNALLM MCP Server 1.0.0")

    args = parser.parse_args()

    # Check if config file exists
    config_path = Path(args.config)
    if not config_path.exists():
        logger.error(f"Configuration file not found: {config_path}")
        logger.error("Please create a configuration file or specify the correct path with --config")
        sys.exit(1)

    try:
        logger.info("Starting DNALLM MCP Server...")
        logger.info(f"Configuration: {config_path}")
        logger.info(
            f"Host: {args.host if args.host is not None else '(from config, default 127.0.0.1)'}"
        )
        logger.info(
            f"Port: {args.port if args.port is not None else '(from config, default 8000)'}"
        )
        logger.info(f"Transport: {args.transport}")
        logger.info(f"Log Level: {args.log_level}")
        logger.info("-" * 50)

        # Initialize server in asyncio context
        server = asyncio.run(initialize_mcp_server(str(config_path)))

        # Get server info
        info = server.get_server_info()
        logger.info(f"Server initialized: {info['name']} v{info['version']}")
        logger.info(f"Loaded models: {info['loaded_models']}")
        logger.info(f"Enabled models: {info['enabled_models']}")
        logger.info("-" * 50)

        # Start server - let uvicorn handle signals for HTTP/SSE transports
        logger.info(f"Starting server with {args.transport} transport")
        logger.info("Press Ctrl+C to stop the server")

        # Start server (uvicorn will handle signals properly)
        server.start_server(host=args.host, port=args.port, transport=args.transport)

    except KeyboardInterrupt:
        logger.info("\nReceived interrupt signal, shutting down...")
    except Exception as e:
        logger.error(f"Server error: {e}")
        import traceback

        traceback.print_exc()
        sys.exit(1)
    finally:
        logger.info("Server stopped")