1616import anyio .abc
1717import mcp_types as types
1818import pytest
19+ from inline_snapshot import snapshot
1920from 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+
96105def _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
195216async 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
0 commit comments