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.exceptions import ServerError
from arcade_mcp_server.server import MCPServer 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.types import Prompt, PromptMessage, Resource
from arcade_mcp_server.worker import run_arcade_mcp from arcade_mcp_server.worker import run_arcade_mcp
@ -59,7 +60,7 @@ class MCPApp:
def __init__( def __init__(
self, self,
name: str = "ArcadeMCP", name: str = "ArcadeMCP",
version: str = "1.0.0dev", version: str = "0.1.0",
title: str | None = None, title: str | None = None,
instructions: str | None = None, instructions: str | None = None,
log_level: str = "INFO", log_level: str = "INFO",
@ -102,6 +103,15 @@ class MCPApp:
# Public handle to the MCPServer (set by caller for runtime ops) # Public handle to the MCPServer (set by caller for runtime ops)
self.server: MCPServer | None = None 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() self._load_env()
if not logger._core.handlers: # type: ignore[attr-defined] if not logger._core.handlers: # type: ignore[attr-defined]
self._setup_logging(transport == "stdio") self._setup_logging(transport == "stdio")
@ -227,6 +237,7 @@ class MCPApp:
host=host, host=host,
port=port, port=port,
reload=reload, reload=reload,
mcp_settings=self._mcp_settings,
**self.server_kwargs, **self.server_kwargs,
) )
elif transport == "stdio": elif transport == "stdio":
@ -237,6 +248,7 @@ class MCPApp:
asyncio.run( asyncio.run(
run_stdio_server( run_stdio_server(
catalog=self._catalog, catalog=self._catalog,
settings=self._mcp_settings,
**self.server_kwargs, **self.server_kwargs,
) )
) )

View file

@ -39,7 +39,7 @@ from arcade_mcp_server.middleware import (
MiddlewareContext, MiddlewareContext,
) )
from arcade_mcp_server.session import InitializationState, NotificationManager, ServerSession 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 ( from arcade_mcp_server.types import (
LATEST_PROTOCOL_VERSION, LATEST_PROTOCOL_VERSION,
BlobResourceContents, BlobResourceContents,
@ -109,8 +109,8 @@ class MCPServer:
self, self,
catalog: ToolCatalog, catalog: ToolCatalog,
*, *,
name: str = "ArcadeMCP", name: str | None = None,
version: str = "0.1.0", version: str | None = None,
title: str | None = None, title: str | None = None,
instructions: str | None = None, instructions: str | None = None,
settings: MCPSettings | None = None, settings: MCPSettings | None = None,
@ -136,17 +136,29 @@ class MCPServer:
arcade_api_key: Arcade API key (overrides settings) arcade_api_key: Arcade API key (overrides settings)
arcade_api_url: Arcade API URL (overrides settings) arcade_api_url: Arcade API URL (overrides settings)
""" """
self.name = name or self.__class__.__name__
self._started = False self._started = False
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
# Server identity # Settings (load first so we can use values from it)
self.version = version
self.title = title or name
self.instructions = instructions or self._default_instructions()
# Settings
self.settings = settings or MCPSettings.from_env() 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 self.auth_disabled = auth_disabled or self.settings.arcade.auth_disabled
# Initialize Arcade client # Initialize Arcade client

View file

@ -115,6 +115,7 @@ def create_arcade_mcp(
await logger.complete() await logger.complete()
logger.debug("Server lifespan shutdown complete.") logger.debug("Server lifespan shutdown complete.")
# Use settings for FastAPI app metadata
app = FastAPI( app = FastAPI(
title=(mcp_settings.server.title or mcp_settings.server.name), title=(mcp_settings.server.title or mcp_settings.server.name),
description=(mcp_settings.server.instructions or ""), 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, docs_url="/docs" if not mcp_settings.arcade.auth_disabled else None,
redoc_url="/redoc" if not mcp_settings.arcade.auth_disabled else None, redoc_url="/redoc" if not mcp_settings.arcade.auth_disabled else None,
lifespan=lifespan, lifespan=lifespan,
**kwargs,
) )
otel_handler.instrument_app(app) otel_handler.instrument_app(app)
app.add_middleware(AddTrailingSlashToPathMiddleware) 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" show_packages = os.environ.get("ARCADE_MCP_SHOW_PACKAGES", "false").lower() == "true"
server_name = os.environ.get("ARCADE_MCP_SERVER_NAME") server_name = os.environ.get("ARCADE_MCP_SERVER_NAME")
server_version = os.environ.get("ARCADE_MCP_SERVER_VERSION") 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 # Rediscover tools since there have been changes
try: try:
@ -244,19 +246,24 @@ def create_arcade_mcp_factory() -> FastAPI:
if otel_enable: if otel_enable:
logger.info("OpenTelemetry is enabled") logger.info("OpenTelemetry is enabled")
# Build kwargs for server creation # Build settings with server metadata from env vars
kwargs = {} from arcade_mcp_server.settings import ServerSettings
if server_name:
kwargs["name"] = server_name mcp_settings = MCPSettings.from_env()
if server_version: if server_name or server_version or server_title or server_instructions:
kwargs["version"] = server_version # 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( return create_arcade_mcp(
catalog=catalog, catalog=catalog,
mcp_settings=None, mcp_settings=mcp_settings,
debug=debug, debug=debug,
otel_enable=otel_enable, otel_enable=otel_enable,
**kwargs,
) )
@ -270,6 +277,7 @@ def run_arcade_mcp(
tool_package: str | None = None, tool_package: str | None = None,
discover_installed: bool = False, discover_installed: bool = False,
show_packages: bool = False, show_packages: bool = False,
mcp_settings: MCPSettings | None = None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
""" """
@ -287,10 +295,13 @@ def run_arcade_mcp(
os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package
os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed) os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed)
os.environ["ARCADE_MCP_SHOW_PACKAGES"] = str(show_packages) os.environ["ARCADE_MCP_SHOW_PACKAGES"] = str(show_packages)
if kwargs.get("name"): if mcp_settings:
os.environ["ARCADE_MCP_SERVER_NAME"] = kwargs["name"] os.environ["ARCADE_MCP_SERVER_NAME"] = mcp_settings.server.name
if kwargs.get("version"): os.environ["ARCADE_MCP_SERVER_VERSION"] = mcp_settings.server.version
os.environ["ARCADE_MCP_SERVER_VERSION"] = kwargs["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 # import string is required for reload mode
app_import_string = "arcade_mcp_server.worker:create_arcade_mcp_factory" app_import_string = "arcade_mcp_server.worker:create_arcade_mcp_factory"
@ -307,6 +318,7 @@ def run_arcade_mcp(
else: else:
app = create_arcade_mcp( app = create_arcade_mcp(
catalog=catalog, catalog=catalog,
mcp_settings=mcp_settings,
debug=debug, debug=debug,
otel_enable=otel_enable, otel_enable=otel_enable,
**kwargs, **kwargs,

View file

@ -17,6 +17,46 @@ class TestMCPApp:
"""Create an MCP app.""" """Create an MCP app."""
return MCPApp(name="TestMCPApp", version="1.0.0") 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): def test_add_tool(self, mcp_app: MCPApp):
"""Test adding a tool to the MCP app.""" """Test adding a tool to the MCP app."""

View file

@ -37,12 +37,12 @@ class TestMCPServer:
server = MCPServer( server = MCPServer(
catalog=tool_catalog, catalog=tool_catalog,
name="Test Server", name="Test Server",
version="1.0.0", version="1.9.0",
settings=mcp_settings, settings=mcp_settings,
) )
assert server.name == "Test Server" assert server.name == "Test Server"
assert server.version == "1.0.0" assert server.version == "1.9.0"
assert server.title == "Test Server" assert server.title == "Test Server"
assert server.settings == mcp_settings assert server.settings == mcp_settings
@ -58,6 +58,126 @@ class TestMCPServer:
assert server2.title == "Custom Title" assert server2.title == "Custom Title"
assert server2.instructions == "Custom instructions" 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): def test_handler_registration(self, tool_catalog):
"""Test that all required handlers are registered.""" """Test that all required handlers are registered."""
server = MCPServer(catalog=tool_catalog) 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"