-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Let a legacy-era tools/call answer with a CreateTaskResult #3161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,7 +40,7 @@ Every section heading below names the API it affects, so searching this page for | |
| | pin dependencies or use the `mcp` CLI | [Packaging, dependencies, and CLI](#packaging-dependencies-and-cli) | | ||
| | import `mcp.types` or touch protocol types (everyone does) | [Types and wire format](#types-and-wire-format) | | ||
| | run `FastMCP`/`MCPServer` servers | [MCPServer (formerly FastMCP)](#mcpserver-formerly-fastmcp) | | ||
| | 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 | | ||
| | 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 | | ||
| | write client code with `Client` or `ClientSession` | [Clients](#clients), plus [`streamablehttp_client` removed](#streamablehttp_client-removed) under Transports | | ||
| | use stdio or streamable HTTP directly, or maintain a custom transport | [Transports](#transports) | | ||
| | maintain OAuth client auth or a protected server | [OAuth and server auth](#oauth-and-server-auth) | | ||
|
|
@@ -2008,11 +2008,59 @@ async def elicitation_callback( | |
| ) -> ElicitResult | ErrorData: ... | ||
| ``` | ||
|
|
||
| ### Experimental Tasks support removed | ||
| ### Experimental Tasks runtime removed | ||
|
|
||
| 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`. | ||
| 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`. | ||
|
|
||
| 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. | ||
| The task types stay, so a server can still serve Tasks ([SEP-1686](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686)) on a 2025-11-25 connection by supplying the parts the runtime used to provide. 2025-11-25 is the only revision that defines them: the earlier handshake revisions predate SEP-1686, so a `CreateTaskResult` there is rejected as an internal error even though `params.task` reaches the handler. 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`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A server created from this example cannot complete the documented Tasks flow because it registers only Prompt for AI agents |
||
|
|
||
| ```python | ||
| async def call_tool( | ||
| ctx: ServerRequestContext, params: CallToolRequestParams | ||
| ) -> CallToolResult | CreateTaskResult: | ||
| if params.task is None or ctx.protocol_version != "2025-11-25": | ||
| return CallToolResult(content=[TextContent(text=run_now())]) | ||
| return CreateTaskResult(task=await store.submit(params)) | ||
|
|
||
|
|
||
| async def get_task(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult: | ||
| return await store.status(params.task_id) | ||
|
|
||
|
|
||
| server = Server("example", on_call_tool=call_tool) | ||
| server.add_request_handler("tasks/get", GetTaskRequestParams, get_task) | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| The same era check belongs in `on_call_tool`, and `params.task` is not a substitute for it. The handler receives the version-free params model, which carries `task` at every version, so a client can set the field on a 2026-07-28 connection and reach a handler that then answers with a `CreateTaskResult`, which that revision rejects as an opaque internal error. Gate on `ctx.protocol_version == "2025-11-25"` and treat `params.task` as the opt-in within that era, not as the era test. | ||
|
|
||
| `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 add the advertisement by overriding `create_initialization_options`. Override rather than building an `InitializationOptions` and passing it in: `streamable_http_app()` and the SSE app call the method themselves and take no override, so only the subclass reaches every transport. | ||
|
|
||
| ```python | ||
| class TasksServer(Server[Any]): | ||
| def create_initialization_options(self, *args: Any, **kwargs: Any) -> InitializationOptions: | ||
| options = super().create_initialization_options(*args, **kwargs) | ||
| tasks = ServerTasksCapability( | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| list=TasksListCapability(), | ||
| cancel=TasksCancelCapability(), | ||
| requests=ServerTasksRequestsCapability(tools=TasksToolsCapability(call={})), | ||
| ) | ||
| return options.model_copy( | ||
| update={"capabilities": options.capabilities.model_copy(update={"tasks": tasks})} | ||
| ) | ||
| ``` | ||
|
|
||
| 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: | ||
|
|
||
| ```python | ||
| result = await client.session.send_request( | ||
| CallToolRequest(params=CallToolRequestParams(name="render", task=TaskMetadata(ttl=60_000))), | ||
| TypeAdapter[CallToolResult | CreateTaskResult](CallToolResult | CreateTaskResult), | ||
| ) | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| There is no drop-in replacement for the tasks runtime (`server.experimental.enable_tasks()`, `ctx.experimental.run_task()`, `ServerTaskContext`, and the client's `session.experimental.call_tool_as_task()` / `poll_task()` / `get_task_result()`); the port depends on what the code used tasks for. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -142,7 +142,7 @@ The renames announce themselves. These do not: | |||||
| Each of these is a section in the **[Migration Guide](migration.md)**: | ||||||
|
|
||||||
| * The **WebSocket transport**, both sides, and the `mcp[ws]` extra. It was never part of the MCP specification. | ||||||
| * 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. | ||||||
| * 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 2025-11-25 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. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A custom store alone does not make this task flow usable: Prompt for AI agents
Suggested change
|
||||||
| * `mcp.shared.version`, `mcp.shared.progress`, and `mcp.shared.session` (with the `RequestResponder` stub v1 `message_handler` annotations imported) as import paths. (`mcp.types` is *not* removed: it remains as a permanent alias for the standalone `mcp_types` package.) | ||||||
| * The deprecated `streamablehttp_client` spelling, and the `get_session_id` callback from `streamable_http_client` (which now yields exactly two streams). | ||||||
| * `McpError`, renamed **`MCPError`** with a direct `(code, message, data)` constructor. | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 nit (optional): Renaming this heading from "Experimental Tasks support removed" to "Experimental Tasks runtime removed" changes its URL fragment, so existing off-site links to migration.md#experimental-tasks-support-removed on the published v2 docs silently land at the top of this ~2000-line page instead of the section. Fix: preserve the old anchor when renaming a heading on this page — attr_list is enabled in mkdocs.yml, so
### Experimental Tasks runtime removed {#experimental-tasks-support-removed}(or an explicit<a id=...>) keeps both; in-repo links were updated, but nothing validates external ones.Extended reasoning...
The base branch publishes the section as
### Experimental Tasks support removed, giving the rendered page the anchor#experimental-tasks-support-removed(mkdocs derives fragments from heading text). v2 is released (AGENTS.md: "v2 is released"; migration.md is "the v1 -> v2 record and is closed to new entries"), so this page has been live and is exactly the kind of page users bookmark and link from issues, release notes, and blog posts when porting the removed tasks runtime. The PR renames the heading at docs/migration.md:2011 to### Experimental Tasks runtime removed, changing the fragment to#experimental-tasks-runtime-removed, and updates the two in-repo references (docs/migration.md:43 and the new docs/advanced/low-level-server.md:199 link) —git grep experimental-tasks-support-removed HEAD~1confirms the only in-repo reference was the table row, so after merge no alias for the old fragment exists anywhere. Why safeguards miss it: mkdocs strict link validation only checks links that exist inside the repo; a missing fragment on an external visitor's URL produces no build…Verification: nit — The diff removes
### Experimental Tasks support removedand adds### Experimental Tasks runtime removedin docs/migration.md, and the base branch's own routing-table link[Experimental Tasks support removed](#experimental-tasks-support-removed)(updated in the same diff to#experimental-tasks-runtime-removed) confirms the published anchor derived from the old heading. mkdocs.yml…