Server-side 2026-07-28 stateless support: classifier, driver split, server/discover (#2928)
This commit is contained in:
parent
44724284b3
commit
2397319a68
1 changed files with 2256 additions and 1088 deletions
151
tests/server/lowlevel/test_server_discover.py
Normal file
151
tests/server/lowlevel/test_server_discover.py
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
"""Direct-handler tests for the auto-derived `server/discover` handler.
|
||||
|
||||
These call the registered handler via the public `Server.get_request_handler`
|
||||
accessor without spinning up a `ServerRunner` or any transport, so they verify
|
||||
the handler's contract in isolation from the dispatch pipeline.
|
||||
"""
|
||||
|
||||
import importlib.metadata
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp import types
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from mcp.shared.version import MODERN_PROTOCOL_VERSIONS
|
||||
|
||||
# `Server._handle_discover` ignores its `ctx` argument entirely (it derives the
|
||||
# result from server state), so a sentinel keeps the call site type-correct
|
||||
# without dragging session machinery into a unit test.
|
||||
_UNUSED_CTX = cast("ServerRequestContext[Any]", None)
|
||||
|
||||
|
||||
async def _discover(server: Server[Any]) -> types.DiscoverResult:
|
||||
entry = server.get_request_handler("server/discover")
|
||||
assert entry is not None
|
||||
result = await entry.handler(_UNUSED_CTX, types.RequestParams())
|
||||
assert isinstance(result, types.DiscoverResult)
|
||||
return result
|
||||
|
||||
|
||||
def test_registered_by_default() -> None:
|
||||
"""SDK-defined: a bare `Server` registers a `server/discover` handler out of
|
||||
the box, typed for the base `RequestParams`."""
|
||||
server = Server("test-server")
|
||||
entry = server.get_request_handler("server/discover")
|
||||
assert entry is not None
|
||||
assert entry.params_type is types.RequestParams
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_supported_versions_is_modern_set() -> None:
|
||||
"""`supportedVersions` is exactly the modern envelope set, not the full
|
||||
legacy-compat list (D-008)."""
|
||||
result = await _discover(Server("test-server"))
|
||||
assert result.supported_versions == list(MODERN_PROTOCOL_VERSIONS)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_server_info_reflects_constructor_fields() -> None:
|
||||
"""SDK-defined: `serverInfo` is built field-for-field from the `Server`
|
||||
constructor arguments."""
|
||||
icons = [types.Icon(src="https://example.test/icon.png")]
|
||||
server = Server(
|
||||
"info-server",
|
||||
version="9.9.9",
|
||||
title="Info Server",
|
||||
description="A server for testing discover.",
|
||||
website_url="https://example.test",
|
||||
icons=icons,
|
||||
)
|
||||
result = await _discover(server)
|
||||
assert result.server_info == types.Implementation(
|
||||
name="info-server",
|
||||
version="9.9.9",
|
||||
title="Info Server",
|
||||
description="A server for testing discover.",
|
||||
website_url="https://example.test",
|
||||
icons=icons,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_server_info_version_falls_back_to_package() -> None:
|
||||
"""SDK-defined: when no explicit version is supplied, `serverInfo.version`
|
||||
falls back to the installed `mcp` package version."""
|
||||
result = await _discover(Server("unversioned"))
|
||||
assert result.server_info.version == importlib.metadata.version("mcp")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_instructions_threaded_through() -> None:
|
||||
"""SDK-defined: the `instructions` constructor argument is passed through
|
||||
verbatim, defaulting to `None` when omitted."""
|
||||
server = Server("inst-server", instructions="Read the docs first.")
|
||||
result = await _discover(server)
|
||||
assert result.instructions == "Read the docs first."
|
||||
|
||||
bare = await _discover(Server("bare"))
|
||||
assert bare.instructions is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_capabilities_derived_from_registered_handlers() -> None:
|
||||
"""SDK-defined: capabilities are computed at handler call time from the
|
||||
live registry, so post-construction `add_request_handler` calls are
|
||||
reflected."""
|
||||
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext[Any], params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
raise NotImplementedError
|
||||
|
||||
async def list_prompts(
|
||||
ctx: ServerRequestContext[Any], params: types.PaginatedRequestParams | None
|
||||
) -> types.ListPromptsResult:
|
||||
raise NotImplementedError
|
||||
|
||||
server = Server("cap-server", on_list_tools=list_tools)
|
||||
|
||||
before = await _discover(server)
|
||||
assert before.capabilities.tools is not None
|
||||
assert before.capabilities.prompts is None
|
||||
|
||||
server.add_request_handler("prompts/list", types.PaginatedRequestParams, list_prompts)
|
||||
|
||||
after = await _discover(server)
|
||||
assert after.capabilities.tools is not None
|
||||
assert after.capabilities.prompts is not None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_discover_result_defaults_to_immediately_stale_private_cache() -> None:
|
||||
"""SDK-defined: `DiscoverResult` is cacheable; the auto-derived handler
|
||||
relies on the model defaults (immediately-stale, private)."""
|
||||
result = await _discover(Server("cache-server"))
|
||||
assert result.ttl_ms == 0
|
||||
assert result.cache_scope == "private"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_overridable_via_add_request_handler() -> None:
|
||||
"""SDK-defined: a custom `server/discover` handler registered via
|
||||
`add_request_handler` replaces the auto-derived default wholesale."""
|
||||
server = Server("custom-server", version="1.0.0")
|
||||
custom = types.DiscoverResult(
|
||||
supported_versions=list(MODERN_PROTOCOL_VERSIONS),
|
||||
capabilities=types.ServerCapabilities(),
|
||||
server_info=types.Implementation(name="custom-server", version="1.0.0"),
|
||||
instructions="overridden",
|
||||
ttl_ms=60_000,
|
||||
cache_scope="public",
|
||||
)
|
||||
|
||||
async def custom_discover(
|
||||
ctx: ServerRequestContext[Any], params: types.RequestParams | None
|
||||
) -> types.DiscoverResult:
|
||||
return custom
|
||||
|
||||
server.add_request_handler("server/discover", types.RequestParams, custom_discover)
|
||||
result = await _discover(server)
|
||||
assert result is custom
|
||||
Loading…
Reference in a new issue