Files
modelcontextprotocol--pytho…/tests/docs_src/test_progress.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) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

103 lines
4.5 KiB
Python

"""`docs/handlers/progress.md`: every claim the page makes, proved against the real SDK."""
import inspect
import anyio
import pytest
from mcp_types import TextContent
from docs_src.progress import tutorial001, tutorial002
from mcp import Client
# 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")]
URLS = ["https://example.com/a.json", "https://example.com/b.json"]
async def test_context_parameter_is_invisible_to_the_model() -> None:
"""tutorial001: `ctx` comes from the type hint and never reaches the input schema."""
async with Client(tutorial001.mcp) as client:
(tool,) = (await client.list_tools()).tools
assert tool.input_schema["properties"] == {
"urls": {"items": {"type": "string"}, "title": "Urls", "type": "array"}
}
assert tool.input_schema["required"] == ["urls"]
async def test_each_report_becomes_one_callback_invocation_in_order() -> None:
"""tutorial001: `progress_callback` receives every `(progress, total, message)` the tool reported."""
updates: list[tuple[float, float | None, str | None]] = []
async def show(progress: float, total: float | None, message: str | None) -> None:
updates.append((progress, total, message))
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("import_catalog", {"urls": URLS}, progress_callback=show)
assert updates == [
(1, 2, "Imported https://example.com/a.json"),
(2, 2, "Imported https://example.com/b.json"),
]
assert result.content == [TextContent(type="text", text="Imported 2 records.")]
assert result.structured_content == {"result": "Imported 2 records."}
async def test_over_a_wire_dispatcher_callbacks_race_the_result() -> None:
"""The `!!! info`: only the in-memory connection runs the callback inline.
On a wire dispatcher (`mode="legacy"` here) each progress notification starts its own task, so
`call_tool` can return while a slow callback is still running. The callbacks below block on an
event that is only set *after* `call_tool` has returned: exactly the situation the page tells
you not to rule out.
"""
release = anyio.Event()
done = anyio.Event()
finished: list[float] = []
async def gated(progress: float, total: float | None, message: str | None) -> None:
await release.wait()
finished.append(progress)
if len(finished) == 2:
done.set()
async with Client(tutorial001.mcp, mode="legacy") as client:
with anyio.fail_after(5):
result = await client.call_tool("import_catalog", {"urls": URLS}, progress_callback=gated)
assert finished == []
release.set()
with anyio.fail_after(5):
await done.wait()
assert sorted(finished) == [1, 2]
assert result.structured_content == {"result": "Imported 2 records."}
async def test_without_a_callback_report_progress_is_a_no_op() -> None:
"""The `!!! check`: omit `progress_callback` and the tool runs to the same result, no error."""
async with Client(tutorial001.mcp) as client:
result = await client.call_tool("import_catalog", {"urls": URLS})
assert not result.is_error
assert result.structured_content == {"result": "Imported 2 records."}
def test_progress_callback_is_per_call_not_per_client() -> None:
"""The `!!! warning`: `call_tool` takes `progress_callback`; the `Client` constructor does not."""
assert "progress_callback" in inspect.signature(Client.call_tool).parameters
assert "progress_callback" not in inspect.signature(Client.__init__).parameters
async def test_omitting_total_reaches_the_callback_as_none() -> None:
"""tutorial002: a report without `total` arrives as `total=None`: activity, not a percentage."""
updates: list[tuple[float, float | None, str | None]] = []
async def show(progress: float, total: float | None, message: str | None) -> None:
updates.append((progress, total, message))
async with Client(tutorial002.mcp) as client:
result = await client.call_tool("import_feed", {"feed_url": "https://example.com/feed"}, progress_callback=show)
assert updates == [
(1, None, "Imported https://example.com/feed#Dune"),
(2, None, "Imported https://example.com/feed#Neuromancer"),
(3, None, "Imported https://example.com/feed#Hyperion"),
]
assert result.structured_content == {"result": "Imported 3 records."}