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 7ae9081a..bad6d94a 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py @@ -6,6 +6,7 @@ Provides a clean, minimal API for building MCP servers with lazy initialization. from __future__ import annotations +import os import sys from pathlib import Path from typing import Any, Callable, Literal, ParamSpec, TypeVar @@ -210,6 +211,8 @@ class MCPApp: logger.info(f"Starting {self.name} v{self.version} with {len(self._catalog)} tools") + host, port, transport = MCPApp._get_configuration_overrides(host, port, transport) + if transport in ["http", "streamable-http", "streamable"]: run_arcade_mcp( catalog=self._catalog, @@ -232,6 +235,37 @@ class MCPApp: else: raise ServerError(f"Invalid transport: {transport}") + @staticmethod + def _get_configuration_overrides( + host: str, port: int, transport: TransportType + ) -> tuple[str, int, TransportType]: + """Get configuration overrides from environment variables.""" + if envvar_transport := os.getenv("ARCADE_SERVER_TRANSPORT"): + transport = envvar_transport + logger.debug( + f"Using '{transport}' as transport from ARCADE_SERVER_TRANSPORT environment variable" + ) + + # host and port are only relevant for HTTP Streamable transport + if transport in ["http", "streamable-http", "streamable"]: + if envvar_host := os.getenv("ARCADE_SERVER_HOST"): + host = envvar_host + logger.debug(f"Using '{host}' as host from ARCADE_SERVER_HOST environment variable") + + if envvar_port := os.getenv("ARCADE_SERVER_PORT"): + try: + port = int(envvar_port) + except ValueError: + logger.warning( + f"Invalid port: '{envvar_port}' from ARCADE_SERVER_PORT environment variable. Using default port {port}" + ) + else: + logger.debug( + f"Using '{port}' as port from ARCADE_SERVER_PORT environment variable" + ) + + return host, port, transport + class _ToolsAPI: """Unified tools API for MCPApp (build-time and runtime).""" diff --git a/libs/tests/arcade_mcp_server/test_mcp_app.py b/libs/tests/arcade_mcp_server/test_mcp_app.py index cdfab6ac..dca812ce 100644 --- a/libs/tests/arcade_mcp_server/test_mcp_app.py +++ b/libs/tests/arcade_mcp_server/test_mcp_app.py @@ -179,3 +179,58 @@ class TestMCPApp: # Test removing a resource at runtime removed_resource = await mcp_app.resources.remove("file:///test.txt") assert removed_resource.uri == "file:///test.txt" + + def test_get_configuration_overrides(self, monkeypatch): + """Test configuration overrides from environment variables.""" + # Ensure environment variables are clear at the start + monkeypatch.delenv("ARCADE_SERVER_TRANSPORT", raising=False) + monkeypatch.delenv("ARCADE_SERVER_HOST", raising=False) + monkeypatch.delenv("ARCADE_SERVER_PORT", raising=False) + + # Test default values (no environment variables) + host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http") + assert host == "127.0.0.1" + assert port == 8000 + assert transport == "http" + + # Test transport override + monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "stdio") + host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http") + assert transport == "stdio" + monkeypatch.delenv("ARCADE_SERVER_TRANSPORT") + + # Test host override (only works with HTTP transport) + monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "http") + monkeypatch.setenv("ARCADE_SERVER_HOST", "192.168.1.1") + host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http") + assert host == "192.168.1.1" + assert transport == "http" + monkeypatch.delenv("ARCADE_SERVER_HOST") + monkeypatch.delenv("ARCADE_SERVER_TRANSPORT") + + # Test port override (only works with HTTP transport) + monkeypatch.setenv("ARCADE_SERVER_PORT", "9000") + host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http") + assert port == 9000 + monkeypatch.delenv("ARCADE_SERVER_PORT") + + # Test invalid port value + monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "http") + monkeypatch.setenv("ARCADE_SERVER_PORT", "invalid_port") + host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http") + assert port == 8000 # Should keep the default value + monkeypatch.delenv("ARCADE_SERVER_PORT") + monkeypatch.delenv("ARCADE_SERVER_TRANSPORT") + + # Test host/port with stdio transport + monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "stdio") + monkeypatch.setenv("ARCADE_SERVER_HOST", "192.168.1.1") + monkeypatch.setenv("ARCADE_SERVER_PORT", "9000") + host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http") + # For stdio, host and port are still returned but not used by the server + assert host == "127.0.0.1" # Host should remain unchanged for stdio transport + assert port == 8000 # Port should remain unchanged for stdio transport + assert transport == "stdio" + monkeypatch.delenv("ARCADE_SERVER_HOST") + monkeypatch.delenv("ARCADE_SERVER_PORT") + monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")