docs: stop presenting the in-memory client as the way to connect (#3443)

This commit is contained in:
Max 2026-09-05 04:15:58 +01:00 • committed by GitHub
commit 7bb486a10f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,10 +1,14 @@
"""`docs/client/index.md`: every claim the page makes, proved against the real SDK."""
"""`docs/client/index.md`: every claim the page makes, proved against the real SDK.
`tutorial001` is the page's `server.py`. The other snippets are `client.py` programs that reach it
by URL, so each test drives the same calls in-process against `tutorial001.mcp` instead.
"""
import pytest
from inline_snapshot import snapshot
from mcp_types import Prompt, PromptArgument, PromptReference, TextContent, TextResourceContents, Tool
from docs_src.client import tutorial001, tutorial002, tutorial003, tutorial004, tutorial005, tutorial006, tutorial007
from docs_src.client import tutorial001, tutorial007
from mcp import Client, MCPDeprecationWarning, MCPError
from mcp.shared.metadata_utils import get_display_name
@ -12,31 +16,23 @@ from mcp.shared.metadata_utils import get_display_name
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
async def test_every_client_program_on_the_page_runs(capsys: pytest.CaptureFixture[str]) -> None:
"""Each `main()` is the literal client program shown on the page; all seven run clean in-memory."""
await tutorial001.main()
await tutorial002.main()
await tutorial003.main()
await tutorial004.main()
await tutorial005.main()
await tutorial006.main()
await tutorial007.main()
assert "Bookshop" in capsys.readouterr().out
async def test_connected_properties_are_populated_inside_the_block() -> None:
"""tutorial001: server_info, server_capabilities, protocol_version and instructions are just there."""
"""tutorial001_client's four prints, in-process against server.py: server_info, capabilities, protocol_version
and instructions are just there, and a capability the server lacks is None."""
async with Client(tutorial001.mcp) as client:
assert client.server_info is not None
assert client.server_info.name == "Bookshop"
assert client.protocol_version == "2026-07-28"
assert client.instructions == "Search the catalog before recommending a book."
assert client.server_capabilities.tools is not None
assert client.server_capabilities.resources is not None
assert client.server_capabilities.prompts is not None
assert client.server_capabilities.completions is not None
assert client.server_capabilities.logging is None
async def test_a_client_is_not_reusable_after_the_block_ends() -> None:
"""tutorial001: `async with` is the whole lifecycle. Construct a new Client per connection."""
"""The lifecycle bullet: `async with` is the whole lifecycle. Construct a new Client per connection."""
client = Client(tutorial001.mcp)
async with client:
assert client.server_info is not None
@ -46,13 +42,14 @@ async def test_a_client_is_not_reusable_after_the_block_ends() -> None:
async def test_list_tools_returns_the_full_definition() -> None:
"""tutorial002: each listed tool carries its name, title, description and the derived input schema."""
async with Client(tutorial002.mcp) as client:
(tool,) = (await client.list_tools()).tools
assert tool.name == "search_books"
assert tool.title == "Search the catalog"
assert tool.description == "Search the catalog by title or author."
assert tool.input_schema == snapshot(
"""tutorial002's listing, in-process: each tool carries its name, title, description and the derived
input schema, and a tool registered without `title=` lists with title None."""
async with Client(tutorial001.mcp) as client:
(search, lookup) = (await client.list_tools()).tools
assert search.name == "search_books"
assert search.title == "Search the catalog"
assert search.description == "Search the catalog by title or author."
assert search.input_schema == snapshot(
{
"type": "object",
"properties": {
@ -63,6 +60,8 @@ async def test_list_tools_returns_the_full_definition() -> None:
"title": "search_booksArguments",
}
)
assert lookup.name == "lookup_book"
assert lookup.title is None
def test_get_display_name_prefers_the_title() -> None:
@ -74,8 +73,8 @@ def test_get_display_name_prefers_the_title() -> None:
async def test_call_tool_result_has_three_things_to_read() -> None:
"""tutorial003: content for the model, structured_content for code, is_error for both."""
async with Client(tutorial003.mcp) as client:
"""tutorial003's call, in-process: content for the model, structured_content for code, is_error for both."""
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("lookup_book", {"title": "Dune"})
assert not result.is_error
(block,) = result.content
@ -85,8 +84,8 @@ async def test_call_tool_result_has_three_things_to_read() -> None:
async def test_a_raising_tool_is_a_result_not_an_exception() -> None:
"""tutorial003 `!!! check`: the ToolError's message comes back in content with is_error=True."""
async with Client(tutorial003.mcp) as client:
"""The `!!! check`: the ToolError's message comes back in content with is_error=True."""
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("lookup_book", {"title": "Solaris"})
assert result.is_error
(block,) = result.content
@ -97,7 +96,7 @@ async def test_a_raising_tool_is_a_result_not_an_exception() -> None:
async def test_an_unknown_tool_name_is_a_result_not_an_exception() -> None:
"""The `!!! warning`: a tool the server doesn't have comes back as is_error=True, not as MCPError."""
async with Client(tutorial003.mcp) as client:
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("does_not_exist", {})
assert result.is_error
(block,) = result.content
@ -107,8 +106,9 @@ async def test_an_unknown_tool_name_is_a_result_not_an_exception() -> None:
async def test_resources_and_templates_are_two_separate_lists() -> None:
"""tutorial004: concrete resources and parameterised templates come back from different verbs."""
async with Client(tutorial004.mcp) as client:
"""tutorial004's two listings, in-process: concrete resources and parameterised templates come back
from different verbs."""
async with Client(tutorial001.mcp) as client:
(resource,) = (await client.list_resources()).resources
assert resource.uri == "catalog://genres"
(template,) = (await client.list_resource_templates()).resource_templates
@ -116,8 +116,9 @@ async def test_resources_and_templates_are_two_separate_lists() -> None:
async def test_read_resource_fills_in_a_template() -> None:
"""tutorial004: read_resource takes a plain str URI; narrow the contents with isinstance."""
async with Client(tutorial004.mcp) as client:
"""tutorial004's read, in-process: read_resource takes a plain str URI, the server matches it to the
template, and the contents narrow with isinstance."""
async with Client(tutorial001.mcp) as client:
(contents,) = (await client.read_resource("catalog://genres/poetry")).contents
assert isinstance(contents, TextResourceContents)
assert contents.text == "3 books filed under poetry."
@ -126,7 +127,7 @@ async def test_read_resource_fills_in_a_template() -> None:
async def test_resource_subscriptions_are_listen_based_on_the_modern_wire() -> None:
"""The Resources section: at 2026-07-28 `resources.subscribe` is True (served via
subscriptions/listen) while the legacy subscribe_resource verb answers -32601."""
async with Client(tutorial004.mcp) as client:
async with Client(tutorial001.mcp) as client:
assert client.server_capabilities.resources is not None
assert client.server_capabilities.resources.subscribe is True
with pytest.raises(MCPError) as exc_info:
@ -138,8 +139,8 @@ async def test_resource_subscriptions_are_listen_based_on_the_modern_wire() -> N
async def test_list_prompts_describes_the_arguments() -> None:
"""tutorial005: a listed prompt carries its name, title and the arguments it needs."""
async with Client(tutorial005.mcp) as client:
"""tutorial005's listing, in-process: a listed prompt carries its name, title and the arguments it needs."""
async with Client(tutorial001.mcp) as client:
(prompt,) = (await client.list_prompts()).prompts
assert prompt == snapshot(
Prompt(
@ -152,8 +153,8 @@ async def test_list_prompts_describes_the_arguments() -> None:
async def test_get_prompt_renders_the_messages() -> None:
"""tutorial005: get_prompt returns the rendered messages a host hands to the model."""
async with Client(tutorial005.mcp) as client:
"""tutorial005's render, in-process: get_prompt returns the messages a host hands to the model."""
async with Client(tutorial001.mcp) as client:
result = await client.get_prompt("recommend", {"genre": "poetry"})
(message,) = result.messages
assert message.role == "user"
@ -163,8 +164,9 @@ async def test_get_prompt_renders_the_messages() -> None:
async def test_complete_suggests_values_for_an_argument() -> None:
"""tutorial006: complete takes a ref and a name/value pair and returns the matching values."""
async with Client(tutorial006.mcp) as client:
"""tutorial006's call, in-process: complete takes a ref and a name/value pair and returns the matching
values."""
async with Client(tutorial001.mcp) as client:
result = await client.complete(
ref=PromptReference(type="ref/prompt", name="recommend"),
argument={"name": "genre", "value": "p"},
@ -172,16 +174,17 @@ async def test_complete_suggests_values_for_an_argument() -> None:
assert result.completion.values == ["poetry"]
async def test_a_single_page_server_ends_the_pagination_loop_immediately() -> None:
"""tutorial007: every list_* takes cursor=; next_cursor is None when there is nothing left."""
async with Client(tutorial007.mcp) as client:
page = await client.list_tools(cursor=None)
assert page.next_cursor is None
assert [tool.name for tool in page.tools] == ["search_books", "reserve_book"]
async def test_the_pagination_loop_collects_every_tool_from_a_single_page_server() -> None:
"""tutorial007's `list_all_tools`, driven in-process against server.py: MCPServer answers in one page,
so the loop ends on the first `next_cursor is None` with every tool collected."""
async with Client(tutorial001.mcp) as client:
tools = await tutorial007.list_all_tools(client)
assert [tool.name for tool in tools] == ["search_books", "lookup_book"]
assert (await client.list_tools(cursor=None)).next_cursor is None
async def test_raise_exceptions_is_a_constructor_flag() -> None:
"""The `## In tests` section: `raise_exceptions=True` is accepted by the in-memory Client."""
async with Client(tutorial001.mcp, raise_exceptions=True) as client:
result = await client.call_tool("search_books", {"query": "dune"})
assert result.structured_content == {"result": "Found 3 books matching 'dune'."}
assert result.structured_content == {"result": "Found 3 books matching 'dune' (showing up to 10)."}