Spaces:
Running
Running
| """Protocol-level discovery, schema, dispatch, and transport tests.""" | |
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Any, cast | |
| import pytest | |
| from mcp import Client | |
| import spotify_mcp_server.server as server_module | |
| from spotify_mcp_server.server import create_server, main, mcp | |
| from spotify_mcp_server.tools.common import ToolResponse | |
| from spotify_mcp_server.tools.service import SpotifyService | |
| pytestmark = pytest.mark.anyio | |
| TOOL_NAMES = [ | |
| "search_catalog", | |
| "get_item", | |
| "player_status", | |
| "player_control", | |
| "playlist_read", | |
| "playlist_modify", | |
| "library_read", | |
| "library_modify", | |
| "listening_activity", | |
| ] | |
| def load_snapshot() -> object: | |
| path = Path(__file__).resolve().parents[1] / "schemas" / "tools.json" | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| async def test_server_discovers_current_protocol_and_exact_tool_catalog() -> None: | |
| async with Client(mcp) as client: | |
| assert client.protocol_version == "2026-07-28" | |
| assert client.server_info is not None | |
| assert client.server_info.name == "Spotify MCP Server" | |
| tools = await client.list_tools() | |
| assert [tool.name for tool in tools.tools] == TOOL_NAMES | |
| async def test_discovery_exposes_strict_structured_contracts_and_annotations() -> None: | |
| tools = await mcp.list_tools() | |
| for tool in tools: | |
| assert tool.description | |
| request_name = tool.input_schema["properties"]["request"]["$ref"].rsplit("/", 1)[-1] | |
| request_schema = tool.input_schema["$defs"][request_name] | |
| assert request_schema["additionalProperties"] is False | |
| assert tool.output_schema is not None | |
| assert tool.output_schema["additionalProperties"] is False | |
| assert tool.annotations and tool.annotations.open_world_hint is True | |
| async def test_committed_schema_snapshot_matches_discovery() -> None: | |
| actual = [ | |
| tool.model_dump(by_alias=True, exclude_none=True, mode="json") | |
| for tool in await mcp.list_tools() | |
| ] | |
| assert actual == load_snapshot() | |
| class RecordingService: | |
| def __init__(self) -> None: | |
| self.called: list[str] = [] | |
| def __getattr__(self, name: str) -> Any: | |
| async def call(_: object) -> ToolResponse: | |
| self.called.append(name) | |
| return ToolResponse(status="ok", data={"tool": name}) | |
| return call | |
| async def test_every_registered_handler_validates_and_dispatches() -> None: | |
| service = RecordingService() | |
| server = create_server(cast(SpotifyService, service)) | |
| payloads = { | |
| "search_catalog": {"query": "focus", "types": ["track"]}, | |
| "get_item": {"items": [{"value": "spotify:track:t"}]}, | |
| "player_status": {}, | |
| "player_control": {"actions": [{"action": "pause_playback"}]}, | |
| "playlist_read": {"requests": [{"operation": "list_current_playlists"}]}, | |
| "playlist_modify": {"actions": [{"action": "create_playlist", "name": "x"}]}, | |
| "library_read": {"requests": [{"operation": "list_saved", "type": "track"}]}, | |
| "library_modify": { | |
| "actions": [ | |
| { | |
| "action": "save", | |
| "items": [{"value": "spotify:track:t"}], | |
| } | |
| ] | |
| }, | |
| "listening_activity": {}, | |
| } | |
| async with Client(server) as client: | |
| for name, request in payloads.items(): | |
| response = await client.call_tool(name, {"request": request}) | |
| assert response.is_error is False | |
| assert response.structured_content is not None | |
| assert response.structured_content["status"] == "ok" | |
| assert service.called == TOOL_NAMES | |
| def test_main_rejects_non_loopback_host(monkeypatch: pytest.MonkeyPatch) -> None: | |
| monkeypatch.setenv("MCP_HOST", "0.0.0.0") | |
| with pytest.raises(ValueError, match="loopback"): | |
| main() | |
| def test_main_runs_stateless_streamable_http(monkeypatch: pytest.MonkeyPatch) -> None: | |
| captured: dict[str, object] = {} | |
| runtime_service = object() | |
| class RuntimeServer: | |
| def run(self, *, transport: str, **kwargs: object) -> None: | |
| captured.update(transport=transport, **kwargs) | |
| def build_service(settings: object) -> object: | |
| captured["settings"] = settings | |
| return runtime_service | |
| def create_server(service: object) -> RuntimeServer: | |
| assert service is runtime_service | |
| return RuntimeServer() | |
| monkeypatch.setenv("MCP_HOST", "127.0.0.1") | |
| monkeypatch.setenv("MCP_PORT", "8123") | |
| monkeypatch.setattr(server_module, "build_service", build_service) | |
| monkeypatch.setattr(server_module, "create_server", create_server) | |
| main() | |
| settings = captured.pop("settings") | |
| assert isinstance(settings, server_module.Settings) | |
| assert settings.host == "127.0.0.1" | |
| assert settings.port == 8123 | |
| assert captured == { | |
| "transport": "streamable-http", | |
| "host": "127.0.0.1", | |
| "port": 8123, | |
| "stateless_http": True, | |
| "json_response": True, | |
| } | |
| def test_main_runs_authenticated_hosted_transport(monkeypatch: pytest.MonkeyPatch) -> None: | |
| captured: dict[str, object] = {} | |
| transport_security = object() | |
| class HostedRuntimeSettings: | |
| host = "0.0.0.0" | |
| port = 7860 | |
| def from_env(cls) -> HostedRuntimeSettings: | |
| return cls() | |
| def transport_security(self) -> object: | |
| return transport_security | |
| class RuntimeServer: | |
| def run(self, *, transport: str, **kwargs: object) -> None: | |
| captured.update(transport=transport, **kwargs) | |
| def create_hosted_server(settings: object) -> RuntimeServer: | |
| assert isinstance(settings, HostedRuntimeSettings) | |
| return RuntimeServer() | |
| monkeypatch.setenv("MCP_DEPLOYMENT_MODE", "hosted") | |
| monkeypatch.setattr(server_module, "HostedSettings", HostedRuntimeSettings) | |
| monkeypatch.setattr(server_module, "create_hosted_server", create_hosted_server) | |
| main() | |
| assert captured == { | |
| "transport": "streamable-http", | |
| "host": "0.0.0.0", | |
| "port": 7860, | |
| "stateless_http": True, | |
| "json_response": True, | |
| "transport_security": transport_security, | |
| } | |