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:
parent
25267ab6ee
commit
36584942f7
7 changed files with 148 additions and 146 deletions
|
|
@ -23,152 +23,12 @@ import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
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 dotenv import load_dotenv
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from arcade_mcp_server.logging_utils import intercept_standard_logging
|
from arcade_mcp_server.logging_utils import setup_logging
|
||||||
from arcade_mcp_server.server import MCPServer
|
from arcade_mcp_server.stdio_runner import initialize_tool_catalog, run_stdio_server
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Shared logging utilities for MCP server."""
|
"""Shared logging utilities for MCP server."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
@ -28,3 +29,28 @@ def intercept_standard_logging() -> None:
|
||||||
standard logging calls are intercepted and formatted consistently.
|
standard logging calls are intercepted and formatted consistently.
|
||||||
"""
|
"""
|
||||||
logging.basicConfig(handlers=[LoguruInterceptHandler()], level=0, force=True)
|
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()
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ class MCPApp:
|
||||||
else:
|
else:
|
||||||
self._create_and_run_server(host, port)
|
self._create_and_run_server(host, port)
|
||||||
elif transport == "stdio":
|
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 = ServerTracker()
|
||||||
tracker.track_server_start(
|
tracker.track_server_start(
|
||||||
|
|
|
||||||
116
libs/arcade-mcp-server/arcade_mcp_server/stdio_runner.py
Normal file
116
libs/arcade-mcp-server/arcade_mcp_server/stdio_runner.py
Normal 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()
|
||||||
|
|
@ -25,9 +25,9 @@ from starlette.requests import Request
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
from starlette.types import Receive, Scope, Send
|
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.auth_routes import create_auth_router
|
||||||
from arcade_mcp_server.fastapi.middleware import AddTrailingSlashToPathMiddleware
|
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.base import ResourceServerValidator
|
||||||
from arcade_mcp_server.resource_server.middleware import ResourceServerMiddleware
|
from arcade_mcp_server.resource_server.middleware import ResourceServerMiddleware
|
||||||
from arcade_mcp_server.server import MCPServer
|
from arcade_mcp_server.server import MCPServer
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "arcade-mcp-server"
|
name = "arcade-mcp-server"
|
||||||
version = "1.17.2"
|
version = "1.17.3"
|
||||||
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
|
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{ name = "Arcade.dev" }]
|
authors = [{ name = "Arcade.dev" }]
|
||||||
|
|
|
||||||
|
|
@ -530,7 +530,7 @@ class TestMCPApp:
|
||||||
|
|
||||||
def test_run_stdio_unaffected_by_reload(self, mcp_app: MCPApp):
|
def test_run_stdio_unaffected_by_reload(self, mcp_app: MCPApp):
|
||||||
"""Test run() with stdio transport is unaffected by reload flag."""
|
"""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
|
# Test with reload=True
|
||||||
mcp_app.run(reload=True, transport="stdio")
|
mcp_app.run(reload=True, transport="stdio")
|
||||||
mock_stdio.assert_called_once()
|
mock_stdio.assert_called_once()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue