Fix runtime warning (#771)

When `python -m arcade_mcp_server` was executed, we would get the
following Runtime Warning:

```
<frozen runpy>: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


<!-- CURSOR_SUMMARY -->
---

> [!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`.
> 
> <sup>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).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
Eric Gustin 2026-02-25 09:55:37 -08:00 committed by GitHub
parent 25267ab6ee
commit 36584942f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 148 additions and 146 deletions

View file

@ -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>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <cyan>{name}:{line}</cyan> | <level>{message}</level>"
else:
format_str = (
"<level>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <level>{message}</level>"
)
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:

View file

@ -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>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <cyan>{name}:{line}</cyan> | <level>{message}</level>"
else:
format_str = (
"<level>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <level>{message}</level>"
)
logger.add(
sink,
format=format_str,
level=level,
colorize=(not stdio_mode),
diagnose=(level == "DEBUG"),
)
intercept_standard_logging()

View file

@ -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(

View file

@ -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()

View file

@ -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

View file

@ -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" }]

View file

@ -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()