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:
parent
4d0ae0cdce
commit
83c0eeab2b
7 changed files with 323 additions and 27 deletions
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
100
libs/tests/arcade_mcp_server/test_settings.py
Normal file
100
libs/tests/arcade_mcp_server/test_settings.py
Normal 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"
|
||||
Loading…
Reference in a new issue