docs: comprehensive feature documentation for SEP-1730 Tier 1 (#2090)

Co-authored-by: Max Isbey <224885523+maxisbey@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-02-23 14:57:16 +00:00 • committed by GitHub
commit c86477c7b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 582 additions and 0 deletions

View file

@ -317,6 +317,117 @@ _Full example: [examples/snippets/clients/oauth_client.py](https://github.com/mo
For a complete working example, see [`examples/clients/simple-auth-client/`](../examples/clients/simple-auth-client/).
## Roots
### Listing Roots
Clients can provide a `list_roots_callback` so that servers can discover the client's workspace roots (directories, project folders, etc.):
```python
from typing import Any
from mcp import ClientSession, types
from mcp.shared.context import RequestContext
async def handle_list_roots(
context: RequestContext[ClientSession, Any],
) -> 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,
)
```
When a `list_roots_callback` is provided, the client automatically declares the `roots` capability (with `listChanged=True`) during initialization.
### Roots Change Notifications
When the client's workspace roots change (e.g., a folder is added or removed), notify the server:
```python
# After roots change, notify the server
await session.send_roots_list_changed()
```
## SSE Transport (Legacy)
For servers that use the older SSE transport, use `sse_client()` from `mcp.client.sse`:
```python
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())
```
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-03-26/basic/transports#streamable-http) for new servers.
## Ping
Send a ping to verify the server is responsive:
```python
# After session.initialize()
result = await session.send_ping()
# Returns EmptyResult on success; raises on timeout
```
## Logging
### Receiving Log Messages
Pass a `logging_callback` to receive log messages from the server:
```python
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,
)
```
### Setting the Server Log Level
Request that the server change its minimum log level:
```python
await session.set_logging_level("debug")
```
The `level` parameter is a `LoggingLevel` string: `"debug"`, `"info"`, `"notice"`, `"warning"`, `"error"`, `"critical"`, `"alert"`, or `"emergency"`.
## Parsing Tool Results
When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.

View file

@ -24,6 +24,116 @@ MCP servers declare capabilities during initialization:
| `logging` | - | Server logging configuration |
| `completions`| - | Argument completion suggestions |
## Ping
Both clients and servers can send ping requests to check that the other side is responsive:
```python
# From a client
result = await session.send_ping()
# From a server (via ServerSession)
result = await server_session.send_ping()
```
Both return an `EmptyResult` on success. If the remote side does not respond within the session timeout, an exception is raised.
## Cancellation
Either side can cancel a previously-issued request by sending a `CancelledNotification`:
```python
import mcp.types as types
# Send a cancellation notification
await session.send_notification(
types.ClientNotification(
types.CancelledNotification(
params=types.CancelledNotificationParams(
requestId="request-id-to-cancel",
reason="User navigated away",
)
)
)
)
```
The `CancelledNotificationParams` fields:
- `requestId` (optional): The ID of the request to cancel. Required for non-task cancellations.
- `reason` (optional): A human-readable string describing why the request was cancelled.
## Capability Negotiation
During initialization, the client and server exchange capability declarations. The Python SDK automatically declares capabilities based on which callbacks and handlers are registered:
**Client capabilities** (auto-declared when callbacks are provided):
- `sampling` -- declared when `sampling_callback` is passed to `ClientSession`
- `roots` -- declared when `list_roots_callback` is passed to `ClientSession`
- `elicitation` -- declared when `elicitation_callback` is passed to `ClientSession`
**Server capabilities** (auto-declared when handlers are registered):
- `prompts` -- declared when a `list_prompts` handler is registered
- `resources` -- declared when a `list_resources` handler is registered
- `tools` -- declared when a `list_tools` handler is registered
- `logging` -- declared when a `set_logging_level` handler is registered
- `completions` -- declared when a `completion` handler is registered
After initialization, clients can inspect server capabilities:
```python
capabilities = session.get_server_capabilities()
if capabilities and capabilities.tools:
tools = await session.list_tools()
```
## Protocol Version Negotiation
The SDK defines `LATEST_PROTOCOL_VERSION` and `SUPPORTED_PROTOCOL_VERSIONS` in `mcp.shared.version`:
```python
from mcp.shared.version import LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS
# LATEST_PROTOCOL_VERSION is the version the SDK advertises during initialization
# SUPPORTED_PROTOCOL_VERSIONS lists all versions the SDK can work with
```
During initialization, the client sends `LATEST_PROTOCOL_VERSION`. If the server responds with a version not in `SUPPORTED_PROTOCOL_VERSIONS`, the client raises a `RuntimeError`. This ensures both sides agree on a compatible protocol version before exchanging messages.
## JSON Schema (2020-12)
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()`:
```python
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",
# }
```
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
For pagination details, see:

View file

@ -101,6 +101,92 @@ def get_settings() -> str:
_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_
<!-- /snippet-source -->
#### Resource Templates and Template Reading
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:
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Template Example")
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""Read a specific user's profile. The user_id is extracted from the URI."""
return f'{{"user_id": "{user_id}", "name": "User {user_id}"}}'
```
Clients read a template resource by providing a concrete URI:
```python
# Client-side: read a template resource with a concrete URI
content = await session.read_resource("users://alice/profile")
```
Templates with multiple parameters work the same way:
```python
@mcp.resource("repos://{owner}/{repo}/readme")
def get_readme(owner: str, repo: str) -> str:
"""Each URI parameter becomes a function argument."""
return f"README for {owner}/{repo}"
```
#### Binary Resources
Resources can return binary data by returning `bytes` instead of `str`. Set the `mime_type` to indicate the content type:
```python
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()
```
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:
```python
import mcp.types as types
from mcp.server.lowlevel import Server
server = Server("Subscription Example")
subscriptions: dict[str, set[str]] = {} # uri -> set of session ids
@server.subscribe_resource()
async def handle_subscribe(uri) -> None:
"""Handle a client subscribing to a resource."""
subscriptions.setdefault(str(uri), set()).add("current_session")
@server.unsubscribe_resource()
async def handle_unsubscribe(uri) -> None:
"""Handle a client unsubscribing from a resource."""
if str(uri) in subscriptions:
subscriptions[str(uri)].discard("current_session")
```
When a subscribed resource changes, notify clients with `send_resource_updated()`:
```python
from pydantic import AnyUrl
# After modifying resource data:
await session.send_resource_updated(AnyUrl("resource://my-resource"))
```
### Tools
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:
@ -378,6 +464,85 @@ def debug_error(error: str) -> list[base.Message]:
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
<!-- /snippet-source -->
#### Prompts with Embedded Resources
Prompts can include embedded resources to provide file contents or data alongside the conversation messages:
```python
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",
),
),
),
]
```
#### Prompts with Image Content
Prompts can include images using `ImageContent` or the `Image` helper class:
```python
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(),
),
]
```
#### Prompt Change Notifications
When your server dynamically adds or removes prompts, notify connected clients:
```python
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"
```
### Icons
MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:
@ -439,6 +604,102 @@ def create_thumbnail(image_path: str) -> Image:
_Full example: [examples/snippets/servers/images.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/images.py)_
<!-- /snippet-source -->
### Audio
FastMCP provides an `Audio` class for returning audio data from tools, similar to `Image`:
```python
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")
```
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:
```python
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",
),
)
```
For binary embedded resources, use `BlobResourceContents` with base64-encoded data:
```python
import base64
from mcp.types import BlobResourceContents, EmbeddedResource
@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",
),
)
```
### Tool Change Notifications
When your server dynamically adds or removes tools at runtime, notify connected clients so they can refresh their tool list:
```python
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP("Dynamic Tools")
@mcp.tool()
async def register_plugin(name: str, ctx: Context[ServerSession, None]) -> str:
"""Dynamically register a new tool and notify the client."""
# ... register the plugin's tools ...
# Notify the client that the tool list has changed
await ctx.session.send_tool_list_changed()
return f"Plugin '{name}' registered"
```
### 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.
@ -714,6 +975,84 @@ The `elicit()` method returns an `ElicitationResult` with:
- `data`: The validated response (only when accepted)
- `validation_error`: Any validation error message
#### Elicitation with Enum Values
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:
```python
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"
```
#### URL Mode Elicitation
URL mode elicitation directs the user to an external URL for out-of-band interactions — for example, entering sensitive data that should not pass through the LLM, or filling out a complex form like a shipping address. The tool call blocks while waiting for the user to complete the interaction:
```python
import asyncio
import uuid
import anyio
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
mcp = FastMCP("Shipping Example")
# Simulates a database that the web form writes to on submit
completed_elicitations: dict[str, dict] = {}
@mcp.tool()
async def place_order(item: str, ctx: Context[ServerSession, None]) -> str:
"""Place an order (shipping details collected via secure form)."""
elicitation_id = str(uuid.uuid4())
# Direct the user to a form to enter their shipping address
result = await ctx.elicit_url(
message=f"Please enter your shipping details for '{item}'.",
url=f"https://example.com/shipping?id={elicitation_id}",
elicitation_id=elicitation_id,
)
if result.action != "accept":
return "Order cancelled."
# Poll until the shipping form is submitted
with anyio.fail_after(120):
while elicitation_id not in completed_elicitations:
await asyncio.sleep(1)
# Notify the client that the out-of-band interaction is done
await ctx.session.send_elicit_complete(elicitation_id)
shipping = completed_elicitations.pop(elicitation_id)
return f"Order placed! Shipping '{item}' to {shipping['address']}"
```
### Sampling
Tools can interact with LLMs through sampling (generating text):
@ -781,6 +1120,28 @@ async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
_Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/notifications.py)_
<!-- /snippet-source -->
#### Setting the Logging Level
Clients can request a minimum logging level via `logging/setLevel`. Use the low-level server API to handle this:
```python
import mcp.types as types
from mcp.server.lowlevel import Server
server = Server("Logging Level Example")
current_level: types.LoggingLevel = "warning"
@server.set_logging_level()
async def handle_set_level(level: types.LoggingLevel) -> None:
"""Handle client request to change the logging level."""
global current_level
current_level = level
```
When this handler is registered, the server automatically declares the `logging` capability during initialization.
### Authentication
Authentication can be used by servers that want to expose tools accessing protected resources.