From 83c0eeab2b6f935b4c76840bdd4ca973834d5359 Mon Sep 17 00:00:00 2001 From: Eric Gustin <34000337+EricGustin@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:04:18 -0700 Subject: [PATCH] Fix server info bug (#614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name, title, version, etc. for an `MCPApp` were being overwritten by its internal `MCPServer`. ⚠️ this is blocking `arcade deploy` from working --- .../arcade_mcp_server/mcp_app.py | 14 +- .../arcade_mcp_server/server.py | 32 +++-- .../arcade_mcp_server/settings.py | 2 +- .../arcade_mcp_server/worker.py | 38 ++++-- libs/tests/arcade_mcp_server/test_mcp_app.py | 40 ++++++ libs/tests/arcade_mcp_server/test_server.py | 124 +++++++++++++++++- libs/tests/arcade_mcp_server/test_settings.py | 100 ++++++++++++++ 7 files changed, 323 insertions(+), 27 deletions(-) create mode 100644 libs/tests/arcade_mcp_server/test_settings.py 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 7e77d027..4af3a8a8 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py @@ -20,6 +20,7 @@ from loguru import logger from arcade_mcp_server.exceptions import ServerError from arcade_mcp_server.server import MCPServer +from arcade_mcp_server.settings import MCPSettings, ServerSettings from arcade_mcp_server.types import Prompt, PromptMessage, Resource from arcade_mcp_server.worker import run_arcade_mcp @@ -59,7 +60,7 @@ class MCPApp: def __init__( self, name: str = "ArcadeMCP", - version: str = "1.0.0dev", + version: str = "0.1.0", title: str | None = None, instructions: str | None = None, log_level: str = "INFO", @@ -102,6 +103,15 @@ class MCPApp: # Public handle to the MCPServer (set by caller for runtime ops) self.server: MCPServer | None = None + self._mcp_settings = MCPSettings( + server=ServerSettings( + name=self.name, + version=self.version, + title=self.title, + instructions=self.instructions, + ) + ) + self._load_env() if not logger._core.handlers: # type: ignore[attr-defined] self._setup_logging(transport == "stdio") @@ -227,6 +237,7 @@ class MCPApp: host=host, port=port, reload=reload, + mcp_settings=self._mcp_settings, **self.server_kwargs, ) elif transport == "stdio": @@ -237,6 +248,7 @@ class MCPApp: asyncio.run( run_stdio_server( catalog=self._catalog, + settings=self._mcp_settings, **self.server_kwargs, ) ) diff --git a/libs/arcade-mcp-server/arcade_mcp_server/server.py b/libs/arcade-mcp-server/arcade_mcp_server/server.py index 94ec47ad..3df6389e 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/server.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/server.py @@ -39,7 +39,7 @@ from arcade_mcp_server.middleware import ( MiddlewareContext, ) from arcade_mcp_server.session import InitializationState, NotificationManager, ServerSession -from arcade_mcp_server.settings import MCPSettings +from arcade_mcp_server.settings import MCPSettings, ServerSettings from arcade_mcp_server.types import ( LATEST_PROTOCOL_VERSION, BlobResourceContents, @@ -109,8 +109,8 @@ class MCPServer: self, catalog: ToolCatalog, *, - name: str = "ArcadeMCP", - version: str = "0.1.0", + name: str | None = None, + version: str | None = None, title: str | None = None, instructions: str | None = None, settings: MCPSettings | None = None, @@ -136,17 +136,29 @@ class MCPServer: arcade_api_key: Arcade API key (overrides settings) arcade_api_url: Arcade API URL (overrides settings) """ - self.name = name or self.__class__.__name__ self._started = False self._lock = asyncio.Lock() - # Server identity - self.version = version - self.title = title or name - self.instructions = instructions or self._default_instructions() - - # Settings + # Settings (load first so we can use values from it) self.settings = settings or MCPSettings.from_env() + + # Server info + self.name = name if name else self.settings.server.name + self.version = version if version else self.settings.server.version + if title: + self.title = title + elif ( + self.settings.server.title + and self.settings.server.title != ServerSettings.model_fields["title"].default + ): + self.title = self.settings.server.title + else: + self.title = self.name + + self.instructions = ( + instructions or self.settings.server.instructions or self._default_instructions() + ) + self.auth_disabled = auth_disabled or self.settings.arcade.auth_disabled # Initialize Arcade client diff --git a/libs/arcade-mcp-server/arcade_mcp_server/settings.py b/libs/arcade-mcp-server/arcade_mcp_server/settings.py index 53fe543d..21dde7ed 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/settings.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/settings.py @@ -79,7 +79,7 @@ class ServerSettings(BaseSettings): description="Server version", ) title: str | None = Field( - default="Arcade MCP", + default="ArcadeMCP", description="Server title for display", ) instructions: str | None = Field( diff --git a/libs/arcade-mcp-server/arcade_mcp_server/worker.py b/libs/arcade-mcp-server/arcade_mcp_server/worker.py index 20abfe9f..6fc6a4f9 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/worker.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/worker.py @@ -115,6 +115,7 @@ def create_arcade_mcp( await logger.complete() logger.debug("Server lifespan shutdown complete.") + # Use settings for FastAPI app metadata app = FastAPI( title=(mcp_settings.server.title or mcp_settings.server.name), description=(mcp_settings.server.instructions or ""), @@ -122,7 +123,6 @@ def create_arcade_mcp( docs_url="/docs" if not mcp_settings.arcade.auth_disabled else None, redoc_url="/redoc" if not mcp_settings.arcade.auth_disabled else None, lifespan=lifespan, - **kwargs, ) otel_handler.instrument_app(app) app.add_middleware(AddTrailingSlashToPathMiddleware) @@ -221,6 +221,8 @@ def create_arcade_mcp_factory() -> FastAPI: show_packages = os.environ.get("ARCADE_MCP_SHOW_PACKAGES", "false").lower() == "true" server_name = os.environ.get("ARCADE_MCP_SERVER_NAME") server_version = os.environ.get("ARCADE_MCP_SERVER_VERSION") + server_title = os.environ.get("ARCADE_MCP_SERVER_TITLE") + server_instructions = os.environ.get("ARCADE_MCP_SERVER_INSTRUCTIONS") # Rediscover tools since there have been changes try: @@ -244,19 +246,24 @@ def create_arcade_mcp_factory() -> FastAPI: if otel_enable: logger.info("OpenTelemetry is enabled") - # Build kwargs for server creation - kwargs = {} - if server_name: - kwargs["name"] = server_name - if server_version: - kwargs["version"] = server_version + # Build settings with server metadata from env vars + from arcade_mcp_server.settings import ServerSettings + + mcp_settings = MCPSettings.from_env() + if server_name or server_version or server_title or server_instructions: + # Override server settings if any were provided via env vars + mcp_settings.server = ServerSettings( + name=server_name or mcp_settings.server.name, + version=server_version or mcp_settings.server.version, + title=server_title or mcp_settings.server.title, + instructions=server_instructions or mcp_settings.server.instructions, + ) return create_arcade_mcp( catalog=catalog, - mcp_settings=None, + mcp_settings=mcp_settings, debug=debug, otel_enable=otel_enable, - **kwargs, ) @@ -270,6 +277,7 @@ def run_arcade_mcp( tool_package: str | None = None, discover_installed: bool = False, show_packages: bool = False, + mcp_settings: MCPSettings | None = None, **kwargs: Any, ) -> None: """ @@ -287,10 +295,13 @@ def run_arcade_mcp( os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed) os.environ["ARCADE_MCP_SHOW_PACKAGES"] = str(show_packages) - if kwargs.get("name"): - os.environ["ARCADE_MCP_SERVER_NAME"] = kwargs["name"] - if kwargs.get("version"): - os.environ["ARCADE_MCP_SERVER_VERSION"] = kwargs["version"] + if mcp_settings: + os.environ["ARCADE_MCP_SERVER_NAME"] = mcp_settings.server.name + os.environ["ARCADE_MCP_SERVER_VERSION"] = mcp_settings.server.version + if mcp_settings.server.title: + os.environ["ARCADE_MCP_SERVER_TITLE"] = mcp_settings.server.title + if mcp_settings.server.instructions: + os.environ["ARCADE_MCP_SERVER_INSTRUCTIONS"] = mcp_settings.server.instructions # import string is required for reload mode app_import_string = "arcade_mcp_server.worker:create_arcade_mcp_factory" @@ -307,6 +318,7 @@ def run_arcade_mcp( else: app = create_arcade_mcp( catalog=catalog, + mcp_settings=mcp_settings, debug=debug, otel_enable=otel_enable, **kwargs, diff --git a/libs/tests/arcade_mcp_server/test_mcp_app.py b/libs/tests/arcade_mcp_server/test_mcp_app.py index dca812ce..def09c0b 100644 --- a/libs/tests/arcade_mcp_server/test_mcp_app.py +++ b/libs/tests/arcade_mcp_server/test_mcp_app.py @@ -17,6 +17,46 @@ class TestMCPApp: """Create an MCP app.""" return MCPApp(name="TestMCPApp", version="1.0.0") + def test_mcp_app_initialization(self): + """Test MCPApp initialization creates proper settings.""" + app = MCPApp( + name="TestApp", + version="1.5.0", + title="Test Title", + instructions="Test instructions", + ) + + assert app.name == "TestApp" + assert app.version == "1.5.0" + assert app.title == "Test Title" + assert app.instructions == "Test instructions" + + assert app._mcp_settings is not None + assert app._mcp_settings.server.name == "TestApp" + assert app._mcp_settings.server.version == "1.5.0" + assert app._mcp_settings.server.title == "Test Title" + assert app._mcp_settings.server.instructions == "Test instructions" + + def test_mcp_app_initialization_defaults(self): + """Test MCPApp initialization with default values.""" + app = MCPApp() + + assert app.name == "ArcadeMCP" + assert app.version == "0.1.0" + + assert app._mcp_settings.server.name == "ArcadeMCP" + assert app._mcp_settings.server.version == "0.1.0" + + def test_mcp_app_initialization_partial_values(self): + """Test MCPApp initialization with partial values.""" + app = MCPApp(name="PartialApp") + + assert app.name == "PartialApp" + assert app.version == "0.1.0" # Default value + + assert app._mcp_settings.server.name == "PartialApp" + assert app._mcp_settings.server.version == "0.1.0" + def test_add_tool(self, mcp_app: MCPApp): """Test adding a tool to the MCP app.""" diff --git a/libs/tests/arcade_mcp_server/test_server.py b/libs/tests/arcade_mcp_server/test_server.py index dc1ca3be..d46d7edf 100644 --- a/libs/tests/arcade_mcp_server/test_server.py +++ b/libs/tests/arcade_mcp_server/test_server.py @@ -37,12 +37,12 @@ class TestMCPServer: server = MCPServer( catalog=tool_catalog, name="Test Server", - version="1.0.0", + version="1.9.0", settings=mcp_settings, ) assert server.name == "Test Server" - assert server.version == "1.0.0" + assert server.version == "1.9.0" assert server.title == "Test Server" assert server.settings == mcp_settings @@ -58,6 +58,126 @@ class TestMCPServer: assert server2.title == "Custom Title" assert server2.instructions == "Custom instructions" + def test_server_initialization_with_settings_defaults(self, tool_catalog): + """Test server initialization uses settings when parameters not provided.""" + from arcade_mcp_server.settings import MCPSettings, ServerSettings + + settings = MCPSettings( + server=ServerSettings( + name="SettingsName", + version="2.0.0", + title="SettingsTitle", + instructions="Settings instructions", + ) + ) + + # Initialize without name/version - should use settings + server = MCPServer(catalog=tool_catalog, settings=settings) + + assert server.name == "SettingsName" + assert server.version == "2.0.0" + assert server.title == "SettingsTitle" + assert server.instructions == "Settings instructions" + + def test_server_initialization_parameters_override_settings(self, tool_catalog): + """Test server initialization parameters override settings.""" + from arcade_mcp_server.settings import MCPSettings, ServerSettings + + settings = MCPSettings( + server=ServerSettings( + name="SettingsName", + version="2.0.0", + title="SettingsTitle", + instructions="Settings instructions", + ) + ) + + # Initialize with explicit parameters (should override settings) + server = MCPServer( + catalog=tool_catalog, + name="ParamName", + version="3.0.0", + title="ParamTitle", + instructions="Param instructions", + settings=settings, + ) + + assert server.name == "ParamName" + assert server.version == "3.0.0" + assert server.title == "ParamTitle" + assert server.instructions == "Param instructions" + + def test_server_initialization_title_fallback_logic(self, tool_catalog): + """Test server initialization title fallback logic.""" + from arcade_mcp_server.settings import MCPSettings, ServerSettings + + # Test 1: Title parameter provided (should be used) + server1 = MCPServer( + catalog=tool_catalog, + name="TestServer", + title="ExplicitTitle", + ) + assert server1.title == "ExplicitTitle" + + # Test 2: No title parameter but settings has non-default title + settings2 = MCPSettings( + server=ServerSettings( + name="SettingsServer", + title="CustomSettingsTitle", + ) + ) + server2 = MCPServer(catalog=tool_catalog, settings=settings2) + assert server2.title == "CustomSettingsTitle" + + # Test 3: No title parameter, settings has default title (should use name) + settings3 = MCPSettings( + server=ServerSettings( + name="SettingsServer", + title="ArcadeMCP", # Default value + ) + ) + server3 = MCPServer(catalog=tool_catalog, settings=settings3) + assert server3.title == "SettingsServer" + + # Test 4: No title parameter, no settings title (should use name) + settings4 = MCPSettings( + server=ServerSettings( + name="SettingsServer", + title=None, + ) + ) + server4 = MCPServer(catalog=tool_catalog, settings=settings4) + assert server4.title == "SettingsServer" + + def test_server_initialization_instructions_fallback(self, tool_catalog): + """Test server initialization instructions fallback logic.""" + from arcade_mcp_server.settings import MCPSettings, ServerSettings + + # Test 1: Instructions parameter provided (should be used) + server1 = MCPServer( + catalog=tool_catalog, + instructions="Explicit instructions", + ) + assert server1.instructions == "Explicit instructions" + + # Test 2: No instructions parameter (should use settings) + settings2 = MCPSettings( + server=ServerSettings( + instructions="Settings instructions", + ) + ) + server2 = MCPServer(catalog=tool_catalog, settings=settings2) + assert server2.instructions == "Settings instructions" + + # Test 3: No instructions parameter, no settings (should use default) + settings3 = MCPSettings( + server=ServerSettings( + instructions=None, + ) + ) + server3 = MCPServer(catalog=tool_catalog, settings=settings3) + assert "available tools" in server3.instructions.lower() + def test_handler_registration(self, tool_catalog): """Test that all required handlers are registered.""" server = MCPServer(catalog=tool_catalog) diff --git a/libs/tests/arcade_mcp_server/test_settings.py b/libs/tests/arcade_mcp_server/test_settings.py new file mode 100644 index 00000000..d47dc491 --- /dev/null +++ b/libs/tests/arcade_mcp_server/test_settings.py @@ -0,0 +1,100 @@ +"""Tests for MCP Settings.""" + +import pytest +from arcade_mcp_server.settings import MCPSettings, ServerSettings + + +class TestServerSettings: + """Test ServerSettings class.""" + + def test_server_settings_defaults(self): + """Test ServerSettings default values.""" + settings = ServerSettings() + + assert settings.name == "ArcadeMCP" + assert settings.version == "0.1.0dev" + assert settings.title == "ArcadeMCP" + assert settings.instructions is not None + assert "available tools" in settings.instructions.lower() + + def test_server_settings_custom_values(self): + """Test ServerSettings with custom values.""" + settings = ServerSettings( + name="CustomServer", + version="2.0.0", + title="Custom Title", + instructions="Custom instructions", + ) + + assert settings.name == "CustomServer" + assert settings.version == "2.0.0" + assert settings.title == "Custom Title" + assert settings.instructions == "Custom instructions" + + def test_server_settings_partial_values(self): + """Test ServerSettings with partial custom values.""" + settings = ServerSettings( + name="PartialServer", + version="1.5.0", + ) + + assert settings.name == "PartialServer" + assert settings.version == "1.5.0" + assert settings.title == "ArcadeMCP" # Default value + assert settings.instructions is not None # Default value + + +class TestMCPSettings: + """Test MCPSettings class.""" + + def test_mcp_settings_defaults(self): + """Test MCPSettings default values.""" + settings = MCPSettings() + + assert settings.server.name == "ArcadeMCP" + assert settings.server.version == "0.1.0dev" + assert settings.server.title == "ArcadeMCP" + assert settings.server.instructions is not None + + def test_mcp_settings_with_custom_server(self): + """Test MCPSettings with custom ServerSettings.""" + server_settings = ServerSettings( + name="TestServer", + version="3.0.0", + title="Test Title", + instructions="Test instructions", + ) + settings = MCPSettings(server=server_settings) + + assert settings.server.name == "TestServer" + assert settings.server.version == "3.0.0" + assert settings.server.title == "Test Title" + assert settings.server.instructions == "Test instructions" + + def test_mcp_settings_from_env(self, monkeypatch): + """Test MCPSettings.from_env() uses environment variables.""" + monkeypatch.setenv("MCP_SERVER_NAME", "EnvServer") + monkeypatch.setenv("MCP_SERVER_VERSION", "4.0.0") + monkeypatch.setenv("MCP_SERVER_TITLE", "Env Title") + monkeypatch.setenv("MCP_SERVER_INSTRUCTIONS", "Env instructions") + + settings = MCPSettings.from_env() + + assert settings.server.name == "EnvServer" + assert settings.server.version == "4.0.0" + assert settings.server.title == "Env Title" + assert settings.server.instructions == "Env instructions" + + +class TestServerSettingsTitleDefault: + """Test that the default title value is 'ArcadeMCP'.""" + + def test_title_default_value(self): + """Test that the default title value is 'ArcadeMCP'.""" + settings = ServerSettings() + assert settings.title == "ArcadeMCP" + + def test_title_field_default(self): + """Test that the title field default is 'ArcadeMCP'.""" + field_info = ServerSettings.model_fields["title"] + assert field_info.default == "ArcadeMCP"