Skip to content

Commit 745a446

Browse files
docs: add documentation for 19 missing features (SEP-1730 Tier 1)
Add documentation with examples for all previously undocumented features: Server (docs/server.md - 296 lines added): - Tools: audio results, change notifications - Resources: binary reading, subscribing, unsubscribing - Prompts: embedded resources, image content, change notifications - Logging: setting level - Elicitation: enum values, complete notification Client (docs/client.md - 137 lines added): - Roots: listing, change notifications - SSE transport (legacy client) - Ping, logging Protocol (docs/protocol.md - 161 lines added): - Ping, cancellation, capability negotiation - Protocol version negotiation, JSON Schema 2020-12 All code snippets verified against SDK source.
1 parent 0654bf7 commit 745a446

3 files changed

Lines changed: 594 additions & 0 deletions

File tree

docs/client.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,143 @@ The `get_display_name()` function implements the proper precedence rules for dis
215215

216216
This ensures your client UI shows the most user-friendly names that servers provide.
217217

218+
## SSE Transport (Legacy)
219+
220+
For servers that only support the older SSE transport, use the `sse_client()` context manager from `mcp.client.sse`:
221+
222+
```python
223+
import asyncio
224+
225+
from mcp import ClientSession
226+
from mcp.client.sse import sse_client
227+
228+
229+
async def main():
230+
# Connect to an SSE server
231+
async with sse_client(
232+
url="http://localhost:8000/sse",
233+
headers={"Authorization": "Bearer token"},
234+
timeout=5,
235+
sse_read_timeout=300, # 5 minutes
236+
) as (read_stream, write_stream):
237+
async with ClientSession(read_stream, write_stream) as session:
238+
await session.initialize()
239+
tools = await session.list_tools()
240+
print(f"Available tools: {[t.name for t in tools.tools]}")
241+
242+
243+
if __name__ == "__main__":
244+
asyncio.run(main())
245+
```
246+
247+
The `sse_client()` accepts:
248+
249+
- `url` - The SSE endpoint URL
250+
- `headers` - Optional HTTP headers
251+
- `timeout` - HTTP timeout for regular operations (default: 5 seconds)
252+
- `sse_read_timeout` - Timeout for SSE read operations (default: 5 minutes)
253+
- `auth` - Optional `httpx.Auth` handler
254+
- `httpx_client_factory` - Optional factory for customizing the HTTP client
255+
256+
Note: SSE transport is legacy. Prefer [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) for new implementations.
257+
258+
## Roots
259+
260+
### Listing Roots
261+
262+
To allow the server to query the client's root URIs, provide a `list_roots_callback` when creating the `ClientSession`. The callback receives a `RequestContext[ClientSession, Any]` and returns a `ListRootsResult`:
263+
264+
```python
265+
import asyncio
266+
267+
from mcp import ClientSession, StdioServerParameters, types
268+
from mcp.client.stdio import stdio_client
269+
from mcp.shared.context import RequestContext
270+
271+
272+
async def handle_list_roots(
273+
context: RequestContext[ClientSession, None],
274+
) -> types.ListRootsResult:
275+
"""Return the client's root URIs."""
276+
return types.ListRootsResult(
277+
roots=[
278+
types.Root(uri="file:///home/user/project", name="My Project"),
279+
types.Root(uri="file:///home/user/data", name="Data Directory"),
280+
]
281+
)
282+
283+
284+
async def main():
285+
server_params = StdioServerParameters(command="my-server")
286+
async with stdio_client(server_params) as (read, write):
287+
async with ClientSession(
288+
read,
289+
write,
290+
list_roots_callback=handle_list_roots,
291+
) as session:
292+
await session.initialize()
293+
# The server can now call roots/list and get our roots
294+
295+
296+
if __name__ == "__main__":
297+
asyncio.run(main())
298+
```
299+
300+
Providing a `list_roots_callback` automatically declares the `roots` capability (with `listChanged=True`) during initialization.
301+
302+
### Roots Change Notifications
303+
304+
When the client's roots change, notify the server so it can re-query:
305+
306+
```python
307+
# After roots change (e.g., user opens a new project):
308+
await session.send_roots_list_changed()
309+
```
310+
311+
## Ping
312+
313+
Send a ping to check that the server is responsive:
314+
315+
```python
316+
result = await session.send_ping()
317+
# Returns EmptyResult if the server is alive
318+
```
319+
320+
## Logging
321+
322+
### Receiving Log Messages
323+
324+
To handle log messages sent by the server, provide a `logging_callback` when creating the `ClientSession`:
325+
326+
```python
327+
from mcp import ClientSession, types
328+
329+
330+
async def handle_log(params: types.LoggingMessageNotificationParams) -> None:
331+
"""Handle log messages from the server."""
332+
print(f"[{params.level}] {params.data}")
333+
334+
335+
async with ClientSession(
336+
read_stream,
337+
write_stream,
338+
logging_callback=handle_log,
339+
) as session:
340+
await session.initialize()
341+
# Log messages from the server will be passed to handle_log
342+
```
343+
344+
### Setting Server Log Level
345+
346+
Request the server to change its minimum logging level:
347+
348+
```python
349+
# Ask the server to only send warning-level and above
350+
await session.set_logging_level("warning")
351+
```
352+
353+
The `set_logging_level()` method sends a `logging/setLevel` request. The server decides how to handle this (see [Setting Log Level](server.md#setting-log-level) for the server side).
354+
218355
## OAuth Authentication for Clients
219356

220357
For OAuth 2.1 client authentication, see [Authorization](authorization.md).

docs/protocol.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,167 @@ MCP servers declare capabilities during initialization:
2424
| `logging` | - | Server logging configuration |
2525
| `completions`| - | Argument completion suggestions |
2626

27+
## Ping
28+
29+
Both clients and servers can send pings to check that the other side is responsive. The SDK provides `send_ping()` on both session types:
30+
31+
```python
32+
# Client pinging the server
33+
from mcp import ClientSession
34+
35+
result = await session.send_ping() # Returns EmptyResult
36+
37+
38+
# Server pinging the client (from a tool or handler)
39+
from mcp.server.fastmcp import Context, FastMCP
40+
from mcp.server.session import ServerSession
41+
42+
mcp = FastMCP("Ping Example")
43+
44+
45+
@mcp.tool()
46+
async def check_client(ctx: Context[ServerSession, None]) -> str:
47+
"""Check if the client is still responsive."""
48+
await ctx.session.send_ping()
49+
return "Client is alive"
50+
```
51+
52+
Ping requests are always allowed, even before initialization is complete.
53+
54+
## Cancellation
55+
56+
Either side can cancel a previously-issued request by sending a `CancelledNotification`. This is useful for long-running operations:
57+
58+
```python
59+
from mcp.types import CancelledNotification, CancelledNotificationParams
60+
61+
# Build a cancellation notification for a specific request
62+
notification = CancelledNotification(
63+
params=CancelledNotificationParams(
64+
requestId="request-123",
65+
reason="User cancelled the operation",
66+
),
67+
)
68+
```
69+
70+
The `CancelledNotificationParams` fields:
71+
72+
- `requestId` - The ID of the request to cancel (optional in the type, but should be provided)
73+
- `reason` - A human-readable reason for cancellation (optional)
74+
75+
Both `ClientNotification` and `ServerNotification` include `CancelledNotification` as a valid notification type.
76+
77+
## Capability Negotiation
78+
79+
During initialization, the client and server exchange their supported capabilities. The Python SDK handles this automatically based on the callbacks you provide.
80+
81+
### Client-Side Auto-Declaration
82+
83+
The `ClientSession` automatically declares capabilities based on which callbacks are provided:
84+
85+
```python
86+
from mcp import ClientSession, types
87+
from mcp.shared.context import RequestContext
88+
89+
90+
# Providing a sampling_callback automatically declares sampling capability
91+
async def my_sampling(
92+
context: RequestContext[ClientSession, None],
93+
params: types.CreateMessageRequestParams,
94+
) -> types.CreateMessageResult:
95+
...
96+
97+
98+
# Providing list_roots_callback declares roots capability (with listChanged=True)
99+
async def my_roots(
100+
context: RequestContext[ClientSession, None],
101+
) -> types.ListRootsResult:
102+
...
103+
104+
105+
session = ClientSession(
106+
read_stream,
107+
write_stream,
108+
sampling_callback=my_sampling, # -> sampling capability declared
109+
list_roots_callback=my_roots, # -> roots capability declared
110+
elicitation_callback=my_elicitation, # -> elicitation capability declared
111+
logging_callback=my_logging, # -> logging messages handled
112+
)
113+
```
114+
115+
### Querying Server Capabilities
116+
117+
After initialization, query what the server supports:
118+
119+
```python
120+
await session.initialize()
121+
122+
capabilities = session.get_server_capabilities()
123+
if capabilities:
124+
if capabilities.tools:
125+
print("Server supports tools")
126+
if capabilities.tools.listChanged:
127+
print("Server will notify on tool list changes")
128+
if capabilities.resources:
129+
print("Server supports resources")
130+
if capabilities.prompts:
131+
print("Server supports prompts")
132+
if capabilities.logging:
133+
print("Server supports logging")
134+
```
135+
136+
`get_server_capabilities()` returns `None` if the session has not been initialized yet.
137+
138+
## Protocol Version Negotiation
139+
140+
The SDK automatically negotiates the protocol version during initialization. Both client and server maintain lists of supported versions:
141+
142+
```python
143+
from mcp.types import LATEST_PROTOCOL_VERSION
144+
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
145+
146+
# The most recent protocol version
147+
print(LATEST_PROTOCOL_VERSION) # "2025-11-25"
148+
149+
# All versions the SDK can work with
150+
print(SUPPORTED_PROTOCOL_VERSIONS)
151+
# ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"]
152+
```
153+
154+
During initialization:
155+
156+
1. The client sends `LATEST_PROTOCOL_VERSION` as its requested version
157+
2. The server checks if that version is in its `SUPPORTED_PROTOCOL_VERSIONS`
158+
3. If supported, the server echoes it back; otherwise it responds with its own `LATEST_PROTOCOL_VERSION`
159+
4. The client verifies the server's chosen version is in its own `SUPPORTED_PROTOCOL_VERSIONS`
160+
5. If the versions are incompatible, the client raises a `RuntimeError`
161+
162+
## JSON Schema Generation
163+
164+
The SDK automatically generates [JSON Schema (2020-12)](https://json-schema.org/specification) from Python type annotations via Pydantic. This is used for tool input schemas, structured output schemas, and elicitation schemas:
165+
166+
```python
167+
from pydantic import BaseModel, Field
168+
169+
from mcp.server.fastmcp import FastMCP
170+
171+
mcp = FastMCP("Schema Example")
172+
173+
174+
class SearchQuery(BaseModel):
175+
query: str = Field(description="The search query string")
176+
max_results: int = Field(default=10, description="Maximum results to return")
177+
include_archived: bool = Field(default=False, description="Include archived items")
178+
179+
180+
@mcp.tool()
181+
def search(params: SearchQuery) -> str:
182+
"""Search with auto-generated JSON schema for the input."""
183+
return f"Searching for: {params.query}"
184+
```
185+
186+
The tool's `inputSchema` is automatically derived from the function's parameter type annotations. Pydantic models, `TypedDict`, dataclasses, and primitive types are all supported. The generated schema includes field descriptions, defaults, types, and validation constraints.
187+
27188
## Pagination
28189

29190
For pagination details, see:

0 commit comments

Comments
 (0)