Skip to content

feat(task): route server-originated tasks/* to ClientHandler (SEP-1686)#816

Open
TheUnderdev wants to merge 2 commits into
modelcontextprotocol:mainfrom
TheUnderdev:feat/bidirectional-tasks
Open

feat(task): route server-originated tasks/* to ClientHandler (SEP-1686)#816
TheUnderdev wants to merge 2 commits into
modelcontextprotocol:mainfrom
TheUnderdev:feat/bidirectional-tasks

Conversation

@TheUnderdev
Copy link
Copy Markdown

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:

  • `capabilities.tasks.requests.sampling.createMessage`
  • `capabilities.tasks.requests.elicitation.create`
  • client-side `capabilities.tasks.list` / `capabilities.tasks.cancel`

…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)

  • `ServerRequest` (`crates/rmcp/src/model.rs`): add `GetTaskInfoRequest | ListTasksRequest | GetTaskResultRequest | CancelTaskRequest` variants; add a `ServerRequest::method()` accessor mirroring `ClientRequest::method()`; extend 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 payloads still surface as `CustomResult` on the wire, matching the server-side pattern.
  • `ClientHandler` (`crates/rmcp/src/handler/client.rs`): add `list_tasks`, `get_task_info`, `get_task_result`, `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.
  • Golden schemas: `crates/rmcp/tests/test_message_schema/*_json_rpc_message_schema{,_current}.json` regenerated with `UPDATE_SCHEMA=1` to include the new `ServerRequest` and `ClientResult` variants.

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":

  • `tasks/result`: `GetTaskPayloadResult` has an existing custom `Deserialize` that always fails (so that `CustomResult` can catch any JSON in the untagged enum). The test asserts the response arrives as `ClientResult::CustomResult`, matching the server-side convention.
  • `tasks/cancel`: `CancelTaskResult` and `GetTaskResult` share the same JSON shape (both are `Result + flattened Task`); serde's untagged enum picks the first match. Callers distinguish by knowing which request they sent. This mirrors existing behavior on the server side and is out of scope for this PR.

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:

  • Clients that had been extracting task methods from `CustomRequest` via a workaround will no longer see them there — they now arrive as the typed variants. Any such code should migrate to the new `ClientHandler` methods.
  • The `ClientResult` enum gains four variants, which will widen JSON Schema definitions for anyone consuming `ClientJsonRpcMessage` via `schemars`. The `test_message_schema` golden files are updated accordingly.

Types of changes

  • New feature (non-breaking change which adds functionality)
  • Bug fix
  • Breaking change
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally (`cargo test -p rmcp --all-features`)
  • I have added appropriate error handling (handlers fall back to `-32601`)
  • I have added or updated documentation as needed — no user-facing docs touched; the trait-level doc comments on each new `ClientHandler` method explain when to override.

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`.

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).
@TheUnderdev TheUnderdev requested a review from a team as a code owner April 20, 2026 16:42
@github-actions github-actions Bot added T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes T-core Core library changes T-handler Handler implementation changes T-model Model/data structure changes labels Apr 20, 2026
Copy link
Copy Markdown
Contributor

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for closing this gap. I left an action required item about tasks/cancel vs tasks/delete

Comment thread crates/rmcp/src/handler/client.rs Outdated
std::future::ready(Err(McpError::method_not_found::<GetTaskResultMethod>()))
}

/// Handle a `tasks/cancel` request from a server. Only relevant when
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • DeleteTaskResult now carries only _meta, matching the §3.6 wire response (the old CancelTaskResult flattened a Task in, which wasn't in the spec).
  • Gave DeleteTaskResult the same always-fail Deserialize impl used for GetTaskPayloadResult so its near-empty shape doesn't shadow EmptyResult / CustomResult in the untagged ClientResult / ServerResult enums. On the wire, delete responses now fall through to EmptyResult.
  • TaskManager got a new delete_task helper that aborts a running task AND removes any stored completed result — the actual delete semantics. The #[task_handler] macro now uses it.

@alexhancock
Copy link
Copy Markdown
Contributor

@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>
@github-actions github-actions Bot added the T-macros Macro changes label May 13, 2026
@TheUnderdev
Copy link
Copy Markdown
Author

@alexhancock yep — addressed in 6d45614, reply on the review thread with the details. tl;dr renamed everything to tasks/delete per SEP §3.6 and aligned the result shape (empty _meta only, instead of the old flattened Task). Ready for another look when you have a moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-handler Handler implementation changes T-macros Macro changes T-model Model/data structure changes T-test Testing related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants