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
docs: annotated low-level before/after example on the whats-new page
Show the same one-tool server written for v1 and for v2 in the 'rebuilt,
not renamed' section, with code annotations carrying the contrasts: how
the request context is reached (ambient ContextVar vs the ctx argument),
return wrapping (bare lists vs full result models), argument validation
(enforced jsonschema in v1 vs advertised-only in v2), exception polarity
(isError=True text the model reads vs a sanitized -32603 protocol
error), and registration (decorators vs constructor arguments).
The v2 half is a tested file, docs_src/whats_new/tutorial001.py, driven
end to end by tests/docs_src/test_whats_new.py (schema advertised
verbatim; a call missing a required argument reaches the handler and
comes back as the sanitized protocol error). The v1 half cannot import
in CI, so it stays an inline fence with a comment recording its ground
truth: the exact code was run verbatim against a real mcp==1.28.1
install.
Copy file name to clipboardExpand all lines: docs/whats-new.md
+56-1Lines changed: 56 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,7 +40,62 @@ v1 handed you three nested layers: a transport context manager yielding raw stre
40
40
41
41
### The low-level `Server` was rebuilt, not renamed
42
42
43
-
If you work at the JSON-RPC layer, this is the "everything is different" part of v2. Handlers are **constructor arguments**, not decorators: every one has the same shape, `async (ctx, params) -> result`, with typed params in and a full result type out. Nothing is wrapped, converted, or inferred for you any more, the old jsonschema check of tool arguments against your `input_schema` is gone, and an exception is a protocol error, never an `is_error=True` tool result. The ambient `server.request_context` ContextVar is gone; the context is the `ctx` argument. Custom, vendor-namespaced methods are first class through `add_request_handler(method, params_type, handler)`, which validates inbound params against your model before your handler runs. And a `middleware` list (deliberately marked provisional) wraps every inbound message, replacing the private `_handle_*` methods people used to override.
43
+
If you work at the JSON-RPC layer, this is the "everything is different" part of v2. Here is the same one-tool server both ways; click the markers for what moved.
44
+
45
+
<!-- The v1 fence cannot be a tested docs_src file (nothing in CI can import the
46
+
1.x SDK). Its ground truth: this exact code was run verbatim against a real
47
+
mcp==1.28.1 install. If you edit it, re-validate it against 1.x. -->
48
+
49
+
```python title="v1"
50
+
from typing import Any
51
+
52
+
import mcp.types as types
53
+
from mcp.server.lowlevel import Server
54
+
55
+
server = Server("Bookshop")
56
+
57
+
58
+
@server.list_tools() # (1)!
59
+
asyncdeflist_tools() -> list[types.Tool]:
60
+
return [ # (2)!
61
+
types.Tool(
62
+
name="search_books",
63
+
description="Search the catalog by title or author.",
1. Handlers are registered with decorators (called, with parentheses), any time after the server exists.
80
+
2. You return a bare `list[Tool]` and the SDK wraps it into a `ListToolsResult`.
81
+
3. Fields are camelCase in Python, and the schema is **enforced**: the SDK jsonschema-validates `call_tool` arguments against it before your function runs, which is why `arguments["query"]` below is safe.
82
+
4. The handler receives the tool name and the already-validated arguments, unpacked and never `None`.
83
+
5. The context comes from an ambient ContextVar, reached through the server object mid-request.
84
+
6. Bare content blocks are wrapped into a `CallToolResult` for you, and an exception raised anywhere in the handler is caught and returned as `CallToolResult(isError=True)` with `str(e)` as its text: the calling model reads your exception messages.
85
+
86
+
```python title="v2"
87
+
--8<--"docs_src/whats_new/tutorial001.py"
88
+
```
89
+
90
+
1. Fields are snake_case now, and the schema is **advertised but never applied**: nothing checks the arguments before your handler runs.
91
+
2. Every handler has the same shape: `async (ctx, params) -> result`. The context is the first argument (`ctx.session`, `ctx.request_id`, `ctx.protocol_version` live on it); this is where `server.request_context` went.
92
+
3. You build the full `ListToolsResult` yourself. Returning a bare list is a server-side `TypeError` now, not something the SDK wraps.
93
+
4. Typed params in (`params.name`, `params.arguments`), a full result out. Nothing is unpacked, wrapped, or converted for you.
94
+
5.`params.arguments` can be `None`; v1 defaulted it to `{}` before your code ever saw it. With no validation in front of the handler, this line is load-bearing.
95
+
6. An exception raised here becomes a sanitized protocol error (`-32603`, `"Internal server error"`): the model never sees the message. For a failure the model should read and react to, return `CallToolResult(is_error=True, ...)`; for a specific wire error, raise `MCPError(code, message)`.
96
+
7. Handlers are constructor arguments, so the server's surface is complete the moment it exists; `add_request_handler()` is the post-construction escape hatch, and the door to custom methods.
97
+
98
+
The example is the pattern. More generally: every handler has the same shape, with typed params in and a full result type out; the old jsonschema check of tool arguments is gone; an exception is a protocol error, never an `is_error=True` tool result; and the ambient `server.request_context` ContextVar is gone. Custom, vendor-namespaced methods are first class through `add_request_handler(method, params_type, handler)`, which validates inbound params against your model before your handler runs. And a `middleware` list (deliberately marked provisional) wraps every inbound message, replacing the private `_handle_*` methods people used to override.
44
99
45
100
Underneath, the v1 `BaseSession` receive loop was replaced by a dispatcher engine that the client and the server now share, and it is what makes several things on this page true at once: one `Server` object serves both protocol eras, `Client(server)` dispatches in process with no JSON-RPC framing, and a timed-out client request now actually cancels the server-side handler.
0 commit comments