feat(task): route server-originated tasks/* to ClientHandler (SEP-1686)#816
feat(task): route server-originated tasks/* to ClientHandler (SEP-1686)#816TheUnderdev wants to merge 2 commits into
Conversation
Tasks are bidirectional per SEP-1686: either party can be the requestor or the receiver. The ServerHandler side is already wired for client→server task flow (tools/call augmentation). This patch mirrors the same wiring on the client side so servers that initiate task-augmented requests (notably sampling/createMessage and elicitation/create) can follow up with tasks/get, tasks/list, tasks/result, and tasks/cancel directed at the client. Changes, purely additive: * ServerRequest: add GetTaskInfoRequest | ListTasksRequest | GetTaskResultRequest | CancelTaskRequest variants. Add a ServerRequest::method() accessor mirroring ClientRequest::method(). Update the variant_extension! invocation so the existing GetExtensions / GetMeta impls cover the new variants. * ClientResult: add ListTasksResult | GetTaskResult | GetTaskPayloadResult | CancelTaskResult response variants. GetTaskPayloadResult retains its existing custom Deserialize-fails behavior, so payload responses are still observed on the wire as CustomResult (matching the server-side pattern). * ClientHandler: add list_tasks, get_task_info, get_task_result, and cancel_task methods with default -32601 Method-not-found impls, mirroring the server-side signatures. Propagate via the Box/Arc wrapper macro. Dispatch all four from the handle_request match. This unblocks clients that want to advertise capabilities.tasks.requests.sampling.createMessage, capabilities.tasks.requests.elicitation.create, or the client-side tasks.list / tasks.cancel capabilities: previously, servers had no way to reach the client's task methods through the typed request enum, and such capabilities couldn't be honored end-to-end. Tests: new test_task_client_receiver.rs exercises a full bidirectional roundtrip for each of the four methods (server → client RPC → ClientHandler → response → server), plus a default-impl test that confirms the unit () client returns -32601 for tasks/get. Existing message-schema golden files regenerated to include the new ServerRequest and ClientResult variants; no other tests affected. Related: modelcontextprotocol#528, modelcontextprotocol#536 (which added the server-side half of SEP-1686).
alexhancock
left a comment
There was a problem hiding this comment.
Thanks for closing this gap. I left an action required item about tasks/cancel vs tasks/delete
| std::future::ready(Err(McpError::method_not_found::<GetTaskResultMethod>())) | ||
| } | ||
|
|
||
| /// Handle a `tasks/cancel` request from a server. Only relevant when |
There was a problem hiding this comment.
tasks/cancel is not in the SEP as I see it https://modelcontextprotocol.io/seps/1686-tasks
tasks/delete I think is perhaps what is meant?
https://modelcontextprotocol.io/seps/1686-tasks#3-6-deleting-tasks
There was a problem hiding this comment.
Good catch — fixed in 6d45614.
Renamed across the board: method string is now tasks/delete, types are DeleteTaskMethod / DeleteTaskParams / DeleteTaskRequest / DeleteTaskResult, and the trait methods on ClientHandler / ServerHandler are delete_task. The terminal cancelled task status stays (it's named that way in the SEP).
A couple of related fixes that fell out of the rename:
DeleteTaskResultnow carries only_meta, matching the §3.6 wire response (the oldCancelTaskResultflattened aTaskin, which wasn't in the spec).- Gave
DeleteTaskResultthe same always-failDeserializeimpl used forGetTaskPayloadResultso its near-empty shape doesn't shadowEmptyResult/CustomResultin the untaggedClientResult/ServerResultenums. On the wire, delete responses now fall through toEmptyResult. TaskManagergot a newdelete_taskhelper that aborts a running task AND removes any stored completed result — the actual delete semantics. The#[task_handler]macro now uses it.
|
@TheUnderdev Do you want to continue on this and address #816 (comment) ? |
tasks/cancel is not part of SEP-1686. The spec defines tasks/delete (§3.6) for explicitly removing a task and its associated stored result. Rename the method, request/params/result types, and trait methods to match. cancelled remains the terminal task *status* name, since that matches the SEP. DeleteTaskResult drops the flattened Task field — per spec the response is just _meta. It also gets a custom always-fail Deserialize impl (same trick used for GetTaskPayloadResult) so its on-wire shape does not shadow EmptyResult / CustomResult in the untagged ClientResult / ServerResult enums. TaskManager gains delete_task(&mut self, &str) -> bool which aborts a running task AND removes any stored completed result — the real delete semantics. The existing cancel_task is left in place as a public API. The #[task_handler] macro now generates delete_task (calling processor.delete_task) instead of cancel_task, and no longer rejects deletion of already-completed tasks. Addresses review feedback on modelcontextprotocol#816. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@alexhancock yep — addressed in 6d45614, reply on the review thread with the details. tl;dr renamed everything to |
Summary
Tasks are bidirectional per SEP-1686: either party can be the requestor or the receiver. The `ServerHandler` side is already wired for the client→server task flow (`tools/call` augmentation, merged in #536). This PR mirrors the same wiring on the client side so servers that initiate task-augmented requests — notably `sampling/createMessage` and `elicitation/create` — can follow up with `tasks/get`, `tasks/list`, `tasks/result`, and `tasks/cancel` directed at the client.
Motivation and Context
The 2025-11-25 spec allows a server to task-augment its own `sampling/createMessage` or `elicitation/create` requests. When it does, the client returns a `CreateTaskResult` immediately and the server polls the client for completion. Today this is impossible because `ServerRequest` (the typed union of requests a client handler dispatches on) doesn't include the four `tasks/*` variants, and `ClientHandler` has no methods for them.
Concretely, this means a client cannot honestly advertise any of:
…because the server has no typed way to poll it. A well-behaved server sees those caps absent and falls back to synchronous request/response, which blocks its connection/thread for the duration of whatever the client is doing (user reply, slow LLM inference, etc.). Closing this gap lets clients offer the same deferred-result ergonomics the spec already gives servers.
Changes (purely additive)
How Has This Been Tested?
New integration test file: `crates/rmcp/tests/test_task_client_receiver.rs` (5 tests).
It stands up a real rmcp client↔server pair over `tokio::io::duplex` and verifies the full RPC roundtrip for each of the four methods: the server sends a `ServerRequest::*` → the client dispatches into the matching `ClientHandler` method → the handler returns the appropriate result variant → the server receives it as the right `ClientResult` variant. There's also a default-impl test asserting that the unit `()` client returns `-32601` for `tasks/get`.
Two edge cases are documented in the tests rather than "fixed":
All existing rmcp tests still pass (`cargo test -p rmcp --all-features`, 48 green test groups, 0 failures).
Breaking Changes
None intended. The new `ServerRequest` / `ClientResult` variants are additive, and the new `ClientHandler` trait methods have default implementations that preserve today's `Method not found` behavior for unaware clients. Unknown methods still route to `on_custom_request` exactly as before.
Two observations worth noting:
Types of changes
Checklist
Additional context
Related: #528 (Implement SEP-1686: Tasks), #536 (server-side half of SEP-1686).
This PR doesn't change any send-side behavior (the four `tasks/*` variants were already in `ClientRequest`); it only closes the receive-side gap on `RoleClient`.