chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""Tests for the SEP-2133 extensions capability negotiation plumbing.
|
||||
|
||||
The extension-map negotiation is independent of any concrete extension (Apps,
|
||||
Tasks): the lowlevel `Server` advertises `self.extensions` under
|
||||
`ServerCapabilities.extensions`, a client mirrors its own support under
|
||||
`ClientCapabilities.extensions`, and `Connection.check_capability` resolves the
|
||||
server-side query. These tests pin that plumbing end-to-end and at the unit
|
||||
level. Per-extension contribution wiring lives in `test_extension.py`; this file
|
||||
covers only the capability advertisement and negotiation.
|
||||
"""
|
||||
|
||||
import mcp_types as types
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
from mcp.client import advertise
|
||||
from mcp.client.client import Client
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from mcp.server.extension import Extension
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
_EXTENSION_ID = "com.example/x"
|
||||
_OTHER_EXTENSION_ID = "com.example/other"
|
||||
|
||||
|
||||
class _Extension(Extension):
|
||||
identifier = _EXTENSION_ID
|
||||
|
||||
def settings(self) -> dict[str, object]:
|
||||
return {"k": 1}
|
||||
|
||||
|
||||
def test_get_capabilities_omits_extensions_when_none_registered() -> None:
|
||||
"""SDK-defined: a lowlevel `Server` with an empty `extensions` map advertises
|
||||
`ServerCapabilities.extensions` as `None`, not an empty map."""
|
||||
server = Server("bare")
|
||||
assert server.get_capabilities().extensions is None
|
||||
|
||||
|
||||
def test_get_capabilities_advertises_populated_self_extensions() -> None:
|
||||
"""SDK-defined: `get_capabilities` reads `self.extensions` (the map higher
|
||||
layers populate) and advertises it under `ServerCapabilities.extensions`."""
|
||||
server = Server("with-ext")
|
||||
settings = {"k": 1}
|
||||
server.extensions = {_EXTENSION_ID: settings}
|
||||
assert server.get_capabilities().extensions == {_EXTENSION_ID: settings}
|
||||
|
||||
|
||||
async def test_modern_connection_carries_the_advertised_extensions_map() -> None:
|
||||
"""SDK-defined: over a modern (`server/discover`) connection the client reads
|
||||
the server's advertised extension map from `server_capabilities`."""
|
||||
server = MCPServer("host", extensions=[_Extension()])
|
||||
async with Client(server, mode="auto") as client:
|
||||
assert client.server_capabilities.extensions == snapshot({"com.example/x": {"k": 1}})
|
||||
|
||||
|
||||
async def test_legacy_handshake_drops_the_extensions_map() -> None:
|
||||
"""Pinned gap: the handshake-era `initialize` result is serialized against the
|
||||
2025 wire schema, which has no `extensions` field, so a legacy handshake cannot
|
||||
carry it; the client sees `None` even though the server advertised one."""
|
||||
server = MCPServer("host", extensions=[_Extension()])
|
||||
async with Client(server, mode="legacy") as client:
|
||||
assert client.server_capabilities.extensions is None
|
||||
|
||||
|
||||
async def test_server_accepts_capability_for_client_advertised_extension() -> None:
|
||||
"""SDK-defined: a client advertising `extensions={id: ...}` makes the
|
||||
server-side `check_client_capability` return True when queried for that id.
|
||||
Observed inside a tool handler."""
|
||||
queried = types.ClientCapabilities(extensions={_EXTENSION_ID: {}})
|
||||
supported: list[bool] = []
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
|
||||
assert params.name == "probe"
|
||||
supported.append(ctx.session.check_client_capability(queried))
|
||||
return types.CallToolResult(content=[])
|
||||
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(tools=[types.Tool(name="probe", input_schema={"type": "object"})])
|
||||
|
||||
server = Server("checker", on_call_tool=call_tool, on_list_tools=list_tools)
|
||||
async with Client(server, extensions=[advertise(_EXTENSION_ID, {"mimeTypes": ["text/html"]})]) as client:
|
||||
await client.call_tool("probe", {})
|
||||
|
||||
assert supported == [True]
|
||||
|
||||
|
||||
async def test_server_rejects_capability_for_undeclared_extension() -> None:
|
||||
"""SDK-defined: when the client advertises one extension, a server query for a
|
||||
*different* identifier returns False - presence, not value, is the check."""
|
||||
queried = types.ClientCapabilities(extensions={_OTHER_EXTENSION_ID: {}})
|
||||
supported: list[bool] = []
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
|
||||
assert params.name == "probe"
|
||||
supported.append(ctx.session.check_client_capability(queried))
|
||||
return types.CallToolResult(content=[])
|
||||
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(tools=[types.Tool(name="probe", input_schema={"type": "object"})])
|
||||
|
||||
server = Server("checker", on_call_tool=call_tool, on_list_tools=list_tools)
|
||||
async with Client(server, extensions=[advertise(_EXTENSION_ID, {"mimeTypes": ["text/html"]})]) as client:
|
||||
await client.call_tool("probe", {})
|
||||
|
||||
assert supported == [False]
|
||||
|
||||
|
||||
async def test_server_rejects_capability_when_client_advertises_no_extensions() -> None:
|
||||
"""SDK-defined: a client that declares no extensions makes any server
|
||||
`check_client_capability` query for an extension return False."""
|
||||
queried = types.ClientCapabilities(extensions={_EXTENSION_ID: {}})
|
||||
supported: list[bool] = []
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
|
||||
assert params.name == "probe"
|
||||
supported.append(ctx.session.check_client_capability(queried))
|
||||
return types.CallToolResult(content=[])
|
||||
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(tools=[types.Tool(name="probe", input_schema={"type": "object"})])
|
||||
|
||||
server = Server("checker", on_call_tool=call_tool, on_list_tools=list_tools)
|
||||
async with Client(server) as client:
|
||||
await client.call_tool("probe", {})
|
||||
|
||||
assert supported == [False]
|
||||
Reference in New Issue
Block a user