You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/protocol.md
+194Lines changed: 194 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,6 +2,29 @@
2
2
3
3
This page covers cross-cutting protocol mechanics that apply to both clients and servers.
4
4
5
+
## Ping
6
+
7
+
Both client and server expose a `send_ping()` method for health/liveness checks. The remote side responds automatically -- no handler registration is needed. Pings are allowed at any point in the session lifecycle, even before initialization completes.
8
+
9
+
**Client pinging the server:**
10
+
11
+
```python
12
+
from mcp import ClientSession
13
+
14
+
asyncwith ClientSession(read_stream, write_stream) as session:
15
+
await session.initialize()
16
+
result =await session.send_ping() # returns EmptyResult
17
+
```
18
+
19
+
**Server pinging the client:**
20
+
21
+
```python
22
+
from mcp.server.session import ServerSession
23
+
24
+
# Inside a server handler where you have access to the session:
25
+
result =await server_session.send_ping() # returns EmptyResult
26
+
```
27
+
5
28
## Progress notifications
6
29
7
30
Long-running requests can report incremental progress to the caller. The SDK handles `progressToken` plumbing automatically when you provide a callback.
@@ -36,6 +59,45 @@ result = await session.call_tool(
36
59
37
60
The `progress_callback` receives three arguments: `progress` (float), `total` (float | None), and `message` (str | None).
38
61
62
+
## Cancellation
63
+
64
+
Either side can cancel an in-flight request by sending a `notifications/cancelled` message. In the Python SDK, the session's receive loop listens for `CancelledNotification` and cancels the corresponding request's scope.
65
+
66
+
**Client cancelling a request:**
67
+
68
+
```python
69
+
import anyio
70
+
from mcp import types
71
+
72
+
asyncwith ClientSession(read_stream, write_stream) as session:
73
+
await session.initialize()
74
+
75
+
asyncwith anyio.create_task_group() as tg:
76
+
asyncdefrun_tool():
77
+
result =await session.call_tool("slow-tool", {})
78
+
79
+
asyncdefcancel_after_delay():
80
+
await anyio.sleep(5)
81
+
# Send cancellation for request ID 0
82
+
await session.send_notification(
83
+
types.ClientNotification(
84
+
types.CancelledNotification(
85
+
params=types.CancelledNotificationParams(
86
+
requestId=0,
87
+
reason="Timed out waiting",
88
+
)
89
+
)
90
+
)
91
+
)
92
+
93
+
tg.start_soon(run_tool)
94
+
tg.start_soon(cancel_after_delay)
95
+
```
96
+
97
+
**Server-side cancellation handling:**
98
+
99
+
When the SDK receives a `CancelledNotification`, it automatically calls `cancel()` on the in-flight `RequestResponder`, which cancels its `anyio.CancelScope`. Tool handlers running inside that scope will receive a `Cancelled` exception from anyio. No additional handler registration is needed.
100
+
39
101
## Pagination
40
102
41
103
All list methods (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`) support cursor-based pagination. Pass the `nextCursor` from the previous response to fetch the next page.
@@ -57,3 +119,135 @@ while True:
57
119
```
58
120
59
121
The same pattern applies to `list_prompts`, `list_resources`, and `list_resource_templates`.
122
+
123
+
## Capability negotiation
124
+
125
+
Both client and server declare their capabilities during the `initialize` handshake. The SDK uses these declarations to determine which features are available.
126
+
127
+
**Client capabilities** are determined automatically based on the callbacks you provide when constructing `ClientSession`:
128
+
129
+
```python
130
+
from mcp import ClientSession
131
+
132
+
session = ClientSession(
133
+
read_stream,
134
+
write_stream,
135
+
# Providing these callbacks automatically declares the corresponding capabilities
# After initialization, inspect server capabilities:
143
+
server_caps = session.get_server_capabilities()
144
+
if server_caps and server_caps.tools:
145
+
tools =await session.list_tools()
146
+
if server_caps and server_caps.resources and server_caps.resources.subscribe:
147
+
# Server supports resource subscriptions
148
+
pass
149
+
```
150
+
151
+
**Server capabilities** are inferred from registered handlers. When using FastMCP, capabilities are set automatically based on what you register (tools, resources, prompts). With the low-level `Server`, the `get_capabilities` method inspects which request handlers are registered:
152
+
153
+
```python
154
+
from mcp.server.lowlevel import Server
155
+
156
+
server = Server("my-server")
157
+
158
+
# Registering a handler for ListToolsRequest causes the server
159
+
# to advertise the tools capability automatically.
160
+
@server.list_tools()
161
+
asyncdefhandle_list_tools():
162
+
return [...]
163
+
164
+
# The capabilities are built when creating initialization options:
165
+
options = server.create_initialization_options()
166
+
# options.capabilities.tools will be set because list_tools handler exists
167
+
```
168
+
169
+
## Protocol version negotiation
170
+
171
+
The SDK automatically negotiates protocol versions during the `initialize` handshake. The client sends `LATEST_PROTOCOL_VERSION` and the server responds with the highest mutually supported version.
172
+
173
+
Supported versions are defined in `SUPPORTED_PROTOCOL_VERSIONS`:
174
+
175
+
```python
176
+
from mcp.types importLATEST_PROTOCOL_VERSION
177
+
from mcp.shared.version importSUPPORTED_PROTOCOL_VERSIONS
During initialization, the server checks whether the client's requested version is in `SUPPORTED_PROTOCOL_VERSIONS`. If it is, the server echoes that version back. Otherwise, the server responds with `LATEST_PROTOCOL_VERSION`. On the client side, if the server's response contains a version not in `SUPPORTED_PROTOCOL_VERSIONS`, the client raises a `RuntimeError`.
187
+
188
+
This negotiation is handled automatically by `ClientSession.initialize()`:
189
+
190
+
```python
191
+
asyncwith ClientSession(read_stream, write_stream) as session:
If you need to specify a default version for clients that do not declare one, the SDK uses `DEFAULT_NEGOTIATED_VERSION` (`"2025-03-26"`).
200
+
201
+
## JSON Schema 2020-12
202
+
203
+
MCP uses [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) for tool input schemas (`inputSchema`) and output schemas (`outputSchema`). When using FastMCP with Python type annotations, schemas are generated automatically via Pydantic:
204
+
205
+
```python
206
+
from pydantic import BaseModel, Field
207
+
from mcp.server.fastmcp import FastMCP
208
+
209
+
mcp = FastMCP("example")
210
+
211
+
@mcp.tool()
212
+
asyncdefcalculate(a: float, b: float) -> float:
213
+
"""Add two numbers."""
214
+
return a + b
215
+
216
+
# The SDK generates an inputSchema like:
217
+
# {
218
+
# "type": "object",
219
+
# "properties": {
220
+
# "a": {"type": "number"},
221
+
# "b": {"type": "number"}
222
+
# },
223
+
# "required": ["a", "b"]
224
+
# }
225
+
```
226
+
227
+
With the low-level `Server`, you provide JSON Schema directly when defining tools:
228
+
229
+
```python
230
+
from mcp import types
231
+
232
+
tool = types.Tool(
233
+
name="calculate",
234
+
description="Add two numbers",
235
+
inputSchema={
236
+
"type": "object",
237
+
"properties": {
238
+
"a": {"type": "number"},
239
+
"b": {"type": "number"},
240
+
},
241
+
"required": ["a", "b"],
242
+
},
243
+
outputSchema={
244
+
"type": "object",
245
+
"properties": {
246
+
"result": {"type": "number"},
247
+
},
248
+
"required": ["result"],
249
+
},
250
+
)
251
+
```
252
+
253
+
When `outputSchema` is provided, the client SDK validates the tool's `structuredContent` response against that schema using `jsonschema.validate`.
0 commit comments