docs: add snippet verification for docs/ pages (#2115)

This commit is contained in:
Felix Weinberger 2026-02-23 15:38:15 +00:00 • committed by GitHub
commit 1ef124e4a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 446 additions and 43 deletions

View file

@ -61,7 +61,7 @@ jobs:
uv run --frozen --no-sync coverage combine
uv run --frozen --no-sync coverage report
readme-snippets:
doc-snippets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@ -74,5 +74,5 @@ jobs:
- name: Install dependencies
run: uv sync --frozen --all-extras --python 3.10
- name: Check README snippets are up to date
run: uv run --frozen scripts/update_readme_snippets.py --check
- name: Check doc snippets are up to date
run: uv run --frozen scripts/update_doc_snippets.py --check

View file

@ -55,9 +55,9 @@ repos:
language: system
files: ^(pyproject\.toml|uv\.lock)$
pass_filenames: false
- id: readme-snippets
name: Check README snippets are up to date
entry: uv run --frozen python scripts/update_readme_snippets.py --check
- id: doc-snippets
name: Check doc snippets are up to date
entry: uv run --frozen python scripts/update_doc_snippets.py --check
language: system
files: ^(README\.md|examples/.*\.py|scripts/update_readme_snippets\.py)$
files: ^(README\.md|docs/.*\.md|examples/.*\.py|scripts/update_doc_snippets\.py)$
pass_filenames: false

View file

@ -131,7 +131,7 @@ if __name__ == "__main__":
mcp.run(transport="streamable-http")
```
_Full example: [examples/snippets/servers/fastmcp_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/fastmcp_quickstart.py)_
_Full example: [examples/snippets/servers/fastmcp_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/fastmcp_quickstart.py)_
<!-- /snippet-source -->
You can install this server in [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp) and interact with it right away. First, run the server:

View file

@ -225,6 +225,7 @@ For OAuth 2.1 client authentication, see [Authorization](authorization.md#client
Clients can provide a `list_roots_callback` so that servers can discover the client's workspace roots (directories, project folders, etc.):
<!-- snippet-source examples/snippets/clients/roots_example.py -->
```python
from mcp import ClientSession, types
from mcp.shared.context import RequestContext
@ -250,6 +251,9 @@ session = ClientSession(
)
```
_Full example: [examples/snippets/clients/roots_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/roots_example.py)_
<!-- /snippet-source -->
When a `list_roots_callback` is provided, the client automatically declares the `roots` capability (with `listChanged=True`) during initialization.
### Roots Change Notifications
@ -265,6 +269,7 @@ await session.send_roots_list_changed()
For servers that use the older SSE transport, use `sse_client()` from `mcp.client.sse`:
<!-- snippet-source examples/snippets/clients/sse_client.py -->
```python
import asyncio
@ -284,6 +289,9 @@ async def main():
asyncio.run(main())
```
_Full example: [examples/snippets/clients/sse_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/sse_client.py)_
<!-- /snippet-source -->
The `sse_client()` function accepts optional `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters. The SSE transport is considered legacy; prefer [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) for new servers.
## Ping
@ -302,6 +310,7 @@ result = await session.send_ping()
Pass a `logging_callback` to receive log messages from the server:
<!-- snippet-source examples/snippets/clients/logging_client.py -->
```python
from mcp import ClientSession, types
@ -318,6 +327,9 @@ session = ClientSession(
)
```
_Full example: [examples/snippets/clients/logging_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/logging_client.py)_
<!-- /snippet-source -->
### Setting the Server Log Level
Request that the server change its minimum log level:

View file

@ -42,22 +42,29 @@ Both return an `EmptyResult` on success. If the remote side does not respond wit
Either side can cancel a previously-issued request by sending a `CancelledNotification`:
<!-- snippet-source examples/snippets/clients/cancellation.py -->
```python
import mcp.types as types
from mcp import ClientSession
# Send a cancellation notification
await session.send_notification(
types.ClientNotification(
types.CancelledNotification(
params=types.CancelledNotificationParams(
requestId="request-id-to-cancel",
reason="User navigated away",
async def cancel_request(session: ClientSession) -> None:
"""Send a cancellation notification for a previously-issued request."""
await session.send_notification(
types.ClientNotification(
types.CancelledNotification(
params=types.CancelledNotificationParams(
requestId="request-id-to-cancel",
reason="User navigated away",
)
)
)
)
)
```
_Full example: [examples/snippets/clients/cancellation.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/clients/cancellation.py)_
<!-- /snippet-source -->
The `CancelledNotificationParams` fields:
- `requestId` (optional): The ID of the request to cancel. Required for non-task cancellations.
@ -106,6 +113,7 @@ During initialization, the client sends `LATEST_PROTOCOL_VERSION`. If the server
MCP uses [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) for tool input schemas, output schemas, and elicitation schemas. When using Pydantic models, schemas are generated automatically via `model_json_schema()`:
<!-- snippet-source examples/snippets/servers/json_schema_example.py -->
```python
from pydantic import BaseModel, Field
@ -132,6 +140,9 @@ schema = SearchParams.model_json_schema()
# }
```
_Full example: [examples/snippets/servers/json_schema_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/json_schema_example.py)_
<!-- /snippet-source -->
For FastMCP tools, input schemas are derived automatically from function signatures. For structured output, the output schema is derived from the return type annotation.
## Pagination

View file

@ -105,6 +105,7 @@ _Full example: [examples/snippets/servers/basic_resource.py](https://github.com/
Resources with URI parameters (e.g., `{name}`) are registered as templates. When a client reads a templated resource, the URI parameters are extracted and passed to the function:
<!-- snippet-source examples/snippets/servers/resource_templates.py -->
```python
from mcp.server.fastmcp import FastMCP
@ -117,6 +118,9 @@ def get_user_profile(user_id: str) -> str:
return f'{{"user_id": "{user_id}", "name": "User {user_id}"}}'
```
_Full example: [examples/snippets/servers/resource_templates.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/resource_templates.py)_
<!-- /snippet-source -->
Clients read a template resource by providing a concrete URI:
```python
@ -137,6 +141,7 @@ def get_readme(owner: str, repo: str) -> str:
Resources can return binary data by returning `bytes` instead of `str`. Set the `mime_type` to indicate the content type:
<!-- snippet-source examples/snippets/servers/binary_resources.py -->
```python
from mcp.server.fastmcp import FastMCP
@ -150,14 +155,17 @@ def get_logo() -> bytes:
return f.read()
```
_Full example: [examples/snippets/servers/binary_resources.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/binary_resources.py)_
<!-- /snippet-source -->
Binary content is automatically base64-encoded and returned as `BlobResourceContents` in the MCP response.
#### Resource Subscriptions
Clients can subscribe to resource updates. Use the low-level server API to handle subscription and unsubscription requests:
<!-- snippet-source examples/snippets/servers/resource_subscriptions.py -->
```python
import mcp.types as types
from mcp.server.lowlevel import Server
server = Server("Subscription Example")
@ -178,6 +186,9 @@ async def handle_unsubscribe(uri) -> None:
subscriptions[str(uri)].discard("current_session")
```
_Full example: [examples/snippets/servers/resource_subscriptions.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/resource_subscriptions.py)_
<!-- /snippet-source -->
When a subscribed resource changes, notify clients with `send_resource_updated()`:
```python
@ -468,6 +479,7 @@ _Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/mo
Prompts can include embedded resources to provide file contents or data alongside the conversation messages:
<!-- snippet-source examples/snippets/servers/prompt_embedded_resources.py -->
```python
import mcp.types as types
from mcp.server.fastmcp import FastMCP
@ -497,10 +509,14 @@ def review_file(filename: str) -> list[base.Message]:
]
```
_Full example: [examples/snippets/servers/prompt_embedded_resources.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/prompt_embedded_resources.py)_
<!-- /snippet-source -->
#### Prompts with Image Content
Prompts can include images using `ImageContent` or the `Image` helper class:
<!-- snippet-source examples/snippets/servers/prompt_image_content.py -->
```python
import mcp.types as types
from mcp.server.fastmcp import FastMCP
@ -524,10 +540,14 @@ def describe_image(image_path: str) -> list[base.Message]:
]
```
_Full example: [examples/snippets/servers/prompt_image_content.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/prompt_image_content.py)_
<!-- /snippet-source -->
#### Prompt Change Notifications
When your server dynamically adds or removes prompts, notify connected clients:
<!-- snippet-source examples/snippets/servers/prompt_change_notifications.py -->
```python
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
@ -543,6 +563,9 @@ async def update_prompts(ctx: Context[ServerSession, None]) -> str:
return "Prompts updated"
```
_Full example: [examples/snippets/servers/prompt_change_notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/prompt_change_notifications.py)_
<!-- /snippet-source -->
### Icons
MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:
@ -608,6 +631,7 @@ _Full example: [examples/snippets/servers/images.py](https://github.com/modelcon
FastMCP provides an `Audio` class for returning audio data from tools, similar to `Image`:
<!-- snippet-source examples/snippets/servers/audio_example.py -->
```python
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.utilities.types import Audio
@ -627,12 +651,16 @@ def get_audio_from_bytes(raw_audio: bytes) -> Audio:
return Audio(data=raw_audio, format="wav")
```
_Full example: [examples/snippets/servers/audio_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/audio_example.py)_
<!-- /snippet-source -->
The `Audio` class accepts `path` or `data` (mutually exclusive) and an optional `format` string. Supported formats include `wav`, `mp3`, `ogg`, `flac`, `aac`, and `m4a`. When using a file path, the MIME type is inferred from the file extension.
### Embedded Resource Results
Tools can return `EmbeddedResource` to attach file contents or data inline in the result:
<!-- snippet-source examples/snippets/servers/embedded_resource_results.py -->
```python
from mcp.server.fastmcp import FastMCP
from mcp.types import EmbeddedResource, TextResourceContents
@ -655,13 +683,20 @@ def read_config(path: str) -> EmbeddedResource:
)
```
_Full example: [examples/snippets/servers/embedded_resource_results.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/embedded_resource_results.py)_
<!-- /snippet-source -->
For binary embedded resources, use `BlobResourceContents` with base64-encoded data:
<!-- snippet-source examples/snippets/servers/embedded_resource_results_binary.py -->
```python
import base64
from mcp.server.fastmcp import FastMCP
from mcp.types import BlobResourceContents, EmbeddedResource
mcp = FastMCP("Binary Embedded Resource Example")
@mcp.tool()
def read_binary_file(path: str) -> EmbeddedResource:
@ -678,10 +713,14 @@ def read_binary_file(path: str) -> EmbeddedResource:
)
```
_Full example: [examples/snippets/servers/embedded_resource_results_binary.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/embedded_resource_results_binary.py)_
<!-- /snippet-source -->
### Tool Change Notifications
When your server dynamically adds or removes tools at runtime, notify connected clients so they can refresh their tool list:
<!-- snippet-source examples/snippets/servers/tool_change_notifications.py -->
```python
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
@ -700,6 +739,9 @@ async def register_plugin(name: str, ctx: Context[ServerSession, None]) -> str:
return f"Plugin '{name}' registered"
```
_Full example: [examples/snippets/servers/tool_change_notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/tool_change_notifications.py)_
<!-- /snippet-source -->
### Context
The Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata.
@ -978,6 +1020,7 @@ The `elicit()` method returns an `ElicitationResult` with:
To present a dropdown or selection list in elicitation forms, use `json_schema_extra` with an `enum` key on a `str` field. Do not use `Literal` -- use a plain `str` field with the enum constraint in the JSON schema:
<!-- snippet-source examples/snippets/servers/elicitation_enum.py -->
```python
from pydantic import BaseModel, Field
@ -1006,10 +1049,14 @@ async def pick_color(ctx: Context[ServerSession, None]) -> str:
return "No color selected"
```
_Full example: [examples/snippets/servers/elicitation_enum.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/elicitation_enum.py)_
<!-- /snippet-source -->
#### Elicitation Complete Notification
For URL mode elicitations, send a completion notification after the out-of-band interaction finishes. This tells the client that the elicitation is done and it may retry any blocked requests:
<!-- snippet-source examples/snippets/servers/elicitation_complete.py -->
```python
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
@ -1018,9 +1065,7 @@ mcp = FastMCP("Elicit Complete Example")
@mcp.tool()
async def handle_oauth_callback(
elicitation_id: str, ctx: Context[ServerSession, None]
) -> str:
async def handle_oauth_callback(elicitation_id: str, ctx: Context[ServerSession, None]) -> str:
"""Called when OAuth flow completes out-of-band."""
# ... process the callback ...
@ -1030,6 +1075,9 @@ async def handle_oauth_callback(
return "Authorization complete"
```
_Full example: [examples/snippets/servers/elicitation_complete.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/elicitation_complete.py)_
<!-- /snippet-source -->
### Sampling
Tools can interact with LLMs through sampling (generating text):
@ -1101,6 +1149,7 @@ _Full example: [examples/snippets/servers/notifications.py](https://github.com/m
Clients can request a minimum logging level via `logging/setLevel`. Use the low-level server API to handle this:
<!-- snippet-source examples/snippets/servers/set_logging_level.py -->
```python
import mcp.types as types
from mcp.server.lowlevel import Server
@ -1117,6 +1166,9 @@ async def handle_set_level(level: types.LoggingLevel) -> None:
current_level = level
```
_Full example: [examples/snippets/servers/set_logging_level.py](https://github.com/modelcontextprotocol/python-sdk/blob/v1.x/examples/snippets/servers/set_logging_level.py)_
<!-- /snippet-source -->
When this handler is registered, the server automatically declares the `logging` capability during initialization.
### Authentication

View file

@ -0,0 +1,16 @@
import mcp.types as types
from mcp import ClientSession
async def cancel_request(session: ClientSession) -> None:
"""Send a cancellation notification for a previously-issued request."""
await session.send_notification(
types.ClientNotification(
types.CancelledNotification(
params=types.CancelledNotificationParams(
requestId="request-id-to-cancel",
reason="User navigated away",
)
)
)
)

View file

@ -0,0 +1,13 @@
from mcp import ClientSession, types
async def handle_log(params: types.LoggingMessageNotificationParams) -> None:
"""Handle log messages from the server."""
print(f"[{params.level}] {params.data}")
session = ClientSession(
read_stream,
write_stream,
logging_callback=handle_log,
)

View file

@ -0,0 +1,22 @@
from mcp import ClientSession, types
from mcp.shared.context import RequestContext
async def handle_list_roots(
context: RequestContext[ClientSession, None],
) -> types.ListRootsResult:
"""Return the client's workspace roots."""
return types.ListRootsResult(
roots=[
types.Root(uri="file:///home/user/project", name="My Project"),
types.Root(uri="file:///home/user/data", name="Data Folder"),
]
)
# Pass the callback when creating the session
session = ClientSession(
read_stream,
write_stream,
list_roots_callback=handle_list_roots,
)

View file

@ -0,0 +1,16 @@
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client
async def main():
async with sse_client("http://localhost:8000/sse") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
tools = await session.list_tools()
print(f"Available tools: {[t.name for t in tools.tools]}")
asyncio.run(main())

View file

@ -0,0 +1,16 @@
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.utilities.types import Audio
mcp = FastMCP("Audio Example")
@mcp.tool()
def get_audio_from_file(file_path: str) -> Audio:
"""Return audio from a file path (format auto-detected from extension)."""
return Audio(path=file_path)
@mcp.tool()
def get_audio_from_bytes(raw_audio: bytes) -> Audio:
"""Return audio from raw bytes with explicit format."""
return Audio(data=raw_audio, format="wav")

View file

@ -0,0 +1,10 @@
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Binary Resource Example")
@mcp.resource("images://logo.png", mime_type="image/png")
def get_logo() -> bytes:
"""Return a binary image resource."""
with open("logo.png", "rb") as f:
return f.read()

View file

@ -0,0 +1,15 @@
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP("Elicit Complete Example")
@mcp.tool()
async def handle_oauth_callback(elicitation_id: str, ctx: Context[ServerSession, None]) -> str:
"""Called when OAuth flow completes out-of-band."""
# ... process the callback ...
# Notify the client that the elicitation is done
await ctx.session.send_elicit_complete(elicitation_id)
return "Authorization complete"

View file

@ -0,0 +1,25 @@
from pydantic import BaseModel, Field
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP("Enum Elicitation Example")
class ColorPreference(BaseModel):
color: str = Field(
description="Pick your favorite color",
json_schema_extra={"enum": ["red", "green", "blue", "yellow"]},
)
@mcp.tool()
async def pick_color(ctx: Context[ServerSession, None]) -> str:
"""Ask the user to pick a color from a list."""
result = await ctx.elicit(
message="Choose a color:",
schema=ColorPreference,
)
if result.action == "accept":
return f"You picked: {result.data.color}"
return "No color selected"

View file

@ -0,0 +1,19 @@
from mcp.server.fastmcp import FastMCP
from mcp.types import EmbeddedResource, TextResourceContents
mcp = FastMCP("Embedded Resource Example")
@mcp.tool()
def read_config(path: str) -> EmbeddedResource:
"""Read a config file and return it as an embedded resource."""
with open(path) as f:
content = f.read()
return EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=f"file://{path}",
text=content,
mimeType="application/json",
),
)

View file

@ -0,0 +1,21 @@
import base64
from mcp.server.fastmcp import FastMCP
from mcp.types import BlobResourceContents, EmbeddedResource
mcp = FastMCP("Binary Embedded Resource Example")
@mcp.tool()
def read_binary_file(path: str) -> EmbeddedResource:
"""Read a binary file and return it as an embedded resource."""
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode()
return EmbeddedResource(
type="resource",
resource=BlobResourceContents(
uri=f"file://{path}",
blob=data,
mimeType="application/octet-stream",
),
)

View file

@ -0,0 +1,23 @@
from pydantic import BaseModel, Field
class SearchParams(BaseModel):
query: str = Field(description="Search query string")
max_results: int = Field(default=10, description="Maximum results to return")
# Pydantic generates a JSON Schema 2020-12 compatible schema:
schema = SearchParams.model_json_schema()
# {
# "properties": {
# "query": {"description": "Search query string", "type": "string"},
# "max_results": {
# "default": 10,
# "description": "Maximum results to return",
# "type": "integer",
# },
# },
# "required": ["query"],
# "title": "SearchParams",
# "type": "object",
# }

View file

@ -0,0 +1,12 @@
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP("Dynamic Prompts")
@mcp.tool()
async def update_prompts(ctx: Context[ServerSession, None]) -> str:
"""Update available prompts and notify clients."""
# ... modify prompts ...
await ctx.session.send_prompt_list_changed()
return "Prompts updated"

View file

@ -0,0 +1,26 @@
import mcp.types as types
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
mcp = FastMCP("Embedded Resource Prompt Example")
@mcp.prompt()
def review_file(filename: str) -> list[base.Message]:
"""Review a file with its contents embedded."""
file_content = open(filename).read()
return [
base.UserMessage(
content=types.TextContent(type="text", text=f"Please review {filename}:"),
),
base.UserMessage(
content=types.EmbeddedResource(
type="resource",
resource=types.TextResourceContents(
uri=f"file://{filename}",
text=file_content,
mimeType="text/plain",
),
),
),
]

View file

@ -0,0 +1,20 @@
import mcp.types as types
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
from mcp.server.fastmcp.utilities.types import Image
mcp = FastMCP("Image Prompt Example")
@mcp.prompt()
def describe_image(image_path: str) -> list[base.Message]:
"""Prompt that includes an image for analysis."""
img = Image(path=image_path)
return [
base.UserMessage(
content=types.TextContent(type="text", text="Describe this image:"),
),
base.UserMessage(
content=img.to_image_content(),
),
]

Some files were not shown because too many files have changed in this diff Show more