Skip to content

Commit 9057285

Browse files
authored
docs: cover the remaining Tier 1 audit items (#3325)
1 parent 2a1cc94 commit 9057285

10 files changed

Lines changed: 247 additions & 1 deletion

File tree

docs/advanced/low-level-server.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-
111111

112112
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)**.
113113

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+
114125
## `_meta`: for the application, not the model
115126

116127
`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.

docs/deprecated.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,55 @@ MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SE
5050
send. These two only work end-to-end on a `mode="legacy"` connection whose client
5151
registered the matching callback.
5252

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. From the client:
56+
57+
```python
58+
async def main() -> None:
59+
async with Client("http://localhost:8000/mcp", mode="legacy") as client:
60+
await client.send_ping() # warns; returns an EmptyResult
61+
```
62+
63+
And from the server, inside any handler:
64+
65+
```python
66+
@mcp.tool()
67+
async def check_client(ctx: Context) -> str:
68+
"""A tool that still pings the client mid-call."""
69+
await ctx.session.send_ping() # no warning; an EmptyResult while the client is connected
70+
return "client answered"
71+
```
72+
73+
* `client.send_ping()` warns with `MCPDeprecationWarning` on every call. On a default (`2026-07-28`) connection the server answers `MCPError: Method not found` instead.
74+
* `ctx.session.send_ping()` carries no warning. On a modern connection it raises the same no-back-channel error as any other server-initiated request.
75+
* Neither side registers anything to answer a ping.
76+
77+
## Roots change notifications
78+
79+
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. On the client, passing `list_roots_callback=` (**[Client callbacks](client/callbacks.md)**) is what declares `"roots": {"listChanged": true}`, and one call keeps that promise:
80+
81+
```python
82+
async def open_folder(client: Client, uri: str, name: str) -> None:
83+
"""The user opened another folder: expose it through the roots callback, then tell the server."""
84+
workspace.append(Root(uri=FileUrl(uri), name=name))
85+
await client.send_roots_list_changed()
86+
```
87+
88+
On the server, the low-level `Server` takes the receiving handler:
89+
90+
```python
91+
async def roots_changed(ctx: ServerRequestContext, params: NotificationParams | None) -> None:
92+
"""The client's roots changed: ask for the new list."""
93+
roots = (await ctx.session.list_roots()).roots
94+
95+
96+
server = Server("Bookshop", on_roots_list_changed=roots_changed)
97+
```
98+
99+
* `workspace` is the list your `list_roots_callback` returns. `client.send_roots_list_changed()` warns, and it needs a `mode="legacy"` client: on a modern connection the notification is silently dropped. Keep the session open afterwards, because the server's follow-up `roots/list` arrives on it.
100+
* `MCPServer` has no hook for the notification. On the low-level `Server`, `on_roots_list_changed=` registers the handler (deprecated too, and it warns at construction). The notification carries no payload, so the handler calls `ctx.session.list_roots()` for the new list.
101+
53102
## Silencing the warning
54103

55104
Don't, in new code.

docs/servers/media.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ A suffix it doesn't recognise falls back to `application/octet-stream`.
8181
`Audio` from MP3 bytes that way and the client is told `mime_type="audio/wav"`, then
8282
faithfully fails to decode it. When you pass `data=`, pass `format=`.
8383

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.
95+
96+
```python
97+
result.content # [EmbeddedResource(type="resource", resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text="# Brand guidelines\n\n..."))]
98+
```
99+
100+
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+
84102
## Icons
85103

86104
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 `
110128

111129
* Return an `Image` or `Audio` from a tool and the client receives an `ImageContent` / `AudioContent` block: your bytes base64-encoded, with a MIME type.
112130
* 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.
113132
* Media results carry no `structured_content` and no output schema.
114133
* An `Icon` is a pointer: a `src` URI plus optional `mime_type`, `sizes`, and `theme`.
115134
* `icons=[...]` works on the server, on tools, on resources, and on prompts, and clients find them on the matching objects.

docs/servers/prompts.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,55 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
134134
```
135135

136136
!!! 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
138138
docstring-as-description, same `Annotated`/`Field`. The only things that change are who
139139
triggers it (the user) and where the result goes (into the conversation).
140140

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:
156+
157+
```json
158+
{"type": "resource", "resource": {"uri": "style://python", "mimeType": "text/markdown", "text": "* Prefer early returns.\n..."}}
159+
```
160+
161+
### Attaching an image
162+
163+
```python title="server.py" hl_lines="4 15"
164+
--8<-- "docs_src/prompts/tutorial005.py"
165+
```
166+
167+
* `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.
169+
170+
```json
171+
{"type": "image", "data": "iVBORw0KGgoAAAANSUhEUg...", "mimeType": "image/png"}
172+
```
173+
174+
## Changing the list at runtime
175+
176+
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="5 23-27"
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. `add_prompt` keeps an existing entry of the same name rather than overwrite it, so the tool removes any old one first to make saving a replace. `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+
141186
## Recap
142187

143188
* `@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
146191
* Return a `str` and it becomes one user message. Return a list of `UserMessage` / `AssistantMessage` to seed a multi-turn conversation.
147192
* `title=` and `Field(description=...)` are what a client puts in its UI.
148193
* 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()`.
149196

150197
Server-side autocomplete for a prompt's (or a resource template's) arguments is **[Completions](completions.md)**.

docs/servers/tools.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ From those type hints the SDK generates a JSON Schema and sends it to the client
3434

3535
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.)
3636

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+
3739
!!! tip
3840
Type hints aren't documentation here. They are **the contract**. If a client sends `"limit": "ten"`,
3941
the SDK rejects it before your function ever runs.

docs_src/lowlevel/tutorial007.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from mcp.server import Server, ServerRequestContext
2+
from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool
3+
4+
FIND_BOOK = Tool(
5+
name="find_book",
6+
description="Find one book by ISBN, or by title and author.",
7+
input_schema={
8+
"type": "object",
9+
"properties": {
10+
"isbn": {"type": "string", "pattern": "^[0-9]{13}$"},
11+
"title": {"type": "string"},
12+
"author": {"type": "string"},
13+
},
14+
"oneOf": [{"required": ["isbn"]}, {"required": ["title", "author"]}],
15+
"additionalProperties": False,
16+
},
17+
)
18+
19+
20+
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
21+
return ListToolsResult(tools=[FIND_BOOK])
22+
23+
24+
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
25+
args = params.arguments or {}
26+
found = f"ISBN {args['isbn']}" if "isbn" in args else f"{args['title']!r} by {args['author']}"
27+
return CallToolResult(content=[TextContent(type="text", text=f"Found {found} on shelf C-3.")])
28+
29+
30+
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)

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/prompts/tutorial004.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pathlib import Path
2+
3+
from mcp.server import MCPServer
4+
from mcp.server.mcpserver import Message, UserMessage
5+
from mcp.types import EmbeddedResource, TextResourceContents
6+
7+
mcp = MCPServer("Code Helper")
8+
9+
STYLE_GUIDE_FILE = Path(__file__).parent / "style-guide.md" # or the path to your file on disk
10+
11+
12+
@mcp.resource("style://python", mime_type="text/markdown")
13+
def style_guide() -> str:
14+
"""The team's Python style guide."""
15+
return STYLE_GUIDE_FILE.read_text(encoding="utf-8")
16+
17+
18+
@mcp.prompt()
19+
def review_code(code: str) -> list[Message]:
20+
"""Review a piece of code against the team style guide."""
21+
guide = TextResourceContents(uri="style://python", mime_type="text/markdown", text=style_guide())
22+
return [
23+
UserMessage(EmbeddedResource(resource=guide)),
24+
UserMessage(f"Review this code against the style guide above:\n\n{code}"),
25+
]

docs_src/prompts/tutorial005.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pathlib import Path
2+
3+
from mcp.server import MCPServer
4+
from mcp.server.mcpserver import Image, Message, UserMessage
5+
6+
mcp = MCPServer("Code Helper")
7+
8+
DIAGRAM_FILE = Path(__file__).parent / "architecture.png" # or the path to your file on disk
9+
10+
11+
@mcp.prompt()
12+
def explain_component(component: str) -> list[Message]:
13+
"""Explain one component using the architecture diagram."""
14+
return [
15+
UserMessage(Image(path=DIAGRAM_FILE)),
16+
UserMessage(f"Where does {component} sit in this architecture, and what does it talk to?"),
17+
]

docs_src/prompts/tutorial006.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from contextlib import suppress
2+
3+
from mcp.server import MCPServer
4+
from mcp.server.mcpserver import Context
5+
from mcp.server.mcpserver.prompts import Prompt
6+
7+
mcp = MCPServer("Code Helper")
8+
9+
10+
@mcp.prompt()
11+
def review_code(code: str) -> str:
12+
"""Review a piece of code."""
13+
return f"Please review this code:\n\n{code}"
14+
15+
16+
@mcp.tool()
17+
async def save_template(name: str, instruction: str, ctx: Context) -> str:
18+
"""Save an instruction as a prompt the user can pick from the menu."""
19+
20+
def template(code: str) -> str:
21+
return f"{instruction}\n\n{code}"
22+
23+
with suppress(ValueError): # replace an existing entry of the same name
24+
mcp.remove_prompt(name)
25+
mcp.add_prompt(Prompt.from_function(template, name=name, description=instruction))
26+
await ctx.notify_prompts_changed()
27+
await ctx.session.send_prompt_list_changed()
28+
return f"Saved '{name}' to the prompt menu."

0 commit comments

Comments
 (0)