"""`docs/handlers/dependencies.md`: every claim the page makes, proved against the real SDK.""" from typing import Literal import pytest from inline_snapshot import snapshot from mcp_types import CreateMessageRequestParams, CreateMessageResult, ElicitRequestParams, ElicitResult, TextContent from docs_src.dependencies import tutorial001, tutorial002, tutorial003, tutorial004 from mcp import Client from mcp.client import ClientRequestContext pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")] async def test_the_resolver_fills_the_parameter_from_the_tools_own_argument() -> None: """tutorial001: `check_stock` receives `title` by name and its return value becomes `stock`.""" async with Client(tutorial001.mcp) as client: in_stock = await client.call_tool("reserve_book", {"title": "Dune"}) sold_out = await client.call_tool("reserve_book", {"title": "Neuromancer"}) assert in_stock.content == [TextContent(type="text", text="Reserved 'Dune' (6 copies left).")] assert sold_out.content == [TextContent(type="text", text="'Neuromancer' is out of stock.")] async def test_the_resolved_parameter_is_invisible_to_the_model() -> None: """tutorial001: the input schema shown on the page is exactly what `tools/list` reports.""" async with Client(tutorial001.mcp) as client: (tool,) = (await client.list_tools()).tools assert tool.input_schema == snapshot( { "type": "object", "properties": {"title": {"title": "Title", "type": "string"}}, "required": ["title"], "title": "reserve_bookArguments", } ) async def test_a_client_supplied_value_for_a_resolved_parameter_is_ignored() -> None: """tutorial001: the resolver's value is the only one the tool can receive.""" async with Client(tutorial001.mcp) as client: result = await client.call_tool("reserve_book", {"title": "Dune", "stock": {"title": "Dune", "copies": 999}}) assert result.content == [TextContent(type="text", text="Reserved 'Dune' (6 copies left).")] async def test_a_resolver_can_depend_on_another_resolver() -> None: """tutorial002: `estimate_delivery` consumes `check_stock`'s result, and the tool gets both.""" async with Client(tutorial002.mcp) as client: in_stock = await client.call_tool("order_book", {"title": "Dune"}) backorder = await client.call_tool("order_book", {"title": "Neuromancer"}) assert in_stock.content == [TextContent(type="text", text="Ordered 'Dune'; it arrives tomorrow.")] assert backorder.content == [ TextContent(type="text", text="'Neuromancer' is on backorder; it would arrive in 2-3 weeks.") ] async def test_a_shared_dependency_runs_once_per_call(monkeypatch: pytest.MonkeyPatch) -> None: """tutorial002: `stock` and `delivery` both need `check_stock`; one call, one inventory lookup.""" class CountingInventory: def __init__(self, data: dict[str, int]) -> None: self.data = data self.lookups: list[str] = [] def get(self, key: str, default: int) -> int: self.lookups.append(key) return self.data.get(key, default) inventory = CountingInventory(dict(tutorial002.INVENTORY)) monkeypatch.setattr(tutorial002, "INVENTORY", inventory) async with Client(tutorial002.mcp) as client: await client.call_tool("order_book", {"title": "Dune"}) assert inventory.lookups == ["Dune"] # Memoization is per call, not per server: the next call looks the title up again. await client.call_tool("order_book", {"title": "Dune"}) assert inventory.lookups == ["Dune", "Dune"] # The `!!! info` claims the tutorial003 behaviour is transport-independent, so each claim is # proved on both: mode="legacy" elicits synchronously mid-call (2025-11-25 and earlier), while # mode="auto" negotiates 2026-07-28, where the question rides a multi-round-trip `tools/call` # and `Client` drives the retries. @pytest.mark.parametrize("mode", ["legacy", "auto"]) async def test_an_in_stock_order_asks_no_question(mode: Literal["legacy", "auto"]) -> None: """tutorial003: `confirm_backorder` returns directly when stock exists - no round-trip.""" async def never(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: # pragma: no cover raise AssertionError("an in-stock order must not elicit") async with Client(tutorial003.mcp, mode=mode, elicitation_callback=never) as client: result = await client.call_tool("order_book", {"title": "Dune"}) assert result.content == [TextContent(type="text", text="Ordered 'Dune'.")] @pytest.mark.parametrize("mode", ["legacy", "auto"]) @pytest.mark.parametrize( ("confirm", "expected"), [ (True, "Backordered 'Neuromancer'; it ships in 2-3 weeks."), (False, "No order placed."), ], ) async def test_an_out_of_stock_order_asks_and_honours_the_answer( mode: Literal["legacy", "auto"], confirm: bool, expected: str ) -> None: """tutorial003: the resolver elicits, the SDK validates the answer, the tool reads it.""" asked: list[str] = [] async def on_elicit(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: asked.append(params.message) return ElicitResult(action="accept", content={"confirm": confirm}) async with Client(tutorial003.mcp, mode=mode, elicitation_callback=on_elicit) as client: result = await client.call_tool("order_book", {"title": "Neuromancer"}) assert result.content == [TextContent(type="text", text=expected)] assert asked == ["'Neuromancer' is out of stock (2-3 weeks). Order anyway?"] @pytest.mark.parametrize("mode", ["legacy", "auto"]) async def test_declining_an_unwrapped_dependency_aborts_the_call(mode: Literal["legacy", "auto"]) -> None: """tutorial003: no answer, no order - the error text on the page is the real one.""" async def decline(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: return ElicitResult(action="decline") async with Client(tutorial003.mcp, mode=mode, elicitation_callback=decline) as client: result = await client.call_tool("order_book", {"title": "Neuromancer"}) assert result.is_error assert isinstance(result.content[0], TextContent) assert result.content[0].text == ( "Error executing tool order_book: Resolver for parameter 'backorder' could not resolve: elicitation was decline" ) @pytest.mark.parametrize("mode", ["legacy", "auto"]) async def test_a_resolver_can_sample_the_clients_llm(mode: Literal["legacy", "auto"]) -> None: """tutorial004: `suggest_title` runs through the client's sampling callback on both eras.""" prompts: list[str] = [] async def sampler(context: ClientRequestContext, params: CreateMessageRequestParams) -> CreateMessageResult: content = params.messages[0].content assert isinstance(content, TextContent) prompts.append(content.text) return CreateMessageResult(role="assistant", content=TextContent(type="text", text="Dune"), model="m") async with Client(tutorial004.mcp, mode=mode, sampling_callback=sampler) as client: result = await client.call_tool("recommend_book", {"genre": "sci-fi"}) assert result.content == [TextContent(type="text", text="Today's sci-fi pick: Dune")] assert prompts == ["Suggest one sci-fi book title. Answer with the title only."]