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