|
7 | 7 | from inline_snapshot import snapshot |
8 | 8 | from mcp_types import CallToolResult, InputRequiredResult, Result |
9 | 9 | from mcp_types.version import MODERN_PROTOCOL_VERSIONS |
10 | | -from pydantic import BaseModel |
| 10 | +from pydantic import AliasChoices, AliasPath, BaseModel, Field |
| 11 | +from pydantic.fields import FieldInfo |
11 | 12 |
|
12 | 13 | from mcp.client.extension import ( |
13 | 14 | ClaimContext, |
14 | 15 | ClientExtension, |
15 | 16 | NotificationBinding, |
16 | 17 | ResultClaim, |
| 18 | + _wire_keys, |
17 | 19 | advertise, |
18 | 20 | ) |
19 | 21 |
|
@@ -148,6 +150,60 @@ def test_claim_rejects_model_aliasing_core_surface_fields() -> None: |
148 | 150 | ) |
149 | 151 |
|
150 | 152 |
|
| 153 | +class _ValidationAliasResult(Result): |
| 154 | + result_type: Literal["va"] = "va" |
| 155 | + vendor_state: dict[str, Any] | None = Field(default=None, validation_alias="requestState") |
| 156 | + |
| 157 | + |
| 158 | +class _SerializationAliasResult(Result): |
| 159 | + result_type: Literal["sa"] = "sa" |
| 160 | + vendor_state: dict[str, Any] | None = Field(default=None, serialization_alias="inputRequests") |
| 161 | + |
| 162 | + |
| 163 | +class _AliasChoicesResult(Result): |
| 164 | + result_type: Literal["ac"] = "ac" |
| 165 | + vendor_state: dict[str, Any] | None = Field( |
| 166 | + default=None, validation_alias=AliasChoices("vendorKey", "requestState") |
| 167 | + ) |
| 168 | + |
| 169 | + |
| 170 | +class _AliasPathResult(Result): |
| 171 | + result_type: Literal["ap"] = "ap" |
| 172 | + vendor_state: dict[str, Any] | None = Field( |
| 173 | + default=None, validation_alias=AliasChoices(AliasPath("requestState", "nested")) |
| 174 | + ) |
| 175 | + |
| 176 | + |
| 177 | +def test_wire_keys_for_a_bare_field_is_just_its_name() -> None: |
| 178 | + """SDK-defined: a field with no aliases reads and writes only its own name.""" |
| 179 | + assert _wire_keys("plain", FieldInfo(annotation=str)) == frozenset({"plain"}) |
| 180 | + |
| 181 | + |
| 182 | +def test_claim_rejects_reserved_aliases_in_every_alias_form() -> None: |
| 183 | + """SDK-defined: validation_alias, serialization_alias, and AliasChoices routes to a reserved key are all caught.""" |
| 184 | + messages: dict[str, str] = {} |
| 185 | + for model in (_ValidationAliasResult, _SerializationAliasResult, _AliasChoicesResult, _AliasPathResult): |
| 186 | + with pytest.raises(ValueError) as exc_info: |
| 187 | + ResultClaim(result_type=model.model_fields["result_type"].default, model=model, resolve=_resolve) |
| 188 | + messages[model.__name__] = str(exc_info.value) |
| 189 | + |
| 190 | + assert messages == snapshot( |
| 191 | + { |
| 192 | + "_ValidationAliasResult": "_ValidationAliasResult.vendor_state aliases " |
| 193 | + "'requestState', a typed field of the core result surface; a colliding value would fail " |
| 194 | + "core validation before the claim adapter runs", |
| 195 | + "_SerializationAliasResult": "_SerializationAliasResult.vendor_state aliases " |
| 196 | + "'inputRequests', a typed field of the core result surface; a colliding value would fail " |
| 197 | + "core validation before the claim adapter runs", |
| 198 | + "_AliasChoicesResult": "_AliasChoicesResult.vendor_state aliases 'requestState', a typed field of the core " |
| 199 | + "result surface; a colliding value would fail core validation before the claim adapter runs", |
| 200 | + "_AliasPathResult": "_AliasPathResult.vendor_state aliases " |
| 201 | + "'requestState', a typed field of the core result surface; a colliding value would fail " |
| 202 | + "core validation before the claim adapter runs", |
| 203 | + } |
| 204 | + ) |
| 205 | + |
| 206 | + |
151 | 207 | def test_claim_rejects_method_outside_the_closed_verb_set() -> None: |
152 | 208 | """SDK-defined: claims attach to `tools/call` only, even for values that dodge the static Literal gate.""" |
153 | 209 | with pytest.raises(ValueError) as exc_info: |
|
0 commit comments