From 36584942f7397555bfd1e30c273b753fad8a4369 Mon Sep 17 00:00:00 2001 From: Eric Gustin <34000337+EricGustin@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:55:37 -0800 Subject: [PATCH] Fix runtime warning (#771) When `python -m arcade_mcp_server` was executed, we would get the following Runtime Warning: ``` :128: RuntimeWarning: 'arcade_mcp_server.__main__' found in sys.modules after import of package 'arcade_mcp_server', but prior to execution of 'arcade_mcp_server.__main__'; this may result in unpredictable behaviour ``` This PR resolves this. This PR is mainly just moving existing functions to new locations; a refactor --- > [!NOTE] > **Low Risk** > Primarily a module-organization refactor with minimal behavior change; main risk is import-path regressions for internal callers and stdio/CLI startup wiring. > > **Overview** > Fixes the `python -m arcade_mcp_server` runtime warning by refactoring `arcade_mcp_server.__main__` to be a thin CLI entrypoint and moving its reusable logic into import-safe modules. > > Extracts stdio execution and tool discovery into a new `arcade_mcp_server.stdio_runner` (`initialize_tool_catalog`, `run_stdio_server`) and moves `setup_logging` into `logging_utils`, updating `MCPApp`, the FastAPI `worker`, and tests to import from the new locations. Bumps package version to `1.17.3`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 210475acea7c5df44fc66be2bde06f1f0c806c4e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- .../arcade_mcp_server/__main__.py | 144 +----------------- .../arcade_mcp_server/logging_utils.py | 26 ++++ .../arcade_mcp_server/mcp_app.py | 2 +- .../arcade_mcp_server/stdio_runner.py | 116 ++++++++++++++ .../arcade_mcp_server/worker.py | 2 +- libs/arcade-mcp-server/pyproject.toml | 2 +- libs/tests/arcade_mcp_server/test_mcp_app.py | 2 +- 7 files changed, 148 insertions(+), 146 deletions(-) create mode 100644 libs/arcade-mcp-server/arcade_mcp_server/stdio_runner.py diff --git a/libs/arcade-mcp-server/arcade_mcp_server/__main__.py b/libs/arcade-mcp-server/arcade_mcp_server/__main__.py index 389c186b..3ab94528 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/__main__.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/__main__.py @@ -23,152 +23,12 @@ import asyncio import os import sys from pathlib import Path -from typing import Any -from arcade_core.catalog import ToolCatalog -from arcade_core.discovery import discover_tools -from arcade_core.toolkit import ToolkitLoadError from dotenv import load_dotenv from loguru import logger -from arcade_mcp_server.logging_utils import intercept_standard_logging -from arcade_mcp_server.server import MCPServer -from arcade_mcp_server.settings import MCPSettings - - -def setup_logging(level: str = "INFO", stdio_mode: bool = False) -> None: - """Configure logging with Loguru.""" - # Remove existing handlers - logger.remove() - - # In stdio mode, use stderr (stdout is reserved for JSON-RPC) - sink = sys.stderr if stdio_mode else sys.stdout - - # Add handler with appropriate format - if level == "DEBUG": - format_str = "{level: <8} | {time:HH:mm:ss} | {name}:{line} | {message}" - else: - format_str = ( - "{level: <8} | {time:HH:mm:ss} | {message}" - ) - - logger.add( - sink, - format=format_str, - level=level, - colorize=(not stdio_mode), - diagnose=(level == "DEBUG"), - ) - - # Intercept standard logging - intercept_standard_logging() - - -def initialize_tool_catalog( - tool_package: str | None = None, - show_packages: bool = False, - discover_installed: bool = False, - server_name: str | None = None, - server_version: str | None = None, -) -> ToolCatalog: - """ - Discover and load tools from various sources. - - Returns a ToolCatalog or exits with a friendly error if nothing found. - """ - try: - catalog = discover_tools( - tool_package=tool_package, - show_packages=show_packages, - discover_installed=discover_installed, - server_name=server_name, - server_version=server_version, - ) - except ToolkitLoadError as exc: - logger.error(str(exc)) - sys.exit(1) - - total_tools = len(catalog) - if total_tools == 0: - logger.error("No tools found. Create Python files with @tool decorated functions.") - sys.exit(1) - - logger.info(f"Total tools loaded: {total_tools}") - return catalog - - -async def run_stdio_server( - catalog: ToolCatalog, - debug: bool = False, - env_file: str | None = None, - settings: MCPSettings | None = None, - **kwargs: Any, -) -> None: - """Run MCP server with stdio transport.""" - from arcade_mcp_server.transports.stdio import StdioTransport - - # Load settings - # Ensure env from provided .env is loaded for stdio runs as well - if env_file: - load_dotenv(env_file) - logger.debug(f"Loaded environment variables from --env-file={env_file}") - - # Use provided settings or load from environment - if settings is None: - settings = MCPSettings.from_env() - - if debug: - settings.debug = True - settings.middleware.enable_logging = True - settings.middleware.log_level = "DEBUG" - - # Debug log settings and env var names (without values) - try: - tool_env_keys = sorted(settings.tool_secrets().keys()) - logger.debug( - f"Arcade settings: \n\ - ARCADE_ENVIRONMENT={settings.arcade.environment} \n\ - ARCADE_API_URL={settings.arcade.api_url}, \n\ - ARCADE_USER_ID={settings.arcade.user_id}, \n\ - api_key_present - {bool(settings.arcade.api_key)}" - ) - logger.debug(f"Tool environment variable names available to tools: {tool_env_keys}") - except Exception as e: - logger.debug(f"Unable to log settings/tool env keys: {e}") - - # Create server - server = MCPServer( - catalog=catalog, - settings=settings, - **kwargs, - ) - - # Create transport - transport = StdioTransport() - - try: - # Start server and transport - await server.start() - await transport.start() - - # Run connection - async with transport.connect_session() as session: - await server.run_connection( - session.read_stream, - session.write_stream, - session.init_options, - ) - except KeyboardInterrupt: - logger.info("Server stopped by user") - except Exception as e: - logger.exception(f"Server error: {e}") - raise - finally: - # Stop transport and server - try: - await transport.stop() - finally: - await server.stop() +from arcade_mcp_server.logging_utils import setup_logging +from arcade_mcp_server.stdio_runner import initialize_tool_catalog, run_stdio_server def main() -> None: diff --git a/libs/arcade-mcp-server/arcade_mcp_server/logging_utils.py b/libs/arcade-mcp-server/arcade_mcp_server/logging_utils.py index 017fa28f..0b59ebea 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/logging_utils.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/logging_utils.py @@ -1,6 +1,7 @@ """Shared logging utilities for MCP server.""" import logging +import sys from loguru import logger @@ -28,3 +29,28 @@ def intercept_standard_logging() -> None: standard logging calls are intercepted and formatted consistently. """ logging.basicConfig(handlers=[LoguruInterceptHandler()], level=0, force=True) + + +def setup_logging(level: str = "INFO", stdio_mode: bool = False) -> None: + """Configure logging with Loguru.""" + logger.remove() + + # In stdio mode, use stderr (stdout is reserved for JSON-RPC) + sink = sys.stderr if stdio_mode else sys.stdout + + if level == "DEBUG": + format_str = "{level: <8} | {time:HH:mm:ss} | {name}:{line} | {message}" + else: + format_str = ( + "{level: <8} | {time:HH:mm:ss} | {message}" + ) + + logger.add( + sink, + format=format_str, + level=level, + colorize=(not stdio_mode), + diagnose=(level == "DEBUG"), + ) + + intercept_standard_logging() diff --git a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py index 252be942..5864ac1e 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py @@ -340,7 +340,7 @@ class MCPApp: else: self._create_and_run_server(host, port) elif transport == "stdio": - from arcade_mcp_server.__main__ import run_stdio_server + from arcade_mcp_server.stdio_runner import run_stdio_server tracker = ServerTracker() tracker.track_server_start( diff --git a/libs/arcade-mcp-server/arcade_mcp_server/stdio_runner.py b/libs/arcade-mcp-server/arcade_mcp_server/stdio_runner.py new file mode 100644 index 00000000..fe2d8bed --- /dev/null +++ b/libs/arcade-mcp-server/arcade_mcp_server/stdio_runner.py @@ -0,0 +1,116 @@ +""" +Stdio transport runner for MCP server. + +Provides the async entry point for running the MCP server over stdio, +and tool catalog initialization used by both stdio and HTTP modes. +""" + +import sys +from typing import Any + +from arcade_core.catalog import ToolCatalog +from arcade_core.discovery import discover_tools +from arcade_core.toolkit import ToolkitLoadError +from dotenv import load_dotenv +from loguru import logger + +from arcade_mcp_server.server import MCPServer +from arcade_mcp_server.settings import MCPSettings + + +def initialize_tool_catalog( + tool_package: str | None = None, + show_packages: bool = False, + discover_installed: bool = False, + server_name: str | None = None, + server_version: str | None = None, +) -> ToolCatalog: + """ + Discover and load tools from various sources. + + Returns a ToolCatalog or exits with a friendly error if nothing found. + """ + try: + catalog = discover_tools( + tool_package=tool_package, + show_packages=show_packages, + discover_installed=discover_installed, + server_name=server_name, + server_version=server_version, + ) + except ToolkitLoadError as exc: + logger.error(str(exc)) + sys.exit(1) + + total_tools = len(catalog) + if total_tools == 0: + logger.error("No tools found. Create Python files with @tool decorated functions.") + sys.exit(1) + + logger.info(f"Total tools loaded: {total_tools}") + return catalog + + +async def run_stdio_server( + catalog: ToolCatalog, + debug: bool = False, + env_file: str | None = None, + settings: MCPSettings | None = None, + **kwargs: Any, +) -> None: + """Run MCP server with stdio transport.""" + from arcade_mcp_server.transports.stdio import StdioTransport + + if env_file: + load_dotenv(env_file) + logger.debug(f"Loaded environment variables from --env-file={env_file}") + + if settings is None: + settings = MCPSettings.from_env() + + if debug: + settings.debug = True + settings.middleware.enable_logging = True + settings.middleware.log_level = "DEBUG" + + try: + tool_env_keys = sorted(settings.tool_secrets().keys()) + logger.debug( + f"Arcade settings: \n\ + ARCADE_ENVIRONMENT={settings.arcade.environment} \n\ + ARCADE_API_URL={settings.arcade.api_url}, \n\ + ARCADE_USER_ID={settings.arcade.user_id}, \n\ + api_key_present - {bool(settings.arcade.api_key)}" + ) + logger.debug(f"Tool environment variable names available to tools: {tool_env_keys}") + except Exception as e: + logger.debug(f"Unable to log settings/tool env keys: {e}") + + server = MCPServer( + catalog=catalog, + settings=settings, + **kwargs, + ) + + transport = StdioTransport() + + try: + await server.start() + await transport.start() + + async with transport.connect_session() as session: + await server.run_connection( + session.read_stream, + session.write_stream, + session.init_options, + ) + except KeyboardInterrupt: + logger.info("Server stopped by user") + except Exception as e: + logger.exception(f"Server error: {e}") + raise + finally: + try: + await transport.stop() + finally: + await server.stop() diff --git a/libs/arcade-mcp-server/arcade_mcp_server/worker.py b/libs/arcade-mcp-server/arcade_mcp_server/worker.py index e4bb6b9e..326b74c0 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/worker.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/worker.py @@ -25,9 +25,9 @@ from starlette.requests import Request from starlette.responses import Response from starlette.types import Receive, Scope, Send -from arcade_mcp_server.__main__ import setup_logging from arcade_mcp_server.fastapi.auth_routes import create_auth_router from arcade_mcp_server.fastapi.middleware import AddTrailingSlashToPathMiddleware +from arcade_mcp_server.logging_utils import setup_logging from arcade_mcp_server.resource_server.base import ResourceServerValidator from arcade_mcp_server.resource_server.middleware import ResourceServerMiddleware from arcade_mcp_server.server import MCPServer diff --git a/libs/arcade-mcp-server/pyproject.toml b/libs/arcade-mcp-server/pyproject.toml index a19089f9..5facba4c 100644 --- a/libs/arcade-mcp-server/pyproject.toml +++ b/libs/arcade-mcp-server/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "arcade-mcp-server" -version = "1.17.2" +version = "1.17.3" description = "Model Context Protocol (MCP) server framework for Arcade.dev" readme = "README.md" authors = [{ name = "Arcade.dev" }] diff --git a/libs/tests/arcade_mcp_server/test_mcp_app.py b/libs/tests/arcade_mcp_server/test_mcp_app.py index 5977cbdc..3cfa21be 100644 --- a/libs/tests/arcade_mcp_server/test_mcp_app.py +++ b/libs/tests/arcade_mcp_server/test_mcp_app.py @@ -530,7 +530,7 @@ class TestMCPApp: def test_run_stdio_unaffected_by_reload(self, mcp_app: MCPApp): """Test run() with stdio transport is unaffected by reload flag.""" - with patch("arcade_mcp_server.__main__.run_stdio_server") as mock_stdio: + with patch("arcade_mcp_server.stdio_runner.run_stdio_server") as mock_stdio: # Test with reload=True mcp_app.run(reload=True, transport="stdio") mock_stdio.assert_called_once()