Skip to content

Commit 40cbe03

Browse files
committed
Connect the subscription examples over HTTP
Real clients connect over streamable HTTP (or stdio, once served); the in-memory client is a maintainer tool for tests and stays there. The examples now construct Client("http://localhost:8000/mcp") themselves, and the tests drive the example functions through an in-memory client instead of main().
1 parent f40df18 commit 40cbe03

6 files changed

Lines changed: 65 additions & 54 deletions

File tree

docs/handlers/subscriptions.md

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,21 @@ Two things the stream is *not*:
5050
its content. To narrow that, serve the method with your own handler on the low-level
5151
`Server` and acknowledge a smaller filter than the client asked for.
5252

53-
!!! warning "Not every transport serves the stream"
54-
`subscriptions/listen` needs a transport that can stream a request's response: streamable
55-
HTTP, or the in-process `Client(server)` the examples below use. Over stdio (and other
56-
stream-pair transports) a 2026-07-28 connection rejects it with METHOD_NOT_FOUND, even
57-
though `server/discover` still advertises the subscription capabilities there. The
58-
open-stream semantics haven't been built for that transport yet.
53+
!!! warning "Streamable HTTP only, for now"
54+
`subscriptions/listen` needs a transport that can stream a request's response, which today
55+
means streamable HTTP. Over stdio a 2026-07-28 connection rejects the method with
56+
METHOD_NOT_FOUND, even though `server/discover` advertises the subscription capabilities
57+
there. Serving it over stdio is planned; the open-stream semantics for that transport are
58+
not built yet.
5959

6060
## Watching the stream
6161

6262
On the client, a subscription is one context manager. Entering it sends the request and waits for the server's acknowledgment, so the stream is live by the time the block starts.
6363

64-
```python title="client.py" hl_lines="16 20 22"
64+
```python title="client.py" hl_lines="16 19 29"
6565
--8<-- "docs_src/subscriptions/tutorial003.py"
6666
```
6767

68-
The examples connect in-process with `Client(mcp)`. Against a running server, pass its URL:
69-
70-
```python
71-
async def main() -> None:
72-
async with Client("http://localhost:8000/mcp") as client:
73-
await follow_board(client)
74-
```
75-
7668
Iteration yields four typed events: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged`, and `ResourceUpdated(uri=...)`.
7769

7870
An event says *what* changed, never *how*. That is why `follow_board` calls `read_resource` and `list_tools`: the event is a cue to refetch. Read `event.uri` rather than assuming which resource moved, because once you subscribe to any URI the client delivers every `ResourceUpdated` on the stream.
@@ -90,23 +82,23 @@ Two more properties of the handle:
9082

9183
Open the subscription first, then start the watcher and get on with your work.
9284

93-
`app.py` imports `mcp` and `read_board` from the two files above, which live on disk as `tutorial001.py` and `tutorial003.py`. If you save the three side by side instead, drop the leading dot from those imports.
85+
`app.py` imports `BOARD` and `read_board` from `client.py` above. If you save the files side by side rather than as a package, drop the leading dot from that import.
9486

9587
=== "asyncio"
9688

97-
```python title="app.py" hl_lines="20 22"
89+
```python title="app.py" hl_lines="18 20"
9890
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
9991
```
10092

10193
=== "trio"
10294

103-
```python title="app.py" hl_lines="20 23"
95+
```python title="app.py" hl_lines="18 21"
10496
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
10597
```
10698

10799
=== "anyio"
108100

109-
```python title="app.py" hl_lines="20 23"
101+
```python title="app.py" hl_lines="18 21"
110102
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
111103
```
112104

docs_src/subscriptions/tutorial003.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,8 @@ async def follow_board(client: Client) -> None:
2323
print("tools:", [tool.name for tool in tools.tools])
2424
case _:
2525
pass # kinds the filter did not ask for never arrive
26+
27+
28+
async def main() -> None:
29+
async with Client("http://localhost:8000/mcp") as client:
30+
await follow_board(client)

docs_src/subscriptions/tutorial004_anyio.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,29 @@
33
from mcp import Client
44
from mcp.client.subscriptions import Subscription
55

6-
from .tutorial001 import mcp
7-
from .tutorial003 import read_board
6+
from .tutorial003 import BOARD, read_board
87

98

109
async def watch(client: Client, sub: Subscription) -> None:
1110
async for _event in sub:
1211
board = await read_board(client)
1312
print(board)
1413
if "[ ]" not in board:
15-
return # sprint finished: the stream closes when main() leaves the block
14+
return # sprint finished: the stream closes when run_sprint leaves the block
15+
16+
17+
async def run_sprint(client: Client) -> None:
18+
async with client.listen(resource_subscriptions=[BOARD]) as sub:
19+
print(await read_board(client)) # snapshot: acknowledged, so nothing after this is missed
20+
async with anyio.create_task_group() as tg:
21+
tg.start_soon(watch, client, sub)
22+
for task in ("design", "build", "ship"):
23+
await client.call_tool("complete_task", {"board": "sprint", "task": task})
1624

1725

1826
async def main() -> None:
19-
async with Client(mcp) as client:
20-
async with client.listen(resource_subscriptions=["board://sprint"]) as sub:
21-
print(await read_board(client)) # snapshot: acknowledged, so nothing after this is missed
22-
async with anyio.create_task_group() as tg:
23-
tg.start_soon(watch, client, sub)
24-
for task in ("design", "build", "ship"):
25-
await client.call_tool("complete_task", {"board": "sprint", "task": task})
27+
async with Client("http://localhost:8000/mcp") as client:
28+
await run_sprint(client)
2629

2730

2831
if __name__ == "__main__":

docs_src/subscriptions/tutorial004_asyncio.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,29 @@
33
from mcp import Client
44
from mcp.client.subscriptions import Subscription
55

6-
from .tutorial001 import mcp
7-
from .tutorial003 import read_board
6+
from .tutorial003 import BOARD, read_board
87

98

109
async def watch(client: Client, sub: Subscription) -> None:
1110
async for _event in sub:
1211
board = await read_board(client)
1312
print(board)
1413
if "[ ]" not in board:
15-
return # sprint finished: the stream closes when main() leaves the block
14+
return # sprint finished: the stream closes when run_sprint leaves the block
15+
16+
17+
async def run_sprint(client: Client) -> None:
18+
async with client.listen(resource_subscriptions=[BOARD]) as sub:
19+
print(await read_board(client)) # snapshot: acknowledged, so nothing after this is missed
20+
watcher = asyncio.create_task(watch(client, sub))
21+
for task in ("design", "build", "ship"):
22+
await client.call_tool("complete_task", {"board": "sprint", "task": task})
23+
await watcher # returns once the watcher has seen the finished board
1624

1725

1826
async def main() -> None:
19-
async with Client(mcp) as client:
20-
async with client.listen(resource_subscriptions=["board://sprint"]) as sub:
21-
print(await read_board(client)) # snapshot: acknowledged, so nothing after this is missed
22-
watcher = asyncio.create_task(watch(client, sub))
23-
for task in ("design", "build", "ship"):
24-
await client.call_tool("complete_task", {"board": "sprint", "task": task})
25-
await watcher # returns once the watcher has seen the finished board
27+
async with Client("http://localhost:8000/mcp") as client:
28+
await run_sprint(client)
2629

2730

2831
if __name__ == "__main__":

docs_src/subscriptions/tutorial004_trio.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,29 @@
33
from mcp import Client
44
from mcp.client.subscriptions import Subscription
55

6-
from .tutorial001 import mcp
7-
from .tutorial003 import read_board
6+
from .tutorial003 import BOARD, read_board
87

98

109
async def watch(client: Client, sub: Subscription) -> None:
1110
async for _event in sub:
1211
board = await read_board(client)
1312
print(board)
1413
if "[ ]" not in board:
15-
return # sprint finished: the stream closes when main() leaves the block
14+
return # sprint finished: the stream closes when run_sprint leaves the block
15+
16+
17+
async def run_sprint(client: Client) -> None:
18+
async with client.listen(resource_subscriptions=[BOARD]) as sub:
19+
print(await read_board(client)) # snapshot: acknowledged, so nothing after this is missed
20+
async with trio.open_nursery() as nursery:
21+
nursery.start_soon(watch, client, sub)
22+
for task in ("design", "build", "ship"):
23+
await client.call_tool("complete_task", {"board": "sprint", "task": task})
1624

1725

1826
async def main() -> None:
19-
async with Client(mcp) as client:
20-
async with client.listen(resource_subscriptions=["board://sprint"]) as sub:
21-
print(await read_board(client)) # snapshot: acknowledged, so nothing after this is missed
22-
async with trio.open_nursery() as nursery:
23-
nursery.start_soon(watch, client, sub)
24-
for task in ("design", "build", "ship"):
25-
await client.call_tool("complete_task", {"board": "sprint", "task": task})
27+
async with Client("http://localhost:8000/mcp") as client:
28+
await run_sprint(client)
2629

2730

2831
if __name__ == "__main__":

tests/docs_src/test_subscriptions.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,22 +234,27 @@ def _assert_snapshot_then_current_board(printed: str) -> None:
234234

235235

236236
async def test_the_asyncio_watcher_runs_beside_the_main_flow(capsys: pytest.CaptureFixture[str]) -> None:
237-
"""tutorial004 (asyncio tab): main() opens the subscription, snapshots the board, then a watcher
238-
task reprints it while main() keeps calling tools."""
239-
await tutorial004_asyncio.main()
237+
"""tutorial004 (asyncio tab): run_sprint opens the subscription, snapshots the board, then a watcher
238+
task reprints it while the main flow keeps calling tools.
239+
240+
The example connects over HTTP; the in-memory client here is the maintainer-side stand-in."""
241+
async with Client(tutorial001.mcp) as client:
242+
await tutorial004_asyncio.run_sprint(client)
240243
_assert_snapshot_then_current_board(capsys.readouterr().out)
241244

242245

243246
@pytest.mark.parametrize("anyio_backend", [pytest.param("trio", id="trio")])
244247
async def test_the_trio_watcher_runs_beside_the_main_flow(capsys: pytest.CaptureFixture[str]) -> None:
245248
"""tutorial004 (trio tab): the same shape as the asyncio tab, with a nursery owning the watcher."""
246-
await tutorial004_trio.main()
249+
async with Client(tutorial001.mcp) as client:
250+
await tutorial004_trio.run_sprint(client)
247251
_assert_snapshot_then_current_board(capsys.readouterr().out)
248252

249253

250254
async def test_the_anyio_watcher_runs_beside_the_main_flow(capsys: pytest.CaptureFixture[str]) -> None:
251255
"""tutorial004 (anyio tab): the same shape again, with a task group owning the watcher."""
252-
await tutorial004_anyio.main()
256+
async with Client(tutorial001.mcp) as client:
257+
await tutorial004_anyio.run_sprint(client)
253258
_assert_snapshot_then_current_board(capsys.readouterr().out)
254259

255260

0 commit comments

Comments
 (0)