Skip to content

Commit b5d40d6

Browse files
Merge branch 'main' into issue-3683-file-system-support
2 parents 7cc4f99 + d7845c0 commit b5d40d6

37 files changed

Lines changed: 563 additions & 375 deletions

‎.github/workflows/main.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ permissions:
1111

1212
jobs:
1313
checks:
14-
uses: ./.github/workflows/shared.yml
14+
uses: $/.github/workflows/shared.yml
1515

1616
all-green:
1717
if: always()

‎.github/workflows/publish-pypi.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
path: dist/
3939

4040
checks:
41-
uses: ./.github/workflows/shared.yml
41+
uses: $/.github/workflows/shared.yml
4242

4343
pypi-publish:
4444
name: Upload release to PyPI

‎.pre-commit-config.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ repos:
66
hooks:
77
- id: end-of-file-fixer
88

9-
- repo: https://github.com/pre-commit/mirrors-prettier
10-
rev: v3.1.0
9+
- repo: https://github.com/rbubley/mirrors-prettier
10+
rev: v3.9.10
1111
hooks:
1212
- id: prettier
1313
types_or: [yaml, json5]

‎docs/advanced/extensions.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ Done. The server now advertises `io.modelcontextprotocol/ui` under
2525
Extensions are fixed at construction. There is no `add_extension` to call later:
2626
a server's capability map should not change while clients are connected to it.
2727

28-
The capability map rides `server/discover`, which is a **2026-07-28** path. A legacy
29-
`initialize` handshake has nowhere to put it, so a legacy client simply doesn't see
30-
the extension. Design for that: an extension *augments* a server, it must not be the
31-
only way the server is usable.
28+
The capability map rides `server/discover` on **2026-07-28** connections and the
29+
`initialize` result on legacy ones. A client that doesn't know an extension ignores
30+
it. Design for that: an extension *augments* a server, it must not be the only way
31+
the server is usable.
3232

3333
## Writing your own
3434

‎docs/advanced/low-level-server.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ asyncio.run(main())
6161
The same text the `@mcp.tool()` version produced. Two honest differences:
6262

6363
* `result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
64-
* `list_tools` returns the schema **you** typed, character for character. The high-level version had `"title": "Query"` on every property and a `"title": "search_booksArguments"` at the root: Pydantic artifacts. Down here, if it's on the wire, you put it there.
64+
* `list_tools` returns the schema **you** typed, character for character. Down here, if it's on the wire, you put it there.
6565

6666
In a test you skip uvicorn and the port: `Client(server)` takes a low-level `Server` in-process exactly like it takes an `MCPServer`, and **[Testing](../get-started/testing.md)** is that pattern.
6767

‎docs/client/index.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,10 @@ and `tool.input_schema` is the JSON Schema the server derived from the function'
7272
{
7373
"type": "object",
7474
"properties": {
75-
"query": {"title": "Query", "type": "string"},
76-
"limit": {"default": 10, "title": "Limit", "type": "integer"}
75+
"query": {"type": "string"},
76+
"limit": {"default": 10, "type": "integer"}
7777
},
78-
"required": ["query"],
79-
"title": "search_booksArguments"
78+
"required": ["query"]
8079
}
8180
```
8281

‎docs/handlers/context.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ This is the part to internalise. Here is the input schema `tools/list` reports f
3030
{
3131
"type": "object",
3232
"properties": {
33-
"query": {"title": "Query", "type": "string"}
33+
"query": {"type": "string"}
3434
},
35-
"required": ["query"],
36-
"title": "search_booksArguments"
35+
"required": ["query"]
3736
}
3837
```
3938

‎docs/handlers/dependencies.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,9 @@ Here is the input schema `tools/list` reports for `reserve_book`:
2828
{
2929
"type": "object",
3030
"properties": {
31-
"title": {"title": "Title", "type": "string"}
31+
"title": {"type": "string"}
3232
},
33-
"required": ["title"],
34-
"title": "reserve_bookArguments"
33+
"required": ["title"]
3534
}
3635
```
3736

‎docs/handlers/lifespan.md‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ Nothing new. `ctx` is a **Context** parameter, so the SDK injects it and it neve
3232
{
3333
"type": "object",
3434
"properties": {
35-
"genre": {"title": "Genre", "type": "string"}
35+
"genre": {"type": "string"}
3636
},
37-
"required": ["genre"],
38-
"title": "count_booksArguments"
37+
"required": ["genre"]
3938
}
4039
```
4140

‎docs/servers/structured-output.md‎

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ Because of it, the tool the SDK sends during `tools/list` carries an `output_sch
1919
```json
2020
{
2121
"properties": {
22-
"result": {"title": "Result", "type": "integer"}
22+
"result": {"type": "integer"}
2323
},
2424
"required": ["result"],
25-
"title": "get_temperatureOutput",
2625
"type": "object"
2726
}
2827
```
@@ -59,9 +58,9 @@ Declare the shape as a Pydantic `BaseModel` and return an instance:
5958
```json
6059
{
6160
"properties": {
62-
"temperature": {"description": "Degrees Celsius.", "title": "Temperature", "type": "number"},
63-
"humidity": {"description": "Relative humidity, 0 to 1.", "title": "Humidity", "type": "number"},
64-
"conditions": {"title": "Conditions", "type": "string"}
61+
"temperature": {"description": "Degrees Celsius.", "type": "number"},
62+
"humidity": {"description": "Relative humidity, 0 to 1.", "type": "number"},
63+
"conditions": {"type": "string"}
6564
},
6665
"required": ["temperature", "humidity", "conditions"],
6766
"title": "WeatherData",
@@ -125,31 +124,30 @@ A `list[...]` isn't a JSON object either, so it gets the `{"result": ...}` wrapp
125124
"$defs": {
126125
"WeatherData": {
127126
"properties": {
128-
"temperature": {"title": "Temperature", "type": "number"},
129-
"humidity": {"title": "Humidity", "type": "number"},
130-
"conditions": {"title": "Conditions", "type": "string"}
127+
"temperature": {"type": "number"},
128+
"humidity": {"type": "number"},
129+
"conditions": {"type": "string"}
131130
},
132131
"required": ["temperature", "humidity", "conditions"],
133132
"title": "WeatherData",
134133
"type": "object"
135134
}
136135
},
137136
"properties": {
138-
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "title": "Result", "type": "array"}
137+
"result": {"items": {"$ref": "#/$defs/WeatherData"}, "type": "array"}
139138
},
140139
"required": ["result"],
141-
"title": "get_forecastOutput",
142140
"type": "object"
143141
}
144142
```
145143

146144
Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}]}`. `content` becomes **two** `TextContent` blocks, one per item: a list is flattened for the model rather than dumped as one string.
147145

148-
`tuple[...]`, unions, and `Optional[...]` are wrapped the same way.
146+
`tuple[...]`, unions, and `Optional[...]` are wrapped the same way, and so are `set` and `frozenset`, with or without an item type. A bare `list` or `tuple` is not: see **[Content blocks and media](#content-blocks-and-media)**.
149147

150148
## Dictionaries
151149

152-
`dict[str, ...]` is the one generic that already *is* a JSON object, so it isn't wrapped:
150+
`dict[str, ...]` (or `typing.Dict[str, ...]`, or a bare `dict`, read as `dict[str, Any]`) is the one generic that already *is* a JSON object, so it isn't wrapped:
153151

154152
```python title="server.py" hl_lines="9"
155153
--8<-- "docs_src/structured_output/tutorial006.py"
@@ -158,7 +156,6 @@ Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}
158156
```json
159157
{
160158
"additionalProperties": {"type": "number"},
161-
"title": "get_temperaturesDictOutput",
162159
"type": "object"
163160
}
164161
```
@@ -169,7 +166,7 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4}
169166

170167
The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper.
171168

172-
Dictionary results use Pydantic's `TypeAdapter` for validation and serialization. If you inspect a tool's `FuncMetadata.output_model`, it holds the dictionary type annotation with its schema title.
169+
Dictionary results use Pydantic's `TypeAdapter` for validation and serialization. If you inspect a tool's `FuncMetadata.output_model`, it holds the dictionary type annotation itself.
173170

174171
## Validation
175172

@@ -215,6 +212,8 @@ The opposite, `structured_output=True`, turns the automatic detection into a req
215212

216213
Content blocks and media (`TextContent`, `EmbeddedResource`, `Image`, `Audio` and friends, on their own, as the items of a `list`, `tuple` or `Sequence`, or as the arms of a union) are opted out for you: they are for the model to read, so auto-detection derives no schema from them (**[Images, audio & icons](media.md)** covers `Image` and `Audio`). `structured_output=True` still forces one for the content-block classes.
217214

215+
A bare `list` or `tuple` is opted out too. Nothing in the annotation says its items aren't blocks, and a schema would send any image it holds twice: once as a block for the model, and again as base64 in `structured_content`. Write the item type (`list[str]`, `tuple[int, ...]`) to get a schema, or pass `structured_output=True` to wrap it as a list of anything. The forced wrapper still has to serialize every item as JSON, so an `Image` or `Audio` in it makes the call fail. Only a bare `list` or `tuple` as the whole annotation is opted out: `list | None` is wrapped like any other union.
216+
218217
## A class without type hints
219218

220219
There is one way to end up unstructured without asking for it: return a class that has **no annotations on its body**.
@@ -244,9 +243,9 @@ There is one way to end up unstructured without asking for it: return a class th
244243
## Recap
245244

246245
* The **return type annotation** is the output schema. It's published in `tools/list` as `output_schema`.
247-
* Scalars, lists, tuples and unions are wrapped in `{"result": ...}`. Models, `TypedDict`s, dataclasses, annotated classes and `dict[str, ...]` are objects already and stay as they are.
246+
* Scalars, `list[...]`, `tuple[...]`, sets and unions are wrapped in `{"result": ...}`. Models, `TypedDict`s, dataclasses, annotated classes, `dict[str, ...]` and a bare `dict` are objects already and stay as they are.
248247
* Every result carries `content` (text, for the model) **and** `structured_content` (data, for the application).
249248
* What you return is validated against the schema. A mismatch is a tool error, not a corrupt result.
250-
* `structured_output=False` opts a tool out. Content blocks, `Image` and `Audio` opt out by default; a class without type hints opts out silently, so watch for it.
249+
* `structured_output=False` opts a tool out. Content blocks, `Image`, `Audio` and a bare `list` or `tuple` opt out by default; a class without type hints opts out silently, so watch for it.
251250

252251
You now own everything a tool can say back. Next, the second primitive: **[Resources](resources.md)**.

0 commit comments

Comments
 (0)