diff --git a/src/mcmqtt/main.py b/src/mcmqtt/main.py index 45b7ca7..73025ff 100644 --- a/src/mcmqtt/main.py +++ b/src/mcmqtt/main.py @@ -1,51 +1,20 @@ -"""Main entry point for mcmqtt FastMCP MQTT server.""" +"""FastMCP MQTT Server - Main entry point following FastMCP conventions.""" import asyncio import os -import logging import sys -from pathlib import Path from typing import Optional import typer from rich.console import Console -from rich.logging import RichHandler -import structlog -from .mqtt.types import MQTTConfig, MQTTQoS +from .mqtt.types import MQTTConfig from .mcp.server import MCMQTTServer + # Setup rich console console = Console() -# Setup logging -def setup_logging(log_level: str = "INFO"): - """Set up structured logging with rich output.""" - logging.basicConfig( - level=getattr(logging, log_level.upper()), - format="%(message)s", - datefmt="[%X]", - handlers=[RichHandler(console=console)] - ) - - # Configure structlog - structlog.configure( - processors=[ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.StackInfoRenderer(), - structlog.processors.format_exc_info, - structlog.processors.UnicodeDecoder(), - structlog.processors.JSONRenderer() - ], - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - wrapper_class=structlog.stdlib.BoundLogger, - cache_logger_on_first_use=True, - ) def get_version() -> str: """Get package version.""" @@ -55,6 +24,7 @@ def get_version() -> str: except Exception: return "0.1.0" + def create_mqtt_config_from_env() -> Optional[MQTTConfig]: """Create MQTT configuration from environment variables.""" try: @@ -69,7 +39,6 @@ def create_mqtt_config_from_env() -> Optional[MQTTConfig]: username=os.getenv("MQTT_USERNAME"), password=os.getenv("MQTT_PASSWORD"), keepalive=int(os.getenv("MQTT_KEEPALIVE", "60")), - qos=MQTTQoS(int(os.getenv("MQTT_QOS", "1"))), use_tls=os.getenv("MQTT_USE_TLS", "false").lower() == "true", clean_session=os.getenv("MQTT_CLEAN_SESSION", "true").lower() == "true", reconnect_interval=int(os.getenv("MQTT_RECONNECT_INTERVAL", "5")), @@ -79,18 +48,11 @@ def create_mqtt_config_from_env() -> Optional[MQTTConfig]: console.print(f"[red]Error creating MQTT config from environment: {e}[/red]") return None -# CLI application -app = typer.Typer( - name="mcmqtt", - help="FastMCP MQTT Server - Enabling MQTT integration for MCP clients", - no_args_is_help=True -) -@app.command() -def serve( - host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind the server to"), - port: int = typer.Option(3000, "--port", "-p", help="Port to bind the server to"), - log_level: str = typer.Option("INFO", "--log-level", "-l", help="Log level (DEBUG, INFO, WARNING, ERROR)"), +def main_server( + transport: str = typer.Option("stdio", "--transport", "-t", help="Transport mode: stdio (default) or http"), + host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind the server to (HTTP mode only)"), + port: int = typer.Option(3000, "--port", "-p", help="Port to bind the server to (HTTP mode only)"), mqtt_broker_host: Optional[str] = typer.Option(None, "--mqtt-host", help="MQTT broker hostname"), mqtt_broker_port: int = typer.Option(1883, "--mqtt-port", help="MQTT broker port"), mqtt_client_id: Optional[str] = typer.Option(None, "--mqtt-client-id", help="MQTT client ID"), @@ -98,15 +60,7 @@ def serve( mqtt_password: Optional[str] = typer.Option(None, "--mqtt-password", help="MQTT password"), auto_connect: bool = typer.Option(False, "--auto-connect", help="Automatically connect to MQTT broker on startup") ): - """Start the mcmqtt FastMCP server.""" - # Setup logging - setup_logging(log_level) - logger = structlog.get_logger() - - # Display startup banner - version = get_version() - console.print(f"[bold blue]🎬 mcmqtt FastMCP MQTT Server v{version}[/bold blue]") - console.print(f"[dim]Starting server on {host}:{port}[/dim]") + """mcmqtt FastMCP MQTT Server - Enabling MQTT integration for MCP clients.""" # Create MQTT configuration mqtt_config = None @@ -120,114 +74,46 @@ def serve( username=mqtt_username, password=mqtt_password ) - console.print(f"[green]MQTT Configuration: {mqtt_broker_host}:{mqtt_broker_port}[/green]") else: # Try environment variables mqtt_config = create_mqtt_config_from_env() - if mqtt_config: - console.print(f"[green]MQTT Configuration (from env): {mqtt_config.broker_host}:{mqtt_config.broker_port}[/green]") - else: - console.print("[yellow]No MQTT configuration provided. Use tools to configure at runtime.[/yellow]") # Create and configure server server = MCMQTTServer(mqtt_config) - async def run_server(): - """Run the server with auto-connect if enabled.""" - try: - if auto_connect and mqtt_config: - console.print("[blue]Auto-connecting to MQTT broker...[/blue]") - success = await server.initialize_mqtt_client(mqtt_config) - if success: - await server.connect_mqtt() - console.print("[green]Connected to MQTT broker[/green]") - else: - console.print("[red]Failed to connect to MQTT broker[/red]") - - # Start FastMCP server - await server.run_server(host, port) - - except KeyboardInterrupt: - console.print("\n[yellow]Shutting down server...[/yellow]") - await server.disconnect_mqtt() - except Exception as e: - logger.error("Server error", error=str(e)) - console.print(f"[red]Server error: {e}[/red]") - sys.exit(1) - - # Run the server - try: - asyncio.run(run_server()) - except KeyboardInterrupt: - console.print("\n[yellow]Server stopped[/yellow]") - -@app.command() -def version(): - """Show version information.""" - version_str = get_version() - console.print(f"mcmqtt version: [bold blue]{version_str}[/bold blue]") - -@app.command() -def health( - host: str = typer.Option("localhost", "--host", "-h", help="Server host"), - port: int = typer.Option(3000, "--port", "-p", help="Server port") -): - """Check server health.""" - import httpx - - try: - url = f"http://{host}:{port}/health" - response = httpx.get(url, timeout=10.0) + # Handle MQTT auto-connect if needed + if auto_connect and mqtt_config: + async def connect_mqtt(): + success = await server.initialize_mqtt_client(mqtt_config) + if success: + await server.connect_mqtt() - if response.status_code == 200: - console.print("[green]✅ Server is healthy[/green]") - console.print(response.json()) + try: + asyncio.run(connect_mqtt()) + except Exception as e: + console.print(f"[red]MQTT connection failed: {e}[/red]") + + # Start FastMCP server based on transport + try: + if transport.lower() == "http": + # HTTP mode uses async + async def run_http(): + await server.run_server(host, port) + asyncio.run(run_http()) else: - console.print(f"[red]❌ Server unhealthy (status: {response.status_code})[/red]") - sys.exit(1) - - except httpx.ConnectError: - console.print(f"[red]❌ Cannot connect to server at {host}:{port}[/red]") - sys.exit(1) + # STDIO mode is synchronous and handles its own event loop + server.run_stdio() + except KeyboardInterrupt: + pass except Exception as e: - console.print(f"[red]❌ Health check failed: {e}[/red]") + console.print(f"[red]Server error: {e}[/red]") sys.exit(1) -@app.command() -def config(): - """Show current configuration.""" - setup_logging() - - console.print("[bold blue]Configuration Sources:[/bold blue]") - - # Environment variables - console.print("\n[bold]Environment Variables:[/bold]") - env_vars = [ - "MQTT_BROKER_HOST", "MQTT_BROKER_PORT", "MQTT_CLIENT_ID", - "MQTT_USERNAME", "MQTT_KEEPALIVE", "MQTT_QOS", "MQTT_USE_TLS", - "MCP_SERVER_PORT", "LOG_LEVEL" - ] - - for var in env_vars: - value = os.getenv(var, "[dim]not set[/dim]") - if "PASSWORD" in var and value != "[dim]not set[/dim]": - value = "[dim]***[/dim]" - console.print(f" {var}: {value}") - - # MQTT config from environment - mqtt_config = create_mqtt_config_from_env() - if mqtt_config: - console.print("\n[bold green]MQTT Configuration (parsed):[/bold green]") - console.print(f" Broker: {mqtt_config.broker_host}:{mqtt_config.broker_port}") - console.print(f" Client ID: {mqtt_config.client_id}") - console.print(f" QoS: {mqtt_config.qos.value}") - console.print(f" TLS: {mqtt_config.use_tls}") - else: - console.print("\n[yellow]No valid MQTT configuration found in environment[/yellow]") def main(): - """Main entry point.""" - app() + """Main entry point following FastMCP conventions.""" + typer.run(main_server) + if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/mcmqtt/mcp/server.py b/src/mcmqtt/mcp/server.py index c014a58..0823056 100644 --- a/src/mcmqtt/mcp/server.py +++ b/src/mcmqtt/mcp/server.py @@ -748,6 +748,16 @@ class MCMQTTServer(MCPMixin): logger.error(f"Server error: {e}") raise + def run_stdio(self): + """Run the FastMCP server with STDIO transport (default for MCP clients).""" + try: + # FastMCP's run() method is synchronous and handles its own event loop + self.mcp.run() + + except Exception as e: + logger.error(f"STDIO server error: {e}") + raise + def get_mcp_server(self) -> FastMCP: """Get the FastMCP server instance.""" return self.mcp \ No newline at end of file