Fix server info bug (#614)

Name, title, version, etc. for an `MCPApp` were being overwritten by its
internal `MCPServer`.

⚠️ this is blocking `arcade deploy` from working
This commit is contained in:
Eric Gustin 2025-10-13 13:04:18 -07:00 committed by GitHub
parent 4d0ae0cdce
commit 83c0eeab2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 323 additions and 27 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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