"""`ClientSession.send_request` mirrors `Request.name_param` into the `Mcp-Name` header on send paths the core `NAME_BEARING_METHODS` table does not cover. The vendor sends also pin the widened `send_request` typing (no cast needed).""" from collections.abc import Mapping from typing import Any, Literal import anyio import anyio.abc import mcp_types as types import pytest from inline_snapshot import snapshot from mcp_types import ( CallToolResult, Implementation, ListToolsResult, Request, ServerCapabilities, TextContent, Tool, ) from mcp_types.version import LATEST_HANDSHAKE_VERSION, LATEST_MODERN_VERSION from mcp.client.session import ClientSession from mcp.shared.dispatcher import CallOptions, OnNotify, OnNotifyIntercept, OnRequest from mcp.shared.inbound import MCP_NAME_HEADER, MCP_PROTOCOL_VERSION_HEADER, encode_header_value class _RecordingDispatcher: """Records `send_raw_request` opts and answers with canned per-method results.""" def __init__(self) -> None: self.calls: list[tuple[str, CallOptions]] = [] async def run( self, on_request: OnRequest, on_notify: OnNotify, on_notify_intercept: OnNotifyIntercept | None = None, *, task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, ) -> None: task_status.started() await anyio.sleep_forever() async def send_raw_request( self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None ) -> dict[str, Any]: self.calls.append((method, opts or {})) if method == "tools/call": return CallToolResult(content=[TextContent(type="text", text="ok")]).model_dump( by_alias=True, mode="json", exclude_none=True ) if method == "tools/list": return ListToolsResult(tools=[Tool(name="my-tool", input_schema={"type": "object"})]).model_dump( by_alias=True, mode="json", exclude_none=True ) return {} async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: raise NotImplementedError class _GetWidgetParams(types.RequestParams): widget_id: str class _GetWidgetRequest(Request[_GetWidgetParams, Literal["vendor/widgets/get"]]): method: Literal["vendor/widgets/get"] = "vendor/widgets/get" name_param = "widgetId" class _RawWidgetRequest(Request[dict[str, Any], Literal["vendor/widgets/get"]]): """Same wire shape with untyped params, so tests can omit or mistype the name value.""" method: Literal["vendor/widgets/get"] = "vendor/widgets/get" name_param = "widgetId" class _ShadowCallToolRequest(Request[dict[str, Any], Literal["tools/call"]]): """A vendor type declaring `name_param` for a method the core table already covers.""" method: Literal["tools/call"] = "tools/call" name_param = "customKey" class _PlainVendorRequest(Request[dict[str, Any], Literal["vendor/widgets/list"]]): method: Literal["vendor/widgets/list"] = "vendor/widgets/list" class _OptionalParamsWidgetRequest(Request[dict[str, Any] | None, Literal["vendor/widgets/get"]]): """Optional params, so a send can carry no params key at all.""" method: Literal["vendor/widgets/get"] = "vendor/widgets/get" params: dict[str, Any] | None = None name_param = "widgetId" def _adopt_modern(session: ClientSession) -> None: session.adopt( types.DiscoverResult( supported_versions=[LATEST_MODERN_VERSION], capabilities=ServerCapabilities(), server_info=Implementation(name="stub", version="0"), ) ) def _adopt_handshake(session: ClientSession) -> None: session.adopt( types.InitializeResult( protocol_version=LATEST_HANDSHAKE_VERSION, capabilities=ServerCapabilities(), server_info=Implementation(name="stub", version="0"), ) ) def _headers(opts: CallOptions) -> dict[str, str]: return opts.get("headers") or {} @pytest.mark.anyio async def test_vendor_name_param_emits_mcp_name_on_the_modern_path() -> None: """A vendor `name_param` emits `Mcp-Name` on a modern wire even outside `NAME_BEARING_METHODS`.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_modern(session) await session.send_request(_GetWidgetRequest(params=_GetWidgetParams(widget_id="w-1")), types.EmptyResult) [(_, opts)] = dispatcher.calls assert _headers(opts)[MCP_NAME_HEADER] == "w-1" @pytest.mark.anyio async def test_vendor_name_param_emits_mcp_name_on_the_handshake_path() -> None: """The handshake stamp sets no `Mcp-Name`, so on a legacy wire the delta is the emitter.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_handshake(session) await session.send_request(_GetWidgetRequest(params=_GetWidgetParams(widget_id="w-1")), types.EmptyResult) [(_, opts)] = dispatcher.calls assert _headers(opts)[MCP_NAME_HEADER] == "w-1" # The stamp's own headers survive the delta. assert _headers(opts)[MCP_PROTOCOL_VERSION_HEADER] == LATEST_HANDSHAKE_VERSION @pytest.mark.anyio async def test_name_value_passes_through_encode_header_value() -> None: """A non-ASCII name is base64-sentinel encoded, a spec MUST for `Mcp-Name`.""" name = "wídget ✨" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_handshake(session) await session.send_request(_GetWidgetRequest(params=_GetWidgetParams(widget_id=name)), types.EmptyResult) [(_, opts)] = dispatcher.calls assert _headers(opts)[MCP_NAME_HEADER] == encode_header_value(name) assert _headers(opts)[MCP_NAME_HEADER].startswith("=?base64?") @pytest.mark.anyio async def test_core_tools_call_header_comes_from_the_stamp_alone() -> None: """Core `tools/call` is unchanged: the modern stamp emits the header; legacy stays headerless.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_modern(session) await session.call_tool("my-tool", {}) _adopt_handshake(session) await session.call_tool("my-tool", {}) (_, modern_opts), (_, legacy_opts) = (call for call in dispatcher.calls if call[0] == "tools/call") assert _headers(modern_opts)[MCP_NAME_HEADER] == "my-tool" assert MCP_NAME_HEADER not in _headers(legacy_opts) @pytest.mark.anyio async def test_stamp_table_rows_win_over_name_param_by_ordering() -> None: """A stamp-emitted `Mcp-Name` wins; `name_param` never overwrites an existing header.""" dispatcher = _RecordingDispatcher() request = _ShadowCallToolRequest(params={"name": "real-tool", "customKey": "other-value"}) with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_modern(session) await session.send_request(request, types.CallToolResult) [(_, opts)] = dispatcher.calls assert _headers(opts)[MCP_NAME_HEADER] == "real-tool" @pytest.mark.anyio async def test_vendor_name_param_emits_mcp_name_on_the_preconnect_path() -> None: """Emission is era-unconditional: a session that never adopts still emits `Mcp-Name`.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: await session.send_request(_GetWidgetRequest(params=_GetWidgetParams(widget_id="w-1")), types.EmptyResult) [(_, opts)] = dispatcher.calls assert _headers(opts) == {MCP_NAME_HEADER: "w-1"} # and no era headers: nothing adopted @pytest.mark.anyio async def test_missing_name_value_fails_loud_naming_method_and_key() -> None: """A missing name value raises ValueError naming the method and key, before the wire.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_handshake(session) with pytest.raises(ValueError) as exc_info: await session.send_request(_RawWidgetRequest(params={}), types.EmptyResult) assert dispatcher.calls == [] # raised before reaching the wire assert str(exc_info.value) == snapshot("vendor/widgets/get requires params['widgetId'] for Mcp-Name") @pytest.mark.anyio async def test_non_string_name_value_fails_loud() -> None: """A non-string name value raises the same ValueError as a missing one.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_handshake(session) with pytest.raises(ValueError) as exc_info: await session.send_request(_RawWidgetRequest(params={"widgetId": 7}), types.EmptyResult) assert dispatcher.calls == [] assert str(exc_info.value) == snapshot("vendor/widgets/get requires params['widgetId'] for Mcp-Name") @pytest.mark.anyio async def test_absent_params_fails_loud_not_attribute_error() -> None: """Absent params still raise the documented ValueError, not an AttributeError.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_handshake(session) with pytest.raises(ValueError) as exc_info: await session.send_request(_OptionalParamsWidgetRequest(), types.EmptyResult) assert dispatcher.calls == [] assert str(exc_info.value) == snapshot("vendor/widgets/get requires params['widgetId'] for Mcp-Name") @pytest.mark.anyio async def test_request_without_name_param_sends_no_mcp_name() -> None: """No `name_param` and a method outside the core table emits no `Mcp-Name` on either era.""" dispatcher = _RecordingDispatcher() with anyio.fail_after(5): async with ClientSession(dispatcher=dispatcher) as session: _adopt_modern(session) await session.send_request(_PlainVendorRequest(params={}), types.EmptyResult) _adopt_handshake(session) await session.send_ping() for _, opts in dispatcher.calls: assert MCP_NAME_HEADER not in _headers(opts)