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
Let a legacy-era tools/call answer with a CreateTaskResult
SEP-1686 makes a task-augmented `tools/call` answer with a `CreateTaskResult`,
but the `tools/call` result rows held `CallToolResult` alone. The request half
shipped and the response half did not, so a server returning a task got
`-32603 "Handler returned an invalid result"`, a server returning both shapes
had `task` silently sieved away, and a client receiving one raised
`ValidationError` before its caller saw anything. The v1.x line could express
that response; v2 cannot, on a revision the SDK still negotiates and still lets
a server advertise `capabilities.tasks` for.
Give the row its second arm as a generated `AnyCallToolResult`, alongside the
2026 aliases, and widen the lowlevel `Server`'s `on_call_tool` to match. Return
types are covariant, so a handler that only returns `CallToolResult` is
unaffected. All four pre-2026 versions get the arm, because all four share
`v2025.CallToolRequest` and therefore already parse `params.task`: answering it
at one of them and not the others is the same incoherence one version along.
`Task.ttl` is required and nullable ("null for unlimited"), and every dump path
passes `exclude_none=True`, which cannot tell a required null from an unset
optional and dropped it, leaving a body that fails the surface it was just
validated against. Rather than patch the 27 dump sites, models carrying such a
field now take a `KeepRequiredNullable` base that puts it back, and only that:
a field the caller filtered out with `include`/`exclude`, or one that was never
set, stays absent. The generator derives which classes need it from the schema
and a test asserts the same rule against the built models, so a nullable-required
field in a future revision is covered without anyone remembering.
That rule also reaches two live bugs of the same shape.
`LoggingMessageNotificationParams.data` is required and untyped, so
`ctx.log("info", None)` dropped the key and the receiving session rejected the
notification before dispatch: the message vanished with no error to either side,
at every protocol version. And `JSONRPCError.id` is required-and-nullable per
JSON-RPC 2.0, which the streamable-HTTP writer worked around by hand at one call
site; that patch is deleted and the base covers every writer.
The base is applied per model rather than to `MCPModel`/`WireModel`: a wrap
serializer costs per dump and pushes pydantic off its fast path for every model
in the tree, which measured 5x on a 20-tool `tools/list`. Its return is
deliberately unannotated, because an annotation there collapses the model's
serialization JSON schema to an opaque object.
The `tasks/*` lifecycle methods are left where they already work. They are
absent from `SPEC_CLIENT_METHODS`, so `Server.add_request_handler` serves them
today; adding registry rows would put those names into that version-flattened
set, which would make it illegal for an extension to bind `tasks/get` and would
gate the 2026 tasks extension's own calls to METHOD_NOT_FOUND. 2026-07-28 keeps
rejecting a `CreateTaskResult` on `tools/call`, which is correct: tasks left the
core protocol there.
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -185,6 +185,7 @@ The handshake belongs to the runner. `server/discover`, `ping`, and every other
185
185
Each of these is one idea you now have the vocabulary for; each has its own page.
186
186
187
187
*`on_call_tool`, `on_get_prompt`, and `on_read_resource` may return an `InputRequiredResult` instead of their normal result to pause the call and ask the client for input; see **[Multi-round-trip requests](../handlers/multi-round-trip.md)**. True to this tier, nothing is installed for you: where `MCPServer` seals `requestState` by default, here the `request_state` you set crosses the wire exactly as written until you opt in with `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: one line (both names import from `mcp.server.request_state`) for the identical sealing and verification `MCPServer` performs (**[Protecting `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
188
+
*`on_call_tool` may also return a `CreateTaskResult` to answer a task-augmented call on a handshake-era connection, which is the one piece of SEP-1686 the SDK still carries; the store and the `tasks/*` handlers are yours to bring, and the 2026-07-28 revision rejects the shape because tasks left its core protocol. See **[Experimental Tasks runtime removed](../migration.md#experimental-tasks-runtime-removed)**.
188
189
*`on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` are the same `(ctx, params) -> result` shape for the other primitives.
189
190
*`on_subscriptions_listen` serves the 2026-07-28 `subscriptions/listen` stream. Pass a `ListenHandler` built over a `SubscriptionBus` and publish events to the bus from your other handlers; see **[Subscriptions](../handlers/subscriptions.md)** for the full composition.
190
191
*`server.streamable_http_app()` returns the same Starlette app `MCPServer`'s does; deploy it the way **[Running your server](../run/index.md)** deploys any other ASGI app. There is no `server.run(transport=...)` down here: `server.run(read_stream, write_stream, server.create_initialization_options())` drives one connection over a pair of streams, and that one line is the whole story.
Copy file name to clipboardExpand all lines: docs/migration.md
+55-4Lines changed: 55 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,7 +33,7 @@ Every section heading below names the API it affects, so searching this page for
33
33
| pin dependencies or use the `mcp` CLI |[Packaging, dependencies, and CLI](#packaging-dependencies-and-cli)|
34
34
| import `mcp.types` or touch protocol types (everyone does) |[Types and wire format](#types-and-wire-format)|
35
35
| run `FastMCP`/`MCPServer` servers |[MCPServer (formerly FastMCP)](#mcpserver-formerly-fastmcp)|
36
-
| use the lowlevel `Server`|[Lowlevel Server](#lowlevel-server), plus [Timeouts take `float` seconds](#timeouts-take-float-seconds-instead-of-timedelta) and [Experimental Tasks support removed](#experimental-tasks-support-removed) under Clients |
36
+
| use the lowlevel `Server`|[Lowlevel Server](#lowlevel-server), plus [Timeouts take `float` seconds](#timeouts-take-float-seconds-instead-of-timedelta) and [Experimental Tasks runtime removed](#experimental-tasks-runtime-removed) under Clients |
37
37
| write client code with `Client` or `ClientSession`|[Clients](#clients), plus [`streamablehttp_client` removed](#streamablehttp_client-removed) under Transports |
38
38
| use stdio or streamable HTTP directly, or maintain a custom transport |[Transports](#transports)|
39
39
| maintain OAuth client auth or a protected server |[OAuth and server auth](#oauth-and-server-auth)|
@@ -1653,11 +1653,62 @@ Behavior changes:
1653
1653
1654
1654
`mcp.shared.session` is now a compatibility module: `ProgressFnT` is re-exported (its home is `mcp.shared.dispatcher`), and `RequestResponder` remains as a typing-only stub so `MessageHandlerFnT` annotations keep importing. `RequestResponder.respond()` no longer exists, and neither do the cancellation-tracking members (`cancel()`, the `cancelled` and `in_flight` properties, the `on_complete` constructor argument) or `BaseSession._in_flight`; inbound cancellation is handled by `JSONRPCDispatcher`.
1655
1655
1656
-
### Experimental Tasks support removed
1656
+
### Experimental Tasks runtime removed
1657
1657
1658
-
Tasks ([SEP-1686](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686)) have been removed from the MCP specification and are no longer part of this SDK. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. The corresponding `Task*` types remain in `mcp_types` as types-only definitions, except the `TaskExecutionMode` alias, whose literal is now inlined on `ToolExecution.task_support`.
1658
+
The task runtime that shipped behind the `experimental` properties is gone. The `mcp.client.experimental`, `mcp.server.experimental`, `mcp.shared.experimental`, and `mcp.server.lowlevel.experimental` modules have been removed, along with the `experimental` properties on `ClientSession`, `ServerSession`, `Server`, and `ServerRequestContext`. There is no built-in task store, no polling helper, and no automatic `tasks/*` routing. The `TaskExecutionMode` alias is also gone; its literal is inlined on `ToolExecution.task_support`.
1659
1659
1660
-
The 2026-07-28 revision reintroduces Tasks as an official extension: [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663), `io.modelcontextprotocol/tasks`, redesigned around polling (`tasks/get`) instead of a blocking `tasks/result`. This SDK does not implement the extension yet.
1660
+
The task types stay, so a server can still serve Tasks ([SEP-1686](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686)) on a handshake-era connection by supplying the parts the runtime used to provide. This covers the server side of a task-augmented `tools/call`; the client side of a task-augmented `sampling/createMessage` or `elicitation/create` is not wired, so a client cannot answer one of those with a `CreateTaskResult`. A task-augmented `tools/call` arrives with `params.task` set and may be answered with a `CreateTaskResult`, and the `tasks/get`, `tasks/result`, `tasks/list`, and `tasks/cancel` methods are registered with `Server.add_request_handler`.
Two things to know about handlers registered this way. They serve every negotiated version, so a server that also answers 2026-era clients should check `ctx.protocol_version` and reject anything outside 2025-11-25; the method names collide with the 2026 tasks extension but the payloads are not compatible. And their results are not validated against a per-version surface, so raise `MCPError` for the failure cases rather than letting an exception escape: an unhandled one reaches the client as an unmapped error carrying the exception text.
1680
+
1681
+
The same era check belongs in `on_call_tool`. Its return type admits a `CreateTaskResult` at every version because the signature has no version to key on, but 2026-07-28 dropped tasks from the core protocol and rejects the shape, turning it into an opaque internal error. Returning one only when `params.task` is set keeps that unreachable for a well-behaved client, since a client that never learned the field never sets it.
1682
+
1683
+
`Server.get_capabilities` does not derive a `tasks` capability from the registered handlers, and a spec-compliant client will not augment a request until it sees one, so build the advertisement explicitly:
A client sends the augmented request and names the result type through `ClientSession.send_request`, since `call_tool` resolves to the two core result arms:
The 2026-07-28 revision drops Tasks from the core protocol and reintroduces them as an official extension: [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663), `io.modelcontextprotocol/tasks`, redesigned around polling (`tasks/get`) instead of a blocking `tasks/result`. It is a different protocol, not a rename, and this SDK does not implement it yet.
Copy file name to clipboardExpand all lines: docs/whats-new.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -145,7 +145,7 @@ The renames announce themselves. These do not:
145
145
Each of these is a section in the **[Migration Guide](migration.md)**:
146
146
147
147
* The **WebSocket transport**, both sides, and the `mcp[ws]` extra. It was never part of the MCP specification.
148
-
* The **experimental Tasks**API (`mcp.*.experimental`). 2026-07-28 moves tasks out of the core protocol and into an official extension ([SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663)), which this SDK does not implement yet.
148
+
* The **experimental Tasks**runtime (`mcp.*.experimental`): the task store, the polling helper, and the automatic `tasks/*` routing. The task types stay, so a server can still answer a task-augmented `tools/call` on a handshake-era connection by bringing its own store; 2026-07-28 moves tasks out of the core protocol into an official extension ([SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663)), which this SDK does not implement yet.
149
149
*`mcp.types`, `mcp.shared.version`, and `mcp.shared.progress` as import paths.
150
150
* The deprecated `streamablehttp_client` spelling, and the `get_session_id` callback from `streamable_http_client` (which now yields exactly two streams).
151
151
*`McpError`, renamed **`MCPError`** with a direct `(code, message, data)` constructor.
0 commit comments