Skip to content

Commit 53ca740

Browse files
committed
docs: embedded resources and resource links on the media page
The media page claimed to cover everything a tool can return besides text but stopped at Image/Audio. Add "Embedding a resource" (a tool returning an EmbeddedResource with the registered resource's URI and MIME type; TextResourceContents vs BlobResourceContents) and "Linking to a resource" (a mixed list result with a ResourceLink the client follows, plus the dangling-link failure), with two new docs_src modules and the tests that pin every claim. No-Verification-Needed: docs, docs_src examples and tests only
1 parent fb443cc commit 53ca740

6 files changed

Lines changed: 442 additions & 8 deletions

File tree

‎docs/servers/index.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Around the three primitives, the rest of what a server declares:
1818
* **[Completions](completions.md)** is server-side autocomplete for prompt
1919
and resource-template arguments.
2020
* **[Images, audio & icons](media.md)** covers everything a tool can
21-
return besides text, and the icons a client shows next to your server.
21+
return besides text (images, audio, embedded documents, links to
22+
resources), and the icons a client shows next to your server.
2223
* **[Handling errors](handling-errors.md)** explains the difference between an
2324
error the model can recover from and one it must never see.
2425

‎docs/servers/media.md‎

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Text is not the only thing a tool can return.
44

5-
The SDK ships two helpers for binary results (**`Image`** and **`Audio`**) and an **`Icon`** type for giving your server, tools, resources, and prompts a face in the client's UI.
5+
The SDK ships two helpers for binary results (**`Image`** and **`Audio`**) and an **`Icon`** type for giving your server, tools, resources, and prompts a face in the client's UI. Two protocol types cover results that are documents rather than pictures or sound: an **`EmbeddedResource`** carries a resource's contents inside the result, and a **`ResourceLink`** points at one for the client to read.
66

77
## Returning an image
88

@@ -31,7 +31,9 @@ Two things to notice:
3131
!!! info
3232
`ImageContent` and `AudioContent` live in `mcp.types`, right next to the `TextContent`
3333
that a plain `str` result becomes (**[Tools](tools.md)**). A tool result is a list of content blocks; `Image` and `Audio` are
34-
the shortest way to produce the two binary kinds.
34+
the shortest way to produce the two binary kinds. The other two kinds, `EmbeddedResource` and
35+
`ResourceLink`, are for documents rather than media: one carries the contents, the other points
36+
at them. Each has its own section below.
3537

3638
### Try it
3739

@@ -62,7 +64,7 @@ Same deal: a file on disk in, base64 and a MIME type out, no output schema.
6264

6365
## Bytes or a file
6466

65-
Both helpers also accept `data=` (raw bytes) instead of `path=`. That is the mode for bytes that never came from a file of their own — a database column, an HTTP response, something Pillow just drew:
67+
Both helpers also accept `data=` (raw bytes) instead of `path=`. That is the mode for bytes that never came from a file of their own (a database column, an HTTP response, something Pillow just drew):
6668

6769
```python title="server.py" hl_lines="14 15"
6870
--8<-- "docs_src/media/tutorial003.py"
@@ -81,6 +83,143 @@ A suffix it doesn't recognise falls back to `application/octet-stream`.
8183
`Audio` from MP3 bytes that way and the client is told `mime_type="audio/wav"`, then
8284
faithfully fails to decode it. When you pass `data=`, pass `format=`.
8385

86+
## Embedding a resource
87+
88+
An **embedded resource** is a document carried inside a tool result: its text or bytes, the URI it lives at, and a MIME type.
89+
90+
A `str` result puts words in front of the model and an `Image` puts pixels there, but neither says what the content *is* or where it came from. An embedded resource does, which is what lets a client treat the block as a file rather than a string: it can render the Markdown, offer a download, or recognise a URI it already has open as context.
91+
92+
Say the example server, a brand kit, publishes its usage guidelines as a resource at `brand://guidelines`. That is how the *application* reads them, as **[Resources](resources.md)** describes. To hand the same document to the *model* on request, return it from a tool:
93+
94+
```python title="server.py" hl_lines="7 14 16-18"
95+
--8<-- "docs_src/media/tutorial005.py"
96+
```
97+
98+
* `brand://guidelines` is an ordinary resource, declared exactly as it would be if no tool embedded it.
99+
* `brand_guidelines` is a tool that returns the same document as an `EmbeddedResource`. There is no SDK helper here as there is for images: `EmbeddedResource` and `TextResourceContents` are the protocol types from `mcp.types`, and the block you build goes into the result untouched.
100+
* The inner `TextResourceContents` holds the `uri`, the `mime_type`, and the `text`, exactly what a client receives when it reads a text resource. The text comes from calling `guidelines()` directly, an ordinary function call (the decorator hands your function back unchanged), so the resource and the tool serve one document from one place and the client needs no follow-up `resources/read`.
101+
102+
On the wire that is one block whose `type` is `"resource"`, with the contents nested one level down, and no `structuredContent` key at all: like an `Image`, a document is content for the model to read, not data for the application to parse.
103+
104+
```json
105+
{
106+
"content": [
107+
{
108+
"type": "resource",
109+
"resource": {
110+
"uri": "brand://guidelines",
111+
"mimeType": "text/markdown",
112+
"text": "# Brand guidelines\n\nUse the primary colour for calls to action.\n"
113+
}
114+
}
115+
],
116+
"isError": false,
117+
"resultType": "complete"
118+
}
119+
```
120+
121+
The URI does not have to name a registered resource: any scheme is legal, and the tool call succeeds either way. Prefer one that resolves, because the URI is what a client uses to reopen or refresh the document later, and to tell that this block and `brand://guidelines` are the same thing.
122+
123+
### Text or bytes
124+
125+
`TextResourceContents` carries `text`. A binary document (the full brand book as a PDF, say) goes in a `BlobResourceContents` instead, and the base64 encoding that `Image` did for you is now yours to do:
126+
127+
```python
128+
import base64
129+
from pathlib import Path
130+
131+
from mcp.types import BlobResourceContents, EmbeddedResource
132+
133+
EmbeddedResource(
134+
resource=BlobResourceContents(
135+
uri="brand://book",
136+
mime_type="application/pdf",
137+
blob=base64.b64encode(Path("brand-book.pdf").read_bytes()).decode(),
138+
)
139+
)
140+
```
141+
142+
For a picture the model should *look at*, `Image` is still the better choice: an `ImageContent` is the block clients know to display, while what a client does with an embedded `image/png` blob is up to that client. Embed bytes for something that is genuinely a file, such as a PDF, a font, or an archive of assets.
143+
144+
## Linking to a resource
145+
146+
A **resource link** is a pointer: the URI and name of a resource the client can read, plus a little metadata about it, and none of its contents.
147+
148+
Embedding copies the document into the result, which is wasteful when the document is large or the model may never need it. A logo PNG is tens of kilobytes that most callers only need to know exists; a link to it is a couple of hundred bytes, and the client decides whether to spend a `resources/read` on the rest.
149+
150+
Put any PNG next to `server.py` as `logo.png`, publish it as a resource at `brand://logo`, and return one result that mixes text, an embedded document, and a link:
151+
152+
```python title="server.py" hl_lines="17 24 30 32-39"
153+
--8<-- "docs_src/media/tutorial006.py"
154+
```
155+
156+
* `brand://logo` is a resource whose function returns `bytes`, so a client that reads it receives a blob.
157+
* `brand_brief` returns a **list**, and each item becomes one content block, in order: the `str` becomes a `TextContent` and the two protocol blocks pass through untouched. This is how any tool returns several blocks of different kinds.
158+
* The guidelines are a few lines of Markdown the model needs now, so they are embedded. `annotations=` is a field of every content block and sits on the `EmbeddedResource` beside `resource=`, not inside the `TextResourceContents`; `audience=["assistant"]` marks the block as context for the model rather than something to display.
159+
* The logo is linked. `ResourceLink` has the fields of a `resources/list` entry: `name` and `uri` are required; `title`, `description`, `mime_type`, and `size` are what a client shows a person deciding whether to fetch it. `size` is the raw byte count before base64, so the client can also estimate what reading it would cost in context window.
160+
161+
On the wire the three blocks arrive in order. The link is flat where the embedded resource is nested:
162+
163+
```json
164+
{
165+
"content": [
166+
{
167+
"type": "text",
168+
"text": "Follow the attached guidelines. The logo is linked; read it if you need the pixels."
169+
},
170+
{
171+
"type": "resource",
172+
"resource": {
173+
"uri": "brand://guidelines",
174+
"mimeType": "text/markdown",
175+
"text": "# Brand guidelines\n\nUse the primary colour for calls to action.\n"
176+
},
177+
"annotations": {"audience": ["assistant"]}
178+
},
179+
{
180+
"type": "resource_link",
181+
"name": "logo",
182+
"title": "Brand logo",
183+
"uri": "brand://logo",
184+
"description": "Full-colour logo, 2000 by 2000 pixels.",
185+
"mimeType": "image/png",
186+
"size": 48213
187+
}
188+
],
189+
"isError": false,
190+
"resultType": "complete"
191+
}
192+
```
193+
194+
None of the logo's bytes are in the result, only its description and size (`48213` here; yours is the size of whichever `logo.png` you used). A client that wants the pixels narrows the block to a `ResourceLink` and reads its URI like any other resource; what comes back is the logo as a blob:
195+
196+
```python
197+
async def show_logo(client: Client, result: CallToolResult) -> None:
198+
link = result.content[2]
199+
assert isinstance(link, ResourceLink) # narrow the union before touching .uri
200+
logo = await client.read_resource(link.uri)
201+
print(logo.contents) # [BlobResourceContents(uri="brand://logo", mime_type="image/png", blob="iVBORw0KGgo...")]
202+
```
203+
204+
That is two requests instead of one, and the second happens only if somebody wants it. As a rule, embed what the model needs now and can read (mostly text), and link what is large, optional, or one of many: a tool that finds forty matching files returns forty links, not forty files.
205+
206+
!!! check
207+
A link is a promise that `resources/read` will answer for that URI, and nothing checks the
208+
promise when the tool returns. Add a second link, to `brand://wordmark`, without registering
209+
a resource there: the tool call succeeds, and the client that follows the link is the one
210+
that finds out.
211+
212+
```text
213+
mcp.shared.exceptions.MCPError: Unknown resource: brand://wordmark
214+
```
215+
216+
That is a JSON-RPC error (code `-32602`) raised at the caller, not a tool result the model
217+
can read. A link does not have to appear in `resources/list` (a URI served by a resource
218+
template never does), but it does have to resolve. Register the resource, then hand out its
219+
address.
220+
221+
A prompt message can carry either block too; **[Prompts](prompts.md#more-than-text)** shows a file embedded in one.
222+
84223
## Icons
85224

86225
An `Icon` is metadata, not content. It doesn't carry the image; it points at one with a URI, and a client may fetch it and show it next to your server's name, a tool, a resource, or a prompt.
@@ -110,6 +249,9 @@ A tool's icons are on the `Tool` object from `tools/list`, a resource's on the `
110249

111250
* Return an `Image` or `Audio` from a tool and the client receives an `ImageContent` / `AudioContent` block: your bytes base64-encoded, with a MIME type.
112251
* Build one from a `path=` and let the suffix decide the MIME type, or from in-memory `data=` plus an explicit `format=`.
252+
* Return an `EmbeddedResource` to put a whole document in the result: a `TextResourceContents` (or a `BlobResourceContents` with a base64 `blob`) carrying its `uri`, a MIME type, and the contents.
253+
* Return a `ResourceLink` to send only the pointer. The client follows it with `resources/read`, so link to URIs that resolve.
254+
* A list return value becomes one content block per item, so one result can mix text, media, documents and links.
113255
* Media results carry no `structured_content` and no output schema.
114256
* An `Icon` is a pointer: a `src` URI plus optional `mime_type`, `sizes`, and `theme`.
115257
* `icons=[...]` works on the server, on tools, on resources, and on prompts, and clients find them on the matching objects.

‎docs/servers/structured-output.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ The opposite, `structured_output=True`, turns the automatic detection into a req
210210

211211
## Content blocks and media
212212

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.
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)** shows a tool returning each of them). `structured_output=True` still forces one for the content-block classes.
214214

215215
## A class without type hints
216216

‎docs_src/media/tutorial005.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from mcp.server import MCPServer
2+
from mcp.types import EmbeddedResource, TextResourceContents
3+
4+
mcp = MCPServer("Brand kit")
5+
6+
7+
@mcp.resource("brand://guidelines", mime_type="text/markdown")
8+
def guidelines() -> str:
9+
"""How to use the brand assets."""
10+
return "# Brand guidelines\n\nUse the primary colour for calls to action.\n"
11+
12+
13+
@mcp.tool()
14+
def brand_guidelines() -> EmbeddedResource:
15+
"""The brand guidelines as a Markdown document."""
16+
return EmbeddedResource(
17+
resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text=guidelines())
18+
)

‎docs_src/media/tutorial006.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from pathlib import Path
2+
3+
from mcp.server import MCPServer
4+
from mcp.types import Annotations, EmbeddedResource, ResourceLink, TextResourceContents
5+
6+
mcp = MCPServer("Brand kit")
7+
8+
LOGO_FILE = Path(__file__).parent / "logo.png"
9+
10+
11+
@mcp.resource("brand://guidelines", mime_type="text/markdown")
12+
def guidelines() -> str:
13+
"""How to use the brand assets."""
14+
return "# Brand guidelines\n\nUse the primary colour for calls to action.\n"
15+
16+
17+
@mcp.resource("brand://logo", mime_type="image/png")
18+
def logo() -> bytes:
19+
"""The brand logo as a PNG."""
20+
return LOGO_FILE.read_bytes()
21+
22+
23+
@mcp.tool()
24+
def brand_brief() -> list[str | EmbeddedResource | ResourceLink]:
25+
"""Everything needed to produce on-brand work."""
26+
return [
27+
"Follow the attached guidelines. The logo is linked; read it if you need the pixels.",
28+
EmbeddedResource(
29+
resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text=guidelines()),
30+
annotations=Annotations(audience=["assistant"]),
31+
),
32+
ResourceLink(
33+
name="logo",
34+
title="Brand logo",
35+
uri="brand://logo",
36+
mime_type="image/png",
37+
size=LOGO_FILE.stat().st_size,
38+
description="Full-colour logo, 2000 by 2000 pixels.",
39+
),
40+
]

0 commit comments

Comments
 (0)