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
Adds the narrative coverage and examples the SEP-1730 docs audit still
flagged as missing on main: a tool returning an EmbeddedResource
(media page), prompt messages carrying an embedded file and an image
plus adding prompts at runtime with the list-changed notification
(prompts page), ping and roots change notifications on a legacy session
(deprecated-features page), and the JSON Schema 2020-12 dialect
(low-level server page, with a pointer from tools). Docs and docs_src
only.
No-Verification-Needed: docs and docs_src examples only
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+11Lines changed: 11 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -111,6 +111,17 @@ The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-
111
111
112
112
The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **[Structured Output](../servers/structured-output.md)**.
113
113
114
+
## The dialect is JSON Schema 2020-12
115
+
116
+
`input_schema` and `output_schema` are JSON Schema, and the [MCP specification](https://modelcontextprotocol.io/specification/latest/basic#json-schema-usage) fixes the dialect: a schema with no `$schema` key is **JSON Schema 2020-12**. The schemas `MCPServer` generates rely on that default (Pydantic writes 2020-12 and omits the key), and a hand-written dict is held to it too, so the full 2020-12 vocabulary is available:
117
+
118
+
```python title="server.py" hl_lines="8 14-15"
119
+
--8<--"docs_src/lowlevel/tutorial007.py"
120
+
```
121
+
122
+
* The root of `input_schema` must be `"type": "object"`. Beside it, `oneOf`, `additionalProperties`, `anyOf`, `if`/`then`/`else`, `prefixItems`, `$defs` with local `$ref`s and the rest of the 2020-12 keywords reach the client exactly as written.
123
+
* No `$schema` key is needed. Add one only to opt into an older draft: this SDK's `Client`, which validates `structured_content` against a tool's `output_schema`, picks its validator from `$schema` and uses 2020-12 when there is none.
124
+
114
125
## `_meta`: for the application, not the model
115
126
116
127
`content` is the part of the answer the model reads. `structured_content` is the same answer as typed data. `_meta` is the third channel: data that rides along with the result for the **client application**, without being part of the answer at all.
Copy file name to clipboardExpand all lines: docs/deprecated.md
+79Lines changed: 79 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,6 +50,85 @@ MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SE
50
50
send. These two only work end-to-end on a `mode="legacy"` connection whose client
51
51
registered the matching callback.
52
52
53
+
## `ping` on a legacy session
54
+
55
+
A **ping** is an empty request either side can send to check that the other is still answering. The 2026-07-28 spec removes it ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575)): every request a modern client sends already proves the server is there, and a modern server has no channel to send one. Both SDK methods still work on a handshake-era session:
56
+
57
+
```python title="client.py" hl_lines="11 16-17"
58
+
from mcp import Client
59
+
from mcp.server import MCPServer
60
+
from mcp.server.mcpserver import Context
61
+
62
+
mcp = MCPServer("Bookshop")
63
+
64
+
65
+
@mcp.tool()
66
+
asyncdefcheck_client(ctx: Context) -> str:
67
+
"""A tool that still pings the client mid-call."""
68
+
await ctx.session.send_ping()
69
+
return"client answered"
70
+
71
+
72
+
asyncdefmain() -> None:
73
+
asyncwith Client(mcp, mode="legacy") as client:
74
+
await client.send_ping()
75
+
result =await client.call_tool("check_client", {})
*`client.send_ping()` is the client asking the server. It warns with `MCPDeprecationWarning` on every call and returns an `EmptyResult`; on a default (`2026-07-28`) connection the server answers `MCPError: Method not found` instead.
80
+
*`ctx.session.send_ping()` is the server asking the client from inside a handler. It carries no warning; on a modern connection it raises the same no-back-channel error as any other server-initiated request.
81
+
* Neither side registers anything to answer a ping. `mode="legacy"` is what makes both round trips possible.
82
+
83
+
## Roots change notifications
84
+
85
+
A 2025-era client that declared the roots capability can tell the server that its workspace folders changed by sending `notifications/roots/list_changed`; the server responds by requesting `roots/list` again. The 2026-07-28 spec removes the notification along with the rest of the push-style roots flow, and `MCPServer` has no hook for it, so this is a legacy client talking to a low-level `Server`, both in one module:
await anyio.sleep_forever() # keep the session open: the server's roots/list arrives on it
126
+
```
127
+
128
+
* Passing `list_roots_callback=` declares `"roots": {"listChanged": true}`, and `client.send_roots_list_changed()` keeps that promise. It warns, and it needs `mode="legacy"`: on a modern connection the notification is silently dropped.
129
+
*`Server(..., on_roots_list_changed=...)` registers the receiving handler (deprecated too, and it warns at construction). The notification carries no payload, so the handler calls `ctx.session.list_roots()` to fetch the new list.
130
+
* A notification has no response, so keep the session open after sending it, as `main()` does; the server's follow-up `roots/list` request arrives on that session and the handler prints the refreshed list.
Copy file name to clipboardExpand all lines: docs/servers/media.md
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -81,6 +81,24 @@ A suffix it doesn't recognise falls back to `application/octet-stream`.
81
81
`Audio` from MP3 bytes that way and the client is told `mime_type="audio/wav"`, then
82
82
faithfully fails to decode it. When you pass `data=`, pass `format=`.
83
83
84
+
## Embedding a resource
85
+
86
+
A tool can also return a document: some text or bytes together with the URI it lives at and a MIME type. That is an **`EmbeddedResource`**, another kind of content block. Unlike a plain `str` it tells the client what the content is, so the client can show it as an attachment or recognise a resource it already knows.
87
+
88
+
```python title="server.py" hl_lines="7 14 16-18"
89
+
--8<--"docs_src/media/tutorial005.py"
90
+
```
91
+
92
+
*`brand://guidelines` is an ordinary resource (**[Resources](resources.md)** covers those). The tool hands the same document to the model on request, and calling `guidelines()` directly keeps one source of truth.
93
+
*`EmbeddedResource` and `TextResourceContents` come from `mcp.types`. There is no helper as there is for images: the block you build goes into the result untouched, and there is no `structured_content`.
94
+
* Use the URI the resource is registered under, so a client can tell that the attachment and `brand://guidelines` are the same document. Any URI is legal, registered or not.
For binary content, use `BlobResourceContents(uri=..., mime_type=..., blob=...)` with the bytes base64-encoded into `blob`, in place of `TextResourceContents`. To send only a pointer the client can `resources/read` later, return a `ResourceLink(name=..., uri=...)` instead; it is a content block too.
101
+
84
102
## Icons
85
103
86
104
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 +128,7 @@ A tool's icons are on the `Tool` object from `tools/list`, a resource's on the `
110
128
111
129
* Return an `Image` or `Audio` from a tool and the client receives an `ImageContent` / `AudioContent` block: your bytes base64-encoded, with a MIME type.
112
130
* Build one from a `path=` and let the suffix decide the MIME type, or from in-memory `data=` plus an explicit `format=`.
131
+
* Return an `EmbeddedResource` to put a document (text or a base64 blob, with its URI and MIME type) in the result, or a `ResourceLink` to send just the pointer.
113
132
* Media results carry no `structured_content` and no output schema.
114
133
* An `Icon` is a pointer: a `src` URI plus optional `mime_type`, `sizes`, and `theme`.
115
134
*`icons=[...]` works on the server, on tools, on resources, and on prompts, and clients find them on the matching objects.
Copy file name to clipboardExpand all lines: docs/servers/prompts.md
+48-1Lines changed: 48 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -134,10 +134,55 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
134
134
```
135
135
136
136
!!! info
137
-
If you have read **[Tools](tools.md)**, you already know everything on this page. Same decorator, same
137
+
If you have read **[Tools](tools.md)**, you already know everything up to this point. Same decorator, same
138
138
docstring-as-description, same `Annotated`/`Field`. The only things that change are who
139
139
triggers it (the user) and where the result goes (into the conversation).
140
140
141
+
## More than text
142
+
143
+
`UserMessage` and `AssistantMessage` also accept a content block, or an `Image` / `Audio` helper, wherever they accept a `str`. Two cases come up in prompts: attaching a document and attaching a picture.
144
+
145
+
### Embedding a file
146
+
147
+
```python title="server.py" hl_lines="5 12 21 23"
148
+
--8<--"docs_src/prompts/tutorial004.py"
149
+
```
150
+
151
+
* The style guide is a resource at `style://python` (**[Resources](resources.md)** covers those), read from a `style-guide.md` next to `server.py`. Put any Markdown file there.
152
+
*`EmbeddedResource(resource=TextResourceContents(...))`, both from `mcp.types`, carries the file with its URI and MIME type as the first message; the request that refers to it follows as plain text.
153
+
* Embedding, rather than pasting the guide into the f-string, lets the client show it as an attachment and reopen `style://python` later, and the model receives the file verbatim. For a binary file use `BlobResourceContents` with a base64 `blob`.
154
+
155
+
Rendered, the first message's `content` is a `resource` block:
*`Image` is the helper from **[Images, audio & icons](media.md)**. `UserMessage` converts it to an `ImageContent` block (the file base64-encoded, MIME type guessed from `.png`) when the prompt renders; `Audio` becomes an `AudioContent` the same way.
168
+
* Put any PNG named `architecture.png` beside `server.py`. Prompt arguments are strings, so the picture always comes from the server; `component` only supplies the words.
Prompts can be added while clients are connected, for example to let a user save an instruction as a menu entry of their own. Register the prompt, then notify:
177
+
178
+
```python title="server.py" hl_lines="3 21-23"
179
+
--8<--"docs_src/prompts/tutorial006.py"
180
+
```
181
+
182
+
*`mcp.add_prompt(Prompt.from_function(fn, name=..., description=...))` registers a function exactly as `@mcp.prompt()` would, and `mcp.remove_prompt(name)` is the reverse. `prompts/list` reflects the change immediately.
183
+
*`await ctx.notify_prompts_changed()` sends `notifications/prompts/list_changed` to every `2026-07-28` client listening on a `subscriptions/listen` stream (**[Subscriptions](../handlers/subscriptions.md)**). `await ctx.session.send_prompt_list_changed()` sends it to the calling client when that client is pre-2026 (**[Serving legacy clients](../run/legacy-clients.md)**). Call both; each does nothing when there is nobody to tell.
184
+
* A client that receives the notification calls `prompts/list` again. In the Python `Client` that is `async with client.listen(prompts_list_changed=True) as sub:`, which yields a `PromptsListChanged` event.
185
+
141
186
## Recap
142
187
143
188
*`@mcp.prompt()` on a function makes it a prompt. Name from the function, description from the docstring.
@@ -146,5 +191,7 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
146
191
* Return a `str` and it becomes one user message. Return a list of `UserMessage` / `AssistantMessage` to seed a multi-turn conversation.
147
192
*`title=` and `Field(description=...)` are what a client puts in its UI.
148
193
* A missing required argument fails the whole request. There is no per-prompt error result.
194
+
* Wrap an `EmbeddedResource` or an `Image` in a `UserMessage` to attach a document or a picture.
195
+
* Add or remove prompts at runtime with `mcp.add_prompt(...)` / `mcp.remove_prompt(...)`, then `await ctx.notify_prompts_changed()` and `await ctx.session.send_prompt_list_changed()`.
149
196
150
197
Server-side autocomplete for a prompt's (or a resource template's) arguments is **[Completions](completions.md)**.
Copy file name to clipboardExpand all lines: docs/servers/tools.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,6 +34,8 @@ From those type hints the SDK generates a JSON Schema and sends it to the client
34
34
35
35
Both arguments are in `required` because neither has a default. You'll fix that in a moment. (The `title` keys are Pydantic artifacts; the properties, their types, and `required` are the contract.)
36
36
37
+
There is no `$schema` key either: MCP treats a schema without one as **JSON Schema 2020-12**, which is what Pydantic generates, so there is nothing to choose until you write schemas by hand on the **[low-level Server](../advanced/low-level-server.md#the-dialect-is-json-schema-2020-12)**.
38
+
37
39
!!! tip
38
40
Type hints aren't documentation here. They are **the contract**. If a client sends `"limit": "ten"`,
39
41
the SDK rejects it before your function ever runs.
0 commit comments