You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b5d40d6
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -61,7 +61,7 @@ asyncio.run(main())
61
61
The same text the `@mcp.tool()` version produced. Two honest differences:
62
62
63
63
*`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.
65
65
66
66
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.
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.
147
145
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)**.
149
147
150
148
## Dictionaries
151
149
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:
153
151
154
152
```python title="server.py" hl_lines="9"
155
153
--8<--"docs_src/structured_output/tutorial006.py"
@@ -158,7 +156,6 @@ Ask for a two-day forecast and `structured_content` is `{"result": [{...}, {...}
The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper.
171
168
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.
173
170
174
171
## Validation
175
172
@@ -215,6 +212,8 @@ The opposite, `structured_output=True`, turns the automatic detection into a req
215
212
216
213
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.
217
214
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
+
218
217
## A class without type hints
219
218
220
219
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
244
243
## Recap
245
244
246
245
* 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.
248
247
* Every result carries `content` (text, for the model) **and**`structured_content` (data, for the application).
249
248
* 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.
251
250
252
251
You now own everything a tool can say back. Next, the second primitive: **[Resources](resources.md)**.
0 commit comments