|
8 | 8 |
|
9 | 9 | import logging |
10 | 10 | from collections.abc import Mapping |
11 | | -from typing import Any |
| 11 | +from typing import Any, Literal |
12 | 12 |
|
13 | 13 | import anyio |
14 | 14 | import pytest |
15 | | -from pydantic import ValidationError |
| 15 | +from pydantic import BaseModel, ValidationError |
16 | 16 |
|
17 | 17 | from mcp.server.connection import Connection |
18 | 18 | from mcp.shared.dispatcher import CallOptions |
|
29 | 29 | ListRootsRequest, |
30 | 30 | ListRootsResult, |
31 | 31 | PingRequest, |
| 32 | + Request, |
| 33 | + RequestParams, |
32 | 34 | RootsCapability, |
33 | 35 | SamplingCapability, |
34 | 36 | SamplingContextCapability, |
@@ -144,6 +146,54 @@ async def test_connection_send_request_nonconforming_result_raises_validation_er |
144 | 146 | await conn.send_request(ListRootsRequest()) |
145 | 147 |
|
146 | 148 |
|
| 149 | +@pytest.mark.anyio |
| 150 | +async def test_send_request_validates_the_client_result_against_the_surface_schema(): |
| 151 | + """A spec-method result that fails the per-version surface schema raises |
| 152 | + `ValidationError` even when the caller's `result_type` would accept it.""" |
| 153 | + conn = Connection(StubOutbound(result={"roots": "nope"}), has_standalone_channel=True) |
| 154 | + with pytest.raises(ValidationError): |
| 155 | + await conn.send_request(ListRootsRequest(), result_type=EmptyResult) |
| 156 | + |
| 157 | + |
| 158 | +@pytest.mark.anyio |
| 159 | +async def test_send_request_passes_a_spec_valid_client_result(): |
| 160 | + """A spec-valid client result passes the surface gate and parses to the typed model.""" |
| 161 | + conn = Connection(StubOutbound(result={"roots": [{"uri": "file:///ws"}]}), has_standalone_channel=True) |
| 162 | + conn.protocol_version = "2025-11-25" |
| 163 | + result = await conn.send_request(ListRootsRequest()) |
| 164 | + assert isinstance(result, ListRootsResult) |
| 165 | + assert str(result.roots[0].uri) == "file:///ws" |
| 166 | + |
| 167 | + |
| 168 | +class _CustomRequest(Request[RequestParams | None, Literal["custom/echo"]]): |
| 169 | + method: Literal["custom/echo"] = "custom/echo" |
| 170 | + params: RequestParams | None = None |
| 171 | + |
| 172 | + |
| 173 | +class _CustomResult(BaseModel): |
| 174 | + value: int |
| 175 | + |
| 176 | + |
| 177 | +@pytest.mark.anyio |
| 178 | +async def test_send_request_skips_the_surface_gate_when_method_absent_at_version(): |
| 179 | + """Surface row absent for the negotiated version: gate is bypassed and only |
| 180 | + the inferred result type validates.""" |
| 181 | + conn = Connection(StubOutbound(result={}), has_standalone_channel=True) |
| 182 | + conn.protocol_version = "2026-07-28" |
| 183 | + result = await conn.send_request(PingRequest()) |
| 184 | + assert isinstance(result, EmptyResult) |
| 185 | + |
| 186 | + |
| 187 | +@pytest.mark.anyio |
| 188 | +async def test_send_request_with_a_custom_method_skips_the_surface_gate(): |
| 189 | + """Non-spec methods are not blocked by the surface gate; `result_type` validates.""" |
| 190 | + conn = Connection(StubOutbound(result={"value": 7}), has_standalone_channel=True) |
| 191 | + conn.protocol_version = "2025-11-25" |
| 192 | + result = await conn.send_request(_CustomRequest(), result_type=_CustomResult) |
| 193 | + assert isinstance(result, _CustomResult) |
| 194 | + assert result.value == 7 |
| 195 | + |
| 196 | + |
147 | 197 | @pytest.mark.anyio |
148 | 198 | async def test_connection_ping_sends_ping_on_standalone(): |
149 | 199 | out = StubOutbound() |
|
0 commit comments