Skip to content

Commit eb768d3

Browse files
Merge branch 'main' of https://github.com/modelcontextprotocol/python-sdk into fix/argmodel-forbid-extra-3067
2 parents 23dba52 + 74a242a commit eb768d3

72 files changed

Lines changed: 3876 additions & 519 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy-docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949

5050
- name: Build combined docs (v1.x at /, main at /v2/)
5151
run: bash scripts/build-docs.sh site
52+
env:
53+
# Silence mkdocs-material's MkDocs 2.0 warning banner in CI logs.
54+
NO_MKDOCS_2_WARNING: "1"
5255

5356
- name: Configure Pages
5457
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0

.github/workflows/docs-preview.yml

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
name: Docs Preview
22

3-
# Builds the mkdocs site for a PR and deploys it to Cloudflare Pages.
3+
# Builds the docs site for a PR and deploys it to Cloudflare Pages.
44
#
5-
# Security: mkdocs executes Python from the PR (mkdocstrings imports src/mcp,
6-
# `!!python/name:` directives). The build is gated by `authorize` (admin sender
7-
# for auto-preview, admin/maintainer commenter for /preview-docs) and isolated
8-
# from Cloudflare secrets — `build` runs PR code with no secrets and hands the
9-
# static site to `deploy` via an artifact, so PR code never shares a runner
10-
# with the Cloudflare token.
5+
# Security: the build executes Python from the PR (mkdocstrings imports
6+
# src/mcp, `!!python/name:` config directives run, and heads may ship their
7+
# own build scripts). The build is gated by `authorize` (admin sender for
8+
# auto-preview, admin/maintainer commenter for /preview-docs) and isolated
9+
# from Cloudflare secrets — `build` runs PR code with no secrets and hands
10+
# the static site to `deploy` via an artifact, so PR code never shares a
11+
# runner with the Cloudflare token.
1112
#
1213
# Required configuration:
1314
# - secrets.CLOUDFLARE_API_TOKEN (scope: Account → Cloudflare Pages → Edit)
@@ -21,6 +22,7 @@ on:
2122
- docs/**
2223
- docs_src/**
2324
- mkdocs.yml
25+
- scripts/docs/**
2426
- pyproject.toml
2527
issue_comment:
2628
types: [created]
@@ -128,14 +130,30 @@ jobs:
128130
enable-cache: false
129131
version: 0.9.5
130132

131-
- run: uv sync --frozen --group docs
132-
- run: uv run --frozen --no-sync mkdocs build
133+
# pull_request_target runs this workflow file from the base branch, so
134+
# the whole recipe — dependency sync included — must come from the
135+
# checkout itself: heads that ship scripts/docs/build.sh (the Zensical
136+
# toolchain) build with it; older heads, and v1.x heads previewed via
137+
# /preview-docs, still build with MkDocs. Both arms must write the site
138+
# to site/. Keep the detection in sync with build_site() in
139+
# scripts/build-docs.sh.
140+
- run: |
141+
if [ -f scripts/docs/build.sh ]; then
142+
bash scripts/docs/build.sh
143+
else
144+
uv sync --frozen --group docs
145+
# The env var silences mkdocs-material's MkDocs 2.0 warning banner.
146+
NO_MKDOCS_2_WARNING=1 uv run --frozen --no-sync mkdocs build
147+
fi
133148
134149
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
135150
with:
136151
name: site
137152
path: site/
138153
retention-days: 1
154+
# An empty site/ means the build arm broke its output contract; fail
155+
# here instead of surfacing as a confusing download error in deploy.
156+
if-no-files-found: error
139157

140158
deploy:
141159
needs: [authorize, build]

.github/workflows/shared.yml

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,23 @@ jobs:
1717
with:
1818
persist-credentials: false
1919

20-
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
20+
# setup-uv's manifest fetch is a single request with a hard 5s timeout
21+
# (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry.
22+
- name: Install uv
23+
id: setup-uv
24+
continue-on-error: true
25+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
26+
with:
27+
enable-cache: true
28+
version: 0.9.5
29+
30+
- name: Install uv (retry)
31+
if: steps.setup-uv.outcome == 'failure'
32+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
2133
with:
2234
enable-cache: true
2335
version: 0.9.5
36+
2437
- name: Install dependencies
2538
run: uv sync --frozen --all-extras --python 3.10
2639

@@ -44,8 +57,8 @@ jobs:
4457
name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }})
4558
runs-on: ${{ matrix.os }}
4659
timeout-minutes: 10
47-
continue-on-error: true
4860
strategy:
61+
fail-fast: false
4962
matrix:
5063
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
5164
dep-resolution:
@@ -60,7 +73,18 @@ jobs:
6073
with:
6174
persist-credentials: false
6275

76+
# setup-uv's manifest fetch is a single request with a hard 5s timeout
77+
# (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry.
6378
- name: Install uv
79+
id: setup-uv
80+
continue-on-error: true
81+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
82+
with:
83+
enable-cache: true
84+
version: 0.9.5
85+
86+
- name: Install uv (retry)
87+
if: steps.setup-uv.outcome == 'failure'
6488
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
6589
with:
6690
enable-cache: true
@@ -92,7 +116,19 @@ jobs:
92116
with:
93117
persist-credentials: false
94118

95-
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
119+
# setup-uv's manifest fetch is a single request with a hard 5s timeout
120+
# (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry.
121+
- name: Install uv
122+
id: setup-uv
123+
continue-on-error: true
124+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
125+
with:
126+
enable-cache: true
127+
version: 0.9.5
128+
129+
- name: Install uv (retry)
130+
if: steps.setup-uv.outcome == 'failure'
131+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
96132
with:
97133
enable-cache: true
98134
version: 0.9.5
@@ -115,7 +151,19 @@ jobs:
115151
with:
116152
persist-credentials: false
117153

118-
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
154+
# setup-uv's manifest fetch is a single request with a hard 5s timeout
155+
# (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry.
156+
- name: Install uv
157+
id: setup-uv
158+
continue-on-error: true
159+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
160+
with:
161+
enable-cache: true
162+
version: 0.9.5
163+
164+
- name: Install uv (retry)
165+
if: steps.setup-uv.outcome == 'failure'
166+
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
119167
with:
120168
enable-cache: true
121169
version: 0.9.5
@@ -125,3 +173,6 @@ jobs:
125173

126174
- name: Build the docs in strict mode
127175
run: uv run --frozen --no-sync mkdocs build --strict
176+
env:
177+
# Silence mkdocs-material's MkDocs 2.0 warning banner in CI logs.
178+
NO_MKDOCS_2_WARNING: "1"

RELEASE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ the publish job — `skip-existing` makes it skip whatever already landed. The
3939
commit — and therefore the README PyPI publishes — names the version
4040
being released. When entering a new phase (alpha → beta → rc), update
4141
the banner wording too.
42-
2. Check the full test matrix is green on the release commit. The matrix runs
43-
with `continue-on-error`, so a green workflow run does not mean the tests
44-
passed — check the individual jobs.
42+
2. Check the full test matrix is green on the release commit. The publish
43+
workflow re-runs the checks and blocks publishing until they pass, so a
44+
red leg there means re-running the failed jobs on the Publishing run.
4545
3. Create the release as a pre-release, passing the exact commit verified in
4646
step 2 as `--target` (otherwise the tag is created from whatever `main`'s
4747
HEAD is by then). The tagged commit determines everything about the

docs/client/caching.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ One rule sits above `"use"`: **calls carrying `meta` always reach the server.**
5353

5454
To turn caching off entirely, construct with `Client(server, cache=False)`: every call is a round trip again, and `cache_mode`, while still accepted, does nothing.
5555

56-
Scope is honored automatically too: `"private"` entries are keyed to the cache's *partition* (below), while `"public"` ones may opt into wider sharing. And **notifications beat TTL** for the exact entries they name: a `list_changed` notification evicts the matching cached listing, and `resources/updated` evicts the cached read stored under exactly its URI, however fresh they were.
56+
Scope is honored automatically too: `"private"` entries are keyed to the cache's *partition* (below), while `"public"` ones may opt into wider sharing. And **notifications beat TTL** for the exact entries they name: a `list_changed` notification evicts the matching cached listing, and `resources/updated` evicts the cached read stored under exactly its URI, however fresh they were. On a 2026-07-28 connection those notifications arrive on a `subscriptions/listen` stream you open with `client.listen(...)`, and eviction completes before your watcher sees the event; **[Subscriptions](subscriptions.md)** is that page.
5757

5858
One caveat on `resources/updated`: eviction is exact-URI only. The store contract has no enumerate or scan operation (same as the reference TypeScript implementation), so a notification carrying a *sub*-resource URI does not evict a cached read of its parent. If your server signals sub-resources this way, refetch the parent with `cache_mode="refresh"`.
5959

docs/client/callbacks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ When a client connects it declares its `capabilities`, the mirror image of the s
7878
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
7979
| none of them | `{}` |
8080

81+
Sampling sub-capabilities are the one refinement: pass `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` alongside `sampling_callback` when your sampler handles the `tools` / `tool_choice` parameters. Servers must see `sampling.tools` declared before they can send them.
82+
8183
`logging_callback` and `message_handler` are not in the table. They handle notifications, and notifications need no capability.
8284

8385
The server reads the declaration back with `ctx.session.check_client_capability(...)`. Add a tool that does:

docs/client/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ The resource verbs come in pairs: two ways to list, one way to read.
145145

146146
`read_resource` returns `contents`, a list of `TextResourceContents` or `BlobResourceContents`. Same idea as tool content: narrow with `isinstance`, then read `.text` (or `.blob`).
147147

148-
A client can also be told when a resource changes. On 2025-era connections that is `subscribe_resource(uri)` / `unsubscribe_resource(uri)` - a method pair `MCPServer` doesn't implement, so on the 2026-07-28 wire (where those verbs no longer exist) the request answers `-32601`, *Method not found*. The 2026 replacement is a `subscriptions/listen` stream, which `MCPServer` *does* serve - `server_capabilities.resources.subscribe` is `True` there, and the server side of the story is **[Subscriptions](../handlers/subscriptions.md)**.
148+
A client can also be told when a resource changes. On 2025-era connections that is `subscribe_resource(uri)` / `unsubscribe_resource(uri)` - a method pair `MCPServer` doesn't implement, so on the 2026-07-28 wire (where those verbs no longer exist) the request answers `-32601`, *Method not found*. The 2026 replacement is a `subscriptions/listen` stream, which `MCPServer` *does* serve - `server_capabilities.resources.subscribe` is `True` there - and consuming it with `client.listen(...)` is this section's **[Subscriptions](subscriptions.md)** page.
149149

150150
## Prompts
151151

docs/client/subscriptions.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Subscriptions
2+
3+
A server's catalog is not fixed. Tools appear at runtime, and the content behind a resource URI changes. A client hears about it through `client.listen(...)`: one `subscriptions/listen` request whose response *is* the stream. It stays open and carries the change notifications the client asked for.
4+
5+
This page is the client end: opening the stream, watching it beside your main flow, and handling its endings. Publishing changes, filtering, and serving the method are the server's side of the story, told in **[Subscriptions](../handlers/subscriptions.md)** under *Inside your handler*. The examples here talk to the sprint-board server built there.
6+
7+
## Watching the stream
8+
9+
A subscription is one context manager. Entering it sends the request, with your keyword arguments as the subscription filter, and waits for the server's acknowledgment, so the stream is live by the time the block starts.
10+
11+
```python title="client.py" hl_lines="16 19 29"
12+
--8<-- "docs_src/subscriptions/tutorial003.py"
13+
```
14+
15+
Iteration yields four typed events: `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged`, and `ResourceUpdated(uri=...)`.
16+
17+
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: a filter can name several URIs, and a server may report a change on a sub-resource of one of them.
18+
19+
Duplicate events waiting to be consumed collapse into one, and refetching still gets you the current state. Only identical events collapse: two `ResourceUpdated` for different URIs are two events.
20+
21+
Two more properties of the handle:
22+
23+
* `sub.honored` is the filter the server acknowledged: a `SubscriptionFilter` with the fields you passed, read as attributes (`sub.honored.prompts_list_changed`). `MCPServer` honors every kind you ask for, so it echoes your request back. A server that narrows the filter (see the [filter warning](../handlers/subscriptions.md#only-what-was-asked-for) on the server page) acknowledges less, and an honored kind may still never fire.
24+
* `sub.subscription_id` is the listen request's id, the one stamped on every frame of this stream. Several subscriptions can be open at once, each demultiplexed by its own id.
25+
26+
## Watching without blocking
27+
28+
`follow_board` runs until the server closes the stream, which may be never, so on its own it owns your program. Real clients want the watcher *beside* the main flow: an agent calls tools while a watcher keeps a cache or a UI current.
29+
30+
Open the subscription first, then start the watcher and get on with your work.
31+
32+
=== "asyncio"
33+
34+
```python title="app.py" hl_lines="18 20"
35+
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
36+
```
37+
38+
=== "trio"
39+
40+
```python title="app.py" hl_lines="18 21"
41+
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
42+
```
43+
44+
=== "anyio"
45+
46+
```python title="app.py" hl_lines="18 21"
47+
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
48+
```
49+
50+
!!! note
51+
`app.py` imports `BOARD` and `read_board` from the first example, which this repo stores as
52+
`tutorial003.py`. If you save the rendered files side by side as `client.py` and `app.py`,
53+
write `from client import BOARD, read_board` instead. The `watch.py` example further down
54+
imports `read_board` the same way.
55+
56+
The order is the point. Nothing is replayed, so an event published before your stream existed is missed. Entering `client.listen(...)` waits for the acknowledgment, so every change from that moment on reaches your watcher, and the snapshot you take inside the block cannot miss one.
57+
58+
Requests run freely beside an open stream, from the watcher task or any other, on the same client. Because *duplicate* unconsumed events coalesce, a busy main flow may produce one refetch rather than three. Events that differ do not coalesce: a filter naming many URIs queues one pending event per URI.
59+
60+
To stop watching, leave the block: there is no `unsubscribe` call. Cancelling the task that owns the block does that for you, and the SDK cancels the listen request the way the transport expects: over streamable HTTP, by closing that request's stream. A watcher that runs for the life of your app never returns on its own, so cancel it, or its task group's scope, at shutdown.
61+
62+
## Streams end
63+
64+
A stream ends in one of two ways, both ordinary control flow. A graceful server close ends the `async for`; an abrupt drop raises `SubscriptionLost`.
65+
66+
The difference is diagnostic, not a difference in what to do next: the stream is gone, nothing was replayed, and a watcher that still cares re-listens and refetches.
67+
68+
```python title="watch.py" hl_lines="16 20"
69+
--8<-- "docs_src/subscriptions/tutorial005.py"
70+
```
71+
72+
Servers close streams gracefully for their own reasons, including shedding a subscriber whose backlog grew too large, so a clean end is not a signal to stop watching. Back off before re-listening.
73+
74+
`SubscriptionLost` has one local cause too. The client holds at most 1024 unconsumed events, and a consumer that falls that far behind loses the subscription rather than grow without bound. Keep the body of the `async for` short and do slow work elsewhere.
75+
76+
`keep_following` catches only `SubscriptionLost`. Entering `listen()` can also raise `MCPError` (the connection failed, or the server does not serve the method), `TimeoutError` (no acknowledgment arrived), and `ListenNotSupportedError` (a pre-2026 connection). Decide which of those your watcher should retry: the last never heals.
77+
78+
## Recap
79+
80+
* Enter `async with client.listen(...)`; entering waits for the acknowledgment, so nothing published after it is missed.
81+
* Iterate with `async for event in sub`. Events are cues to refetch, never payloads.
82+
* Open the subscription, then run the watcher as a task, and tool calls keep flowing beside it.
83+
* A clean end stops the loop; a drop raises `SubscriptionLost`. Either way: re-listen, refetch, back off first.
84+
* Leaving the block is the unsubscribe.
85+
86+
Publishing these events, narrowing the filter, and scaling past one process are the server's story: **[Subscriptions](../handlers/subscriptions.md)**. These same events also keep a client-side cache honest, and **[Caching](caching.md)** is the next page.

0 commit comments

Comments
 (0)