From 355daaff01ac3e71169e4fed7fdd0702e330bdb9 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 17 Sep 2025 05:53:00 -0600 Subject: [PATCH] Fix package entry point and remove legacy CLI files - Update pyproject.toml to use new Typer-based CLI - Remove old mcmqtt.py and mcmqtt_old.py legacy files - Package now correctly loads modern CLI with proper commands - Version command works: mcmqtt version -> 2025.9.17 - Ready for PyPI publication --- pyproject.toml | 3 +- src/mcmqtt/mcmqtt.py | 86 ---------- src/mcmqtt/mcmqtt_old.py | 330 --------------------------------------- tests/test_main.py | 10 +- 4 files changed, 6 insertions(+), 423 deletions(-) delete mode 100644 src/mcmqtt/mcmqtt.py delete mode 100644 src/mcmqtt/mcmqtt_old.py diff --git a/pyproject.toml b/pyproject.toml index 6fb24b8..4e7e2aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,7 @@ dev = [ ] [project.scripts] -mcmqtt = "mcmqtt.mcmqtt:main" -mcmqtt-server = "mcmqtt.main:main" +mcmqtt = "mcmqtt.main:main" [project.urls] Homepage = "https://git.supported.systems/MCP/mcmqtt" diff --git a/src/mcmqtt/mcmqtt.py b/src/mcmqtt/mcmqtt.py deleted file mode 100644 index 9de5688..0000000 --- a/src/mcmqtt/mcmqtt.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -""" -mcmqtt - FastMCP MQTT Server launcher script. - -Standard MCP server entry point that defaults to STDIO transport -for seamless integration with MCP clients like Claude Desktop. - -This module has been refactored for better testability and maintainability. -""" - -import asyncio -import sys - -import structlog - -from .cli import parse_arguments, get_version -from .config import create_mqtt_config_from_env, create_mqtt_config_from_args -from .logging import setup_logging -from .server import run_stdio_server, run_http_server -from .mcp.server import MCMQTTServer - - -def main(): - """Main entry point for mcmqtt MCP server.""" - args = parse_arguments() - - if args.version: - print(f"mcmqtt version {get_version()}") - sys.exit(0) - - # Setup logging - setup_logging(args.log_level, args.log_file) - logger = structlog.get_logger() - - # Create MQTT configuration - mqtt_config = None - - if args.mqtt_host: - # Use command line arguments - mqtt_config = create_mqtt_config_from_args(args) - if mqtt_config: - logger.info("MQTT configuration from command line", - broker=f"{args.mqtt_host}:{args.mqtt_port}") - else: - # Try environment variables - mqtt_config = create_mqtt_config_from_env() - if mqtt_config: - logger.info("MQTT configuration from environment", - broker=f"{mqtt_config.broker_host}:{mqtt_config.broker_port}") - else: - logger.info("No MQTT configuration provided - use tools to configure at runtime") - - # Create server instance - server = MCMQTTServer(mqtt_config) - - # Log startup info - logger.info("Starting mcmqtt FastMCP server", - version=get_version(), - transport=args.transport, - auto_connect=args.auto_connect) - - # Run server based on transport - try: - if args.transport == "stdio": - asyncio.run(run_stdio_server( - server, - auto_connect=args.auto_connect, - log_file=args.log_file - )) - elif args.transport == "http": - asyncio.run(run_http_server( - server, - host=args.host, - port=args.port, - auto_connect=args.auto_connect - )) - except KeyboardInterrupt: - logger.info("Server stopped by user") - sys.exit(0) - except Exception as e: - logger.error("Failed to start server", error=str(e)) - sys.exit(1) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/mcmqtt/mcmqtt_old.py b/src/mcmqtt/mcmqtt_old.py deleted file mode 100644 index af9434c..0000000 --- a/src/mcmqtt/mcmqtt_old.py +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env python3 -""" -mcmqtt - FastMCP MQTT Server launcher script. - -Standard MCP server entry point that defaults to STDIO transport -for seamless integration with MCP clients like Claude Desktop. -""" - -import asyncio -import logging -import os -import sys -from typing import Optional -import argparse - -import structlog -from fastmcp import FastMCP - -from .mcp.server import MCMQTTServer -from .mqtt.types import MQTTConfig - - -def setup_logging(log_level: str = "WARNING", log_file: Optional[str] = None): - """Set up logging for MCP server.""" - # For STDIO transport, we need to be careful about logging to avoid interfering - # with MCP protocol communication over stdout/stdin - - handlers = [] - - if log_file: - # Log to file when specified - handlers.append(logging.FileHandler(log_file)) - else: - # For STDIO mode, log to stderr to avoid protocol interference - handlers.append(logging.StreamHandler(sys.stderr)) - - logging.basicConfig( - level=getattr(logging, log_level.upper()), - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - handlers=handlers - ) - - # Configure structlog for clean logging - structlog.configure( - processors=[ - structlog.stdlib.filter_by_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.add_log_level, - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.JSONRenderer() - ], - wrapper_class=structlog.stdlib.BoundLogger, - logger_factory=structlog.stdlib.LoggerFactory(), - cache_logger_on_first_use=True, - ) - - -def get_version() -> str: - """Get package version.""" - try: - from importlib.metadata import version - return version("mcmqtt") - except Exception: - return "0.1.0" - - -def create_mqtt_config_from_env() -> Optional[MQTTConfig]: - """Create MQTT configuration from environment variables.""" - try: - broker_host = os.getenv("MQTT_BROKER_HOST") - if not broker_host: - return None - - from .mqtt.types import MQTTQoS - - return MQTTConfig( - broker_host=broker_host, - broker_port=int(os.getenv("MQTT_BROKER_PORT", "1883")), - client_id=os.getenv("MQTT_CLIENT_ID", f"mcmqtt-{os.getpid()}"), - 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")), - max_reconnect_attempts=int(os.getenv("MQTT_MAX_RECONNECT_ATTEMPTS", "10")) - ) - except Exception as e: - logging.error(f"Error creating MQTT config from environment: {e}") - return None - - -async def run_stdio_server( - server: MCMQTTServer, - auto_connect: bool = False, - log_file: Optional[str] = None -): - """Run FastMCP server with STDIO transport.""" - logger = structlog.get_logger() - - try: - # Auto-connect to MQTT if configured and requested - if auto_connect and server.mqtt_config: - logger.info("Auto-connecting to MQTT broker", - broker=f"{server.mqtt_config.broker_host}:{server.mqtt_config.broker_port}") - success = await server.initialize_mqtt_client(server.mqtt_config) - if success: - await server.connect_mqtt() - logger.info("Connected to MQTT broker") - else: - logger.warning("Failed to connect to MQTT broker", error=server._last_error) - - # Get FastMCP instance and run with STDIO transport - mcp = server.get_mcp_server() - - # Run server with STDIO transport (default for MCP) - await mcp.run_stdio_async() - - except KeyboardInterrupt: - logger.info("Server shutting down...") - await server.disconnect_mqtt() - except Exception as e: - logger.error("Server error", error=str(e)) - await server.disconnect_mqtt() - sys.exit(1) - - -async def run_http_server( - server: MCMQTTServer, - host: str = "0.0.0.0", - port: int = 3000, - auto_connect: bool = False -): - """Run FastMCP server with HTTP transport.""" - logger = structlog.get_logger() - - try: - # Auto-connect to MQTT if configured and requested - if auto_connect and server.mqtt_config: - logger.info("Auto-connecting to MQTT broker", - broker=f"{server.mqtt_config.broker_host}:{server.mqtt_config.broker_port}") - success = await server.initialize_mqtt_client(server.mqtt_config) - if success: - await server.connect_mqtt() - logger.info("Connected to MQTT broker") - else: - logger.warning("Failed to connect to MQTT broker", error=server._last_error) - - # Get FastMCP instance and run with HTTP transport - mcp = server.get_mcp_server() - - # Run server with HTTP transport - await mcp.run_http_async(host=host, port=port) - - except KeyboardInterrupt: - logger.info("Server shutting down...") - await server.disconnect_mqtt() - except Exception as e: - logger.error("Server error", error=str(e)) - await server.disconnect_mqtt() - sys.exit(1) - - -def main(): - """Main entry point for mcmqtt MCP server.""" - parser = argparse.ArgumentParser( - description="mcmqtt - FastMCP MQTT Server", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - mcmqtt # Run with STDIO transport (default) - mcmqtt --transport http --port 3000 # Run with HTTP transport - mcmqtt --auto-connect # Auto-connect to MQTT broker - mcmqtt --log-level INFO --log-file mcp.log # Enable logging to file - -Environment Variables: - MQTT_BROKER_HOST MQTT broker hostname - MQTT_BROKER_PORT MQTT broker port (default: 1883) - MQTT_CLIENT_ID MQTT client ID - MQTT_USERNAME MQTT username - MQTT_PASSWORD MQTT password - MQTT_USE_TLS Enable TLS (true/false) - MQTT_QOS QoS level (0, 1, 2) - """ - ) - - # Transport options - parser.add_argument( - "--transport", "-t", - choices=["stdio", "http"], - default="stdio", - help="Transport protocol (default: stdio)" - ) - - # HTTP transport options - parser.add_argument( - "--host", - default="0.0.0.0", - help="Host for HTTP transport (default: 0.0.0.0)" - ) - - parser.add_argument( - "--port", "-p", - type=int, - default=3000, - help="Port for HTTP transport (default: 3000)" - ) - - # MQTT configuration - parser.add_argument( - "--mqtt-host", - help="MQTT broker hostname (overrides MQTT_BROKER_HOST)" - ) - - parser.add_argument( - "--mqtt-port", - type=int, - default=1883, - help="MQTT broker port (default: 1883)" - ) - - parser.add_argument( - "--mqtt-client-id", - help="MQTT client ID" - ) - - parser.add_argument( - "--mqtt-username", - help="MQTT username" - ) - - parser.add_argument( - "--mqtt-password", - help="MQTT password" - ) - - parser.add_argument( - "--auto-connect", - action="store_true", - help="Automatically connect to MQTT broker on startup" - ) - - # Logging options - parser.add_argument( - "--log-level", - choices=["DEBUG", "INFO", "WARNING", "ERROR"], - default="WARNING", - help="Log level (default: WARNING)" - ) - - parser.add_argument( - "--log-file", - help="Log file path (logs to stderr if not specified)" - ) - - # Version - parser.add_argument( - "--version", - action="store_true", - help="Show version and exit" - ) - - args = parser.parse_args() - - if args.version: - print(f"mcmqtt version {get_version()}") - sys.exit(0) - - # Setup logging - setup_logging(args.log_level, args.log_file) - logger = structlog.get_logger() - - # Create MQTT configuration - mqtt_config = None - - if args.mqtt_host: - # Use command line arguments - mqtt_config = MQTTConfig( - broker_host=args.mqtt_host, - broker_port=args.mqtt_port, - client_id=args.mqtt_client_id or f"mcmqtt-{os.getpid()}", - username=args.mqtt_username, - password=args.mqtt_password - ) - logger.info("MQTT configuration from command line", - broker=f"{args.mqtt_host}:{args.mqtt_port}") - else: - # Try environment variables - mqtt_config = create_mqtt_config_from_env() - if mqtt_config: - logger.info("MQTT configuration from environment", - broker=f"{mqtt_config.broker_host}:{mqtt_config.broker_port}") - else: - logger.info("No MQTT configuration provided - use tools to configure at runtime") - - # Create server instance - server = MCMQTTServer(mqtt_config) - - # Log startup info - logger.info("Starting mcmqtt FastMCP server", - version=get_version(), - transport=args.transport, - auto_connect=args.auto_connect) - - # Run server based on transport - try: - if args.transport == "stdio": - asyncio.run(run_stdio_server( - server, - auto_connect=args.auto_connect, - log_file=args.log_file - )) - elif args.transport == "http": - asyncio.run(run_http_server( - server, - host=args.host, - port=args.port, - auto_connect=args.auto_connect - )) - except KeyboardInterrupt: - logger.info("Server stopped by user") - sys.exit(0) - except Exception as e: - logger.error("Failed to start server", error=str(e)) - sys.exit(1) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index f748520..69afd5e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -52,16 +52,16 @@ class TestCLI: assert result.exit_code == 1 assert "Cannot connect to server" in result.stdout - @patch('mcmqtt.main.uvicorn.Server') @patch('mcmqtt.main.MCMQTTServer') - def test_serve_command_basic(self, mock_server_class, mock_uvicorn_server): + def test_serve_command_basic(self, mock_server_class): """Test basic serve command.""" mock_server = MagicMock() + mock_server.run_server = MagicMock() + mock_server.initialize_mqtt_client = MagicMock() + mock_server.connect_mqtt = MagicMock() + mock_server.disconnect_mqtt = MagicMock() mock_server_class.return_value = mock_server - mock_uvicorn_instance = MagicMock() - mock_uvicorn_server.return_value = mock_uvicorn_instance - runner = CliRunner() # Mock asyncio.run to avoid actually starting server