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
Treat content-block, Image and Audio return annotations as unstructured tool output
A tool annotated to return a content block (-> EmbeddedResource, -> TextContent,
-> list[ContentBlock], ...) had the block model's own pydantic schema published as
its output_schema and every block echoed into structured_content a second time,
while Image/Audio inside a generic (-> list[Image], -> Image | Audio) failed to
register at all. -> Image escaped only because Image is a plain class.
In auto-detect mode, an annotation that mentions a content block class or the
Image/Audio helpers anywhere in its type tree now derives no output schema,
matching what _convert_to_content already does with those values at runtime.
structured_output=True still forces a schema. Behaviour change vs v1/2.0, so it
is documented in the migration guide and the structured-output page.
Copy file name to clipboardExpand all lines: docs/migration.md
+17-1Lines changed: 17 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -677,7 +677,7 @@ All submodules under `mcp.server.fastmcp.*` are now under `mcp.server.mcpserver.
677
677
Beyond the changes covered in this section, the everyday `FastMCP` surface carries over to `MCPServer` as-is:
678
678
679
679
-**Decorators.**`@mcp.tool()`, `@mcp.resource()`, `@mcp.prompt()`, and `@mcp.completion()` take the same arguments and handler signatures as v1. The lowlevel [`on_completion` reshape](#lowlevel-server-decorator-based-handlers-replaced-with-constructor-on_-params) applies only to the lowlevel `Server`; a high-level `@mcp.completion()` handler is still called as `(ref, argument, context)`.
680
-
-**Tool return handling.** A returned `CallToolResult` (including an `Annotated[CallToolResult, YourModel]` output schema, and `_meta`) is passed through, `Image` and `Audio` convert to content blocks as before, ready-made content blocks are kept as-is, and dict, list, scalar, and model returns are wrapped into `content` and `structured_content` by the same rules.
680
+
-**Tool return handling.** A returned `CallToolResult` (including an `Annotated[CallToolResult, YourModel]` output schema, and `_meta`) is passed through, `Image` and `Audio` convert to content blocks as before, ready-made content blocks are kept as-is (neither is [structured by default](#content-block-image-and-audio-return-annotations-are-unstructured) now, even inside a `list`), and dict, list, scalar, and model returns are wrapped into `content` and `structured_content` by the same rules.
681
681
-**Listing and registration methods.**`list_tools()`, `list_resources()`, `list_resource_templates()`, and `list_prompts()` return the same lists and are still what the protocol handlers call, so subclass overrides still take effect. `add_tool()`, `add_resource()`, and `add_prompt()` are unchanged.
682
682
-**Helpers.**`Image.to_image_content()`, `Audio.to_audio_content()`, and the prompt `Message`, `UserMessage`, and `AssistantMessage` classes.
683
683
-**Lifespan.** The `lifespan=` constructor argument and `ctx.request_context.lifespan_context` work as before, and the class is still generic over the lifespan result: `FastMCP[MyState]` becomes `MCPServer[MyState]`. (`Context`'s own type parameters did change; see [`RequestContext` type parameters simplified](#requestcontext-type-parameters-simplified).)
@@ -924,6 +924,22 @@ running on the event-loop thread:
924
924
925
925
Declare the handler `async def` to keep it on the event loop.
926
926
927
+
### Content-block, `Image`, and `Audio` return annotations are unstructured
928
+
929
+
A tool whose return annotation mentions a content block (`TextContent`,
930
+
`ImageContent`, `AudioContent`, `ResourceLink`, `EmbeddedResource`), `Image`, or
931
+
`Audio` anywhere (alone, or inside a `list`, `tuple`, or union) now registers
932
+
with no `output_schema` and returns no `structured_content`; `content` is built
933
+
exactly as before. v1 (and 2.0) published the block type's own Pydantic schema as
934
+
the tool's `output_schema` and echoed the serialized blocks into
935
+
`structured_content` a second time, while an annotation with `Image` or `Audio`
936
+
inside a generic (`-> list[Image]`) raised at registration (a bare `-> Image` was
937
+
already unstructured).
938
+
939
+
If a client reads `structured_content` from such a tool, return a model,
940
+
`TypedDict`, or `dict` instead, or pass `structured_output=True` to keep
Copy file name to clipboardExpand all lines: docs/servers/structured-output.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -210,7 +210,7 @@ The opposite, `structured_output=True`, turns the automatic detection into a req
210
210
211
211
## A class without type hints
212
212
213
-
There is one way to end up unstructured without asking for it: return a class that has **no annotations on its body**.
213
+
Content blocks and media (`TextContent`, `EmbeddedResource`, `Image`, `Audio` and friends, alone or inside a `list` or union) are not structured automatically: they are for the model to read (**[Images, audio & icons](media.md)** covers `Image` and `Audio`). Beyond those, there is one way to end up unstructured without asking for it: return a class that has **no annotations on its body**.
214
214
215
215
```python title="server.py" hl_lines="6-9"
216
216
--8<--"docs_src/structured_output/tutorial009.py"
@@ -240,6 +240,6 @@ There is one way to end up unstructured without asking for it: return a class th
240
240
* 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.
241
241
* Every result carries `content` (text, for the model) **and**`structured_content` (data, for the application).
242
242
* What you return is validated against the schema. A mismatch is a tool error, not a corrupt result.
243
-
*`structured_output=False` opts a tool out. A class without type hints opts out silently; watch for it.
243
+
*`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.
244
244
245
245
You now own everything a tool can say back. Next, the second primitive: **[Resources](resources.md)**.
0 commit comments