|
1 | | -"""Request task-augmented execution, then drive the task lifecycle via `tasks/*`.""" |
| 1 | +"""Start a tool call as a task, then poll the task to completion and fetch its result. |
| 2 | +
|
| 3 | +`Client` exposes only spec verbs, so the `task` augmentation and the `tasks/*` |
| 4 | +methods drop to `client.session`. The thin `_start_task` / `_get_task` / |
| 5 | +`_task_result` helpers keep that `cast` noise out of the story below; `main` |
| 6 | +itself reads as: kick off the work, see it as a task, collect the report. |
| 7 | +""" |
2 | 8 |
|
3 | 9 | from typing import cast |
4 | 10 |
|
5 | 11 | import mcp_types as types |
6 | 12 |
|
7 | | -from mcp.client import Client |
| 13 | +from mcp.client import Client, ClientSession |
8 | 14 | from mcp.server.tasks import EXTENSION_ID, RELATED_TASK_META_KEY |
9 | 15 | from stories._harness import Target, run_client |
10 | 16 |
|
11 | 17 |
|
| 18 | +async def _start_task(session: ClientSession, name: str, arguments: dict[str, object]) -> types.CallToolResult: |
| 19 | + """Call a tool with task augmentation; the result carries the task id in `_meta`.""" |
| 20 | + request = types.CallToolRequest( |
| 21 | + params=types.CallToolRequestParams(name=name, arguments=arguments, task=types.TaskMetadata(ttl=60)) |
| 22 | + ) |
| 23 | + return await session.send_request(cast("types.ClientRequest", request), types.CallToolResult) |
| 24 | + |
| 25 | + |
| 26 | +async def _get_task(session: ClientSession, task_id: str) -> types.GetTaskResult: |
| 27 | + request = types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)) |
| 28 | + return await session.send_request(cast("types.ClientRequest", request), types.GetTaskResult) |
| 29 | + |
| 30 | + |
| 31 | +async def _task_result(session: ClientSession, task_id: str) -> types.CallToolResult: |
| 32 | + request = types.GetTaskPayloadRequest(params=types.GetTaskPayloadRequestParams(task_id=task_id)) |
| 33 | + return await session.send_request(cast("types.ClientRequest", request), types.CallToolResult) |
| 34 | + |
| 35 | + |
12 | 36 | async def main(target: Target, *, mode: str = "auto") -> None: |
13 | 37 | async with Client(target, mode=mode) as client: |
14 | 38 | # The extensions capability map rides `server/discover` (modern only); a legacy |
15 | 39 | # connection (today's stdio) omits it, so assert it only when present. |
16 | 40 | if client.server_capabilities.extensions is not None: |
17 | 41 | assert client.server_capabilities.extensions == {EXTENSION_ID: {"list": {}, "cancel": {}}} |
18 | 42 |
|
19 | | - # `Client` exposes only spec verbs, so task-augmented calls and the |
20 | | - # `tasks/*` methods drop to `client.session` (see custom_methods/). The |
21 | | - # casts satisfy the closed `ClientRequest` union; at runtime the body |
22 | | - # only calls `.model_dump()`. |
23 | | - session = client.session |
24 | | - call = types.CallToolRequest( |
25 | | - params=types.CallToolRequestParams( |
26 | | - name="echo", arguments={"text": "async"}, task=types.TaskMetadata(ttl=60) |
27 | | - ) |
28 | | - ) |
29 | | - result = await session.send_request(cast("types.ClientRequest", call), types.CallToolResult) |
30 | | - assert result.meta is not None, result |
31 | | - task_id = result.meta[RELATED_TASK_META_KEY]["taskId"] |
32 | | - assert isinstance(result.content[0], types.TextContent) |
33 | | - assert result.content[0].text == "async", result |
34 | | - |
35 | | - get = types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)) |
36 | | - status = await session.send_request(cast("types.ClientRequest", get), types.GetTaskResult) |
37 | | - assert status.status == "completed", status |
38 | | - |
39 | | - payload_req = types.GetTaskPayloadRequest(params=types.GetTaskPayloadRequestParams(task_id=task_id)) |
40 | | - payload = await session.send_request(cast("types.ClientRequest", payload_req), types.CallToolResult) |
41 | | - assert isinstance(payload.content[0], types.TextContent) |
42 | | - assert payload.content[0].text == "async", payload |
| 43 | + started = await _start_task(client.session, "render_report", {"title": "Q3", "sections": 2}) |
| 44 | + task_id = started.meta[RELATED_TASK_META_KEY]["taskId"] if started.meta else None |
| 45 | + assert task_id is not None, started |
| 46 | + |
| 47 | + task = await _get_task(client.session, task_id) |
| 48 | + assert task.status == "completed", task |
| 49 | + |
| 50 | + report = await _task_result(client.session, task_id) |
| 51 | + assert isinstance(report.content[0], types.TextContent) |
| 52 | + assert report.content[0].text.startswith("# Q3"), report |
43 | 53 |
|
44 | 54 |
|
45 | 55 | if __name__ == "__main__": |
|
0 commit comments