Skip to content

Commit 9de9e44

Browse files
committed
Pin preconnect and absent-params arms of the Mcp-Name delta
Emission must hold with no version adopted at all, and a dropped None params must still surface the documented ValueError. Also snapshot the SDK-authored failure messages instead of regex-matching them.
1 parent a9edb2d commit 9de9e44

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

‎tests/client/test_send_request_mcp_name.py‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import anyio.abc
1717
import mcp_types as types
1818
import pytest
19+
from inline_snapshot import snapshot
1920
from mcp_types import (
2021
CallToolResult,
2122
Implementation,
@@ -93,6 +94,14 @@ class _PlainVendorRequest(Request[dict[str, Any], Literal["vendor/widgets/list"]
9394
method: Literal["vendor/widgets/list"] = "vendor/widgets/list"
9495

9596

97+
class _OptionalParamsWidgetRequest(Request[dict[str, Any] | None, Literal["vendor/widgets/get"]]):
98+
"""Optional params, so a send can carry no params key at all."""
99+
100+
method: Literal["vendor/widgets/get"] = "vendor/widgets/get"
101+
params: dict[str, Any] | None = None
102+
name_param = "widgetId"
103+
104+
96105
def _adopt_modern(session: ClientSession) -> None:
97106
session.adopt(
98107
types.DiscoverResult(
@@ -191,15 +200,28 @@ async def test_stamp_table_rows_win_over_name_param_by_ordering() -> None:
191200
assert _headers(opts)[MCP_NAME_HEADER] == "real-tool"
192201

193202

203+
@pytest.mark.anyio
204+
async def test_vendor_name_param_emits_mcp_name_on_the_preconnect_path() -> None:
205+
"""Emission is era-unconditional: a lowlevel caller that never adopts any
206+
version (the preconnect stamp) still gets `Mcp-Name` from `name_param`."""
207+
dispatcher = _RecordingDispatcher()
208+
with anyio.fail_after(5):
209+
async with ClientSession(dispatcher=dispatcher) as session:
210+
await session.send_request(_GetWidgetRequest(params=_GetWidgetParams(widget_id="w-1")), types.EmptyResult)
211+
[(_, opts)] = dispatcher.calls
212+
assert _headers(opts) == {MCP_NAME_HEADER: "w-1"} # and no era headers: nothing adopted
213+
214+
194215
@pytest.mark.anyio
195216
async def test_missing_name_value_fails_loud_naming_method_and_key() -> None:
196217
dispatcher = _RecordingDispatcher()
197218
with anyio.fail_after(5):
198219
async with ClientSession(dispatcher=dispatcher) as session:
199220
_adopt_handshake(session)
200-
with pytest.raises(ValueError, match=r"vendor/widgets/get requires params\['widgetId'\] for Mcp-Name"):
221+
with pytest.raises(ValueError) as exc_info:
201222
await session.send_request(_RawWidgetRequest(params={}), types.EmptyResult)
202223
assert dispatcher.calls == [] # raised before reaching the wire
224+
assert str(exc_info.value) == snapshot("vendor/widgets/get requires params['widgetId'] for Mcp-Name")
203225

204226

205227
@pytest.mark.anyio
@@ -208,9 +230,24 @@ async def test_non_string_name_value_fails_loud() -> None:
208230
with anyio.fail_after(5):
209231
async with ClientSession(dispatcher=dispatcher) as session:
210232
_adopt_handshake(session)
211-
with pytest.raises(ValueError, match=r"vendor/widgets/get requires params\['widgetId'\] for Mcp-Name"):
233+
with pytest.raises(ValueError) as exc_info:
212234
await session.send_request(_RawWidgetRequest(params={"widgetId": 7}), types.EmptyResult)
213235
assert dispatcher.calls == []
236+
assert str(exc_info.value) == snapshot("vendor/widgets/get requires params['widgetId'] for Mcp-Name")
237+
238+
239+
@pytest.mark.anyio
240+
async def test_absent_params_fails_loud_not_attribute_error() -> None:
241+
"""`exclude_none` drops a None params entirely; the delta still answers with
242+
the documented ValueError, not an AttributeError on the missing key."""
243+
dispatcher = _RecordingDispatcher()
244+
with anyio.fail_after(5):
245+
async with ClientSession(dispatcher=dispatcher) as session:
246+
_adopt_handshake(session)
247+
with pytest.raises(ValueError) as exc_info:
248+
await session.send_request(_OptionalParamsWidgetRequest(), types.EmptyResult)
249+
assert dispatcher.calls == []
250+
assert str(exc_info.value) == snapshot("vendor/widgets/get requires params['widgetId'] for Mcp-Name")
214251

215252

216253
@pytest.mark.anyio

‎tests/client/test_session_promotions.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ async def test_validate_tool_result_passes_a_conforming_result() -> None:
6565
async def test_validate_tool_result_raises_on_schema_mismatch() -> None:
6666
server = _make_server({"type": "object", "properties": {"x": {"type": "integer"}}, "required": ["x"]})
6767
async with Client(server) as client:
68+
# Stable SDK prefix only: the message tail is jsonschema text that shifts with the dependency.
6869
with pytest.raises(RuntimeError, match="Invalid structured content returned by tool t"):
6970
await client.session.validate_tool_result("t", CallToolResult(content=[], structured_content={"x": "no"}))
7071

0 commit comments

Comments
 (0)