forked from modelcontextprotocol/python-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_extensions_capability.py
More file actions
135 lines (101 loc) · 5.98 KB
/
Copy pathtest_extensions_capability.py
File metadata and controls
135 lines (101 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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]