chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Asserts progress + log notifications arrive in order, then cancels a call mid-flight."""
|
||||
|
||||
import anyio
|
||||
from mcp_types import LoggingMessageNotificationParams
|
||||
|
||||
from mcp.client import Client
|
||||
from stories._harness import Target, run_client
|
||||
|
||||
|
||||
async def main(target: Target, *, mode: str = "auto") -> None:
|
||||
# `logging_callback` is constructor-only on `Client`, so the list it fills
|
||||
# has to exist before the connection does.
|
||||
logs: list[LoggingMessageNotificationParams] = []
|
||||
|
||||
async def on_log(params: LoggingMessageNotificationParams) -> None:
|
||||
logs.append(params)
|
||||
|
||||
async with Client(target, mode=mode, logging_callback=on_log) as client:
|
||||
# ── progress + logging: a short countdown delivers exactly `steps` of each, in order ──
|
||||
updates: list[tuple[float, float | None, str | None]] = []
|
||||
|
||||
async def collect(progress: float, total: float | None, message: str | None) -> None:
|
||||
updates.append((progress, total, message))
|
||||
|
||||
result = await client.call_tool("countdown", {"steps": 3}, progress_callback=collect)
|
||||
assert result.structured_content == {"completed": 3, "total": 3}, result
|
||||
assert updates == [(1.0, 3.0, "step 1/3"), (2.0, 3.0, "step 2/3"), (3.0, 3.0, "step 3/3")]
|
||||
assert [(m.level, m.logger, m.data) for m in logs] == [
|
||||
("info", "countdown", "step 1/3"),
|
||||
("info", "countdown", "step 2/3"),
|
||||
("info", "countdown", "step 3/3"),
|
||||
]
|
||||
|
||||
# ── cancellation: abandon the awaiting scope once the call is provably in flight ──
|
||||
in_flight = anyio.Event()
|
||||
with anyio.fail_after(5):
|
||||
with anyio.CancelScope() as scope:
|
||||
|
||||
async def cancel_once_in_flight(progress: float, total: float | None, message: str | None) -> None:
|
||||
in_flight.set()
|
||||
scope.cancel()
|
||||
|
||||
await client.call_tool("countdown", {"steps": 1_000}, progress_callback=cancel_once_in_flight)
|
||||
|
||||
assert in_flight.is_set(), "the call must have started before it was cancelled"
|
||||
assert scope.cancelled_caught, "abandoning the scope should have cancelled the in-flight call"
|
||||
|
||||
# The session survives cancellation: a follow-up call still works.
|
||||
after = await client.call_tool("countdown", {"steps": 1}, progress_callback=collect)
|
||||
assert after.structured_content == {"completed": 1, "total": 1}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_client(main)
|
||||
Reference in New Issue
Block a user