Files
modelcontextprotocol--pytho…/tests/docs_src/test_client_callbacks.py
T
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

130 lines
5.9 KiB
Python

"""`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")])
)