docs: cover the remaining Tier 1 audit items (#3325)

This commit is contained in:
Max 2026-08-17 20:18:30 +01:00 • committed by GitHub
commit 9057285683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 247 additions and 1 deletions

View file

@ -111,6 +111,17 @@ The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-
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)**.
## The dialect is JSON Schema 2020-12
`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:
```python title="server.py" hl_lines="8 14-15"
--8<-- "docs_src/lowlevel/tutorial007.py"
```
* 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.
* 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.
## `_meta`: for the application, not the model
`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.

View file

@ -50,6 +50,55 @@ MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SE
send. These two only work end-to-end on a `mode="legacy"` connection whose client
registered the matching callback.
## `ping` on a legacy session
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:
```python
async def main() -> None:
async with Client("http://localhost:8000/mcp", mode="legacy") as client:
await client.send_ping() # warns; returns an EmptyResult
```
And from the server, inside any handler:
```python
@mcp.tool()
async def check_client(ctx: Context) -> str:
"""A tool that still pings the client mid-call."""
await ctx.session.send_ping() # no warning; an EmptyResult while the client is connected
return "client answered"
```
* `client.send_ping()` warns with `MCPDeprecationWarning` on every call. On a default (`2026-07-28`) connection the server answers `MCPError: Method not found` instead.
* `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.
* Neither side registers anything to answer a ping.
## Roots change notifications
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:
```python
async def open_folder(client: Client, uri: str, name: str) -> None:
"""The user opened another folder: expose it through the roots callback, then tell the server."""
workspace.append(Root(uri=FileUrl(uri), name=name))
await client.send_roots_list_changed()
```
On the server, the low-level `Server` takes the receiving handler:
```python
async def roots_changed(ctx: ServerRequestContext, params: NotificationParams | None) -> None:
"""The client's roots changed: ask for the new list."""
roots = (await ctx.session.list_roots()).roots
server = Server("Bookshop", on_roots_list_changed=roots_changed)
```
* `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.
* `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.
## Silencing the warning
Don't, in new code.

View file

@ -81,6 +81,24 @@ A suffix it doesn't recognise falls back to `application/octet-stream`.
`Audio` from MP3 bytes that way and the client is told `mime_type="audio/wav"`, then
faithfully fails to decode it. When you pass `data=`, pass `format=`.
## Embedding a resource
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.
```python title="server.py" hl_lines="7 14 16-18"
--8<-- "docs_src/media/tutorial005.py"
```
* `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.
* `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`.
* 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.
```python
result.content # [EmbeddedResource(type="resource", resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text="# Brand guidelines\n\n..."))]
```
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.
## Icons
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 `
* Return an `Image` or `Audio` from a tool and the client receives an `ImageContent` / `AudioContent` block: your bytes base64-encoded, with a MIME type.
* Build one from a `path=` and let the suffix decide the MIME type, or from in-memory `data=` plus an explicit `format=`.
* 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.
* Media results carry no `structured_content` and no output schema.
* An `Icon` is a pointer: a `src` URI plus optional `mime_type`, `sizes`, and `theme`.
* `icons=[...]` works on the server, on tools, on resources, and on prompts, and clients find them on the matching objects.

View file

@ -134,10 +134,55 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
```
!!! info
If you have read **[Tools](tools.md)**, you already know everything on this page. Same decorator, same
If you have read **[Tools](tools.md)**, you already know everything up to this point. Same decorator, same
docstring-as-description, same `Annotated`/`Field`. The only things that change are who
triggers it (the user) and where the result goes (into the conversation).
## More than text
`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.
### Embedding a file
```python title="server.py" hl_lines="5 12 21 23"
--8<-- "docs_src/prompts/tutorial004.py"
```
* 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.
* `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.
* 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`.
Rendered, the first message's `content` is a `resource` block:
```json
{"type": "resource", "resource": {"uri": "style://python", "mimeType": "text/markdown", "text": "* Prefer early returns.\n..."}}
```
### Attaching an image
```python title="server.py" hl_lines="4 15"
--8<-- "docs_src/prompts/tutorial005.py"
```
* `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.
* 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.
```json
{"type": "image", "data": "iVBORw0KGgoAAAANSUhEUg...", "mimeType": "image/png"}
```
## Changing the list at runtime
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:
```python title="server.py" hl_lines="5 23-27"
--8<-- "docs_src/prompts/tutorial006.py"
```
* `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.
* `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.
* 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.
## Recap
* `@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
* Return a `str` and it becomes one user message. Return a list of `UserMessage` / `AssistantMessage` to seed a multi-turn conversation.
* `title=` and `Field(description=...)` are what a client puts in its UI.
* A missing required argument fails the whole request. There is no per-prompt error result.
* Wrap an `EmbeddedResource` or an `Image` in a `UserMessage` to attach a document or a picture.
* 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()`.
Server-side autocomplete for a prompt's (or a resource template's) arguments is **[Completions](completions.md)**.

View file

@ -34,6 +34,8 @@ From those type hints the SDK generates a JSON Schema and sends it to the client
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.)
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)**.
!!! tip
Type hints aren't documentation here. They are **the contract**. If a client sends `"limit": "ten"`,
the SDK rejects it before your function ever runs.

View file

@ -0,0 +1,30 @@
from mcp.server import Server, ServerRequestContext
from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool
FIND_BOOK = Tool(
name="find_book",
description="Find one book by ISBN, or by title and author.",
input_schema={
"type": "object",
"properties": {
"isbn": {"type": "string", "pattern": "^[0-9]{13}$"},
"title": {"type": "string"},
"author": {"type": "string"},
},
"oneOf": [{"required": ["isbn"]}, {"required": ["title", "author"]}],
"additionalProperties": False,
},
)
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(tools=[FIND_BOOK])
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
args = params.arguments or {}
found = f"ISBN {args['isbn']}" if "isbn" in args else f"{args['title']!r} by {args['author']}"
return CallToolResult(content=[TextContent(type="text", text=f"Found {found} on shelf C-3.")])
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)

View file

@ -0,0 +1,18 @@
from mcp.server import MCPServer
from mcp.types import EmbeddedResource, TextResourceContents
mcp = MCPServer("Brand kit")
@mcp.resource("brand://guidelines", mime_type="text/markdown")
def guidelines() -> str:
"""How to use the brand assets."""
return "# Brand guidelines\n\nUse the primary colour for calls to action.\n"
@mcp.tool()
def brand_guidelines() -> EmbeddedResource:
"""The brand guidelines as a Markdown document."""
return EmbeddedResource(
resource=TextResourceContents(uri="brand://guidelines", mime_type="text/markdown", text=guidelines())
)

View file

@ -0,0 +1,25 @@
from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver import Message, UserMessage
from mcp.types import EmbeddedResource, TextResourceContents
mcp = MCPServer("Code Helper")
STYLE_GUIDE_FILE = Path(__file__).parent / "style-guide.md" # or the path to your file on disk
@mcp.resource("style://python", mime_type="text/markdown")
def style_guide() -> str:
"""The team's Python style guide."""
return STYLE_GUIDE_FILE.read_text(encoding="utf-8")
@mcp.prompt()
def review_code(code: str) -> list[Message]:
"""Review a piece of code against the team style guide."""
guide = TextResourceContents(uri="style://python", mime_type="text/markdown", text=style_guide())
return [
UserMessage(EmbeddedResource(resource=guide)),
UserMessage(f"Review this code against the style guide above:\n\n{code}"),
]

View file

@ -0,0 +1,17 @@
from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver import Image, Message, UserMessage
mcp = MCPServer("Code Helper")
DIAGRAM_FILE = Path(__file__).parent / "architecture.png" # or the path to your file on disk
@mcp.prompt()
def explain_component(component: str) -> list[Message]:
"""Explain one component using the architecture diagram."""
return [
UserMessage(Image(path=DIAGRAM_FILE)),
UserMessage(f"Where does {component} sit in this architecture, and what does it talk to?"),
]

View file

@ -0,0 +1,28 @@
from contextlib import suppress
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts import Prompt
mcp = MCPServer("Code Helper")
@mcp.prompt()
def review_code(code: str) -> str:
"""Review a piece of code."""
return f"Please review this code:\n\n{code}"
@mcp.tool()
async def save_template(name: str, instruction: str, ctx: Context) -> str:
"""Save an instruction as a prompt the user can pick from the menu."""
def template(code: str) -> str:
return f"{instruction}\n\n{code}"
with suppress(ValueError): # replace an existing entry of the same name
mcp.remove_prompt(name)
mcp.add_prompt(Prompt.from_function(template, name=name, description=instruction))
await ctx.notify_prompts_changed()
await ctx.session.send_prompt_list_changed()
return f"Saved '{name}' to the prompt menu."