"""`docs/client/callbacks.md`: every claim the page makes, proved against the real SDK.""" import pytest from inline_snapshot import snapshot from mcp_types import ( INVALID_REQUEST, CreateMessageRequestParams, CreateMessageResult, ElicitRequestFormParams, ElicitRequestParams, ElicitResult, ErrorData, ListRootsResult, Root, SamplingMessage, TextContent, ) from pydantic import FileUrl from docs_src.client_callbacks import tutorial001, tutorial002, tutorial003, tutorial004 from mcp import Client, MCPError from mcp.client import ClientRequestContext # See test_index.py for why this is a per-module mark and not a conftest hook. pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")] async def test_the_callback_answers_the_servers_question() -> None: """tutorial001+002: the server's `ctx.elicit` is resolved by the client's `elicitation_callback`.""" async with Client(tutorial001.mcp, mode="legacy", elicitation_callback=tutorial002.handle_elicitation) as client: result = await client.call_tool("issue_card") assert not result.is_error assert result.content == [TextContent(type="text", text="Card issued to Ada Lovelace.")] async def test_the_callback_receives_the_servers_question_as_form_params() -> None: """tutorial002: the callback gets `ElicitRequestFormParams` (the message and the requested schema).""" received: list[ElicitRequestParams] = [] async def recording(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: received.append(params) return await tutorial002.handle_elicitation(context, params) async with Client(tutorial001.mcp, mode="legacy", elicitation_callback=recording) as client: await client.call_tool("issue_card") (params,) = received assert isinstance(params, ElicitRequestFormParams) assert params.mode == "form" assert params.message == "What name should go on the card?" assert params.requested_schema == snapshot( { "properties": {"name": {"title": "Name", "type": "string"}}, "required": ["name"], "title": "CardHolder", "type": "object", } ) async def test_returning_error_data_refuses_the_request_and_fails_the_call() -> None: """The callback's only other return type: `ErrorData` refuses the request and fails the whole call.""" async def refuse(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult | ErrorData: return ErrorData(code=INVALID_REQUEST, message="No forms here.") async with Client(tutorial001.mcp, mode="legacy", elicitation_callback=refuse) as client: with pytest.raises(MCPError, match="No forms here") as exc_info: await client.call_tool("issue_card") assert exc_info.value.error.code == INVALID_REQUEST async def test_without_the_callback_the_servers_request_is_refused() -> None: """The `!!! check`: no `elicitation_callback` means the SDK answers with an error and the call fails.""" async with Client(tutorial001.mcp, mode="legacy") as client: with pytest.raises(MCPError, match="Elicitation not supported") as exc_info: await client.call_tool("issue_card") assert exc_info.value.error.code == INVALID_REQUEST async def test_registering_the_callback_declares_the_capability() -> None: """tutorial003: `elicitation_callback` alone advertises exactly the `elicitation` capability.""" async with Client(tutorial003.mcp, mode="legacy", elicitation_callback=tutorial002.handle_elicitation) as client: result = await client.call_tool("client_features") assert result.structured_content == {"result": ["elicitation"]} async def test_no_callbacks_means_no_capabilities() -> None: """tutorial003: a client constructed without callbacks declares nothing.""" async with Client(tutorial003.mcp, mode="legacy") as client: result = await client.call_tool("client_features") assert result.structured_content == {"result": []} async def test_each_callback_declares_its_own_capability() -> None: """The page's table: the elicitation, sampling, and roots callbacks each declare their capability.""" async with Client( tutorial003.mcp, mode="legacy", elicitation_callback=tutorial002.handle_elicitation, sampling_callback=tutorial004.handle_sampling, list_roots_callback=tutorial004.handle_list_roots, ) as client: result = await client.call_tool("client_features") assert result.structured_content == {"result": ["elicitation", "sampling", "roots"]} async def test_the_modern_in_memory_path_has_no_back_channel() -> None: """The `!!! info`: under the default mode the negotiated path has no back-channel for `elicitation/create`.""" async with Client(tutorial001.mcp, elicitation_callback=tutorial002.handle_elicitation) as client: with pytest.raises(MCPError, match="no back-channel"): await client.call_tool("issue_card") async def test_the_deprecated_callbacks_return_what_the_page_says() -> None: """tutorial004: the sampling and roots callbacks produce the result types the page names.""" async with Client(tutorial003.mcp, mode="legacy") as client: context = ClientRequestContext(session=client.session, request_id=1) params = CreateMessageRequestParams( messages=[SamplingMessage(role="user", content=TextContent(type="text", text="6 * 7?"))], max_tokens=16, ) assert await tutorial004.handle_sampling(context, params) == snapshot( CreateMessageResult( role="assistant", content=TextContent(type="text", text="The answer is 42."), model="my-llm" ) ) assert await tutorial004.handle_list_roots(context) == snapshot( ListRootsResult(roots=[Root(uri=FileUrl("file:///home/ada/notebooks"), name="notebooks")]) )