fix: align task response types with MCP spec (#658)

This commit is contained in:
Dale Seo 2026-02-13 17:56:44 -05:00 • committed by GitHub
commit 08a5b0551b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 349 additions and 134 deletions

View file

@ -116,15 +116,15 @@ impl<H: ServerHandler> Service<RoleServer> for H {
ClientRequest::GetTaskInfoRequest(request) => self
.get_task_info(request.params, context)
.await
.map(ServerResult::GetTaskInfoResult),
.map(ServerResult::GetTaskResult),
ClientRequest::GetTaskResultRequest(request) => self
.get_task_result(request.params, context)
.await
.map(ServerResult::TaskResult),
.map(ServerResult::GetTaskPayloadResult),
ClientRequest::CancelTaskRequest(request) => self
.cancel_task(request.params, context)
.await
.map(ServerResult::empty),
.map(ServerResult::CancelTaskResult),
}
}
@ -339,7 +339,8 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
&self,
request: GetTaskInfoParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetTaskInfoResult, McpError>> + Send + '_ {
) -> impl Future<Output = Result<GetTaskResult, McpError>> + Send + '_ {
let _ = (request, context);
std::future::ready(Err(McpError::method_not_found::<GetTaskInfoMethod>()))
}
@ -347,7 +348,7 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
&self,
request: GetTaskResultParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<TaskResult, McpError>> + Send + '_ {
) -> impl Future<Output = Result<GetTaskPayloadResult, McpError>> + Send + '_ {
let _ = (request, context);
std::future::ready(Err(McpError::method_not_found::<GetTaskResultMethod>()))
}
@ -356,7 +357,7 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
&self,
request: CancelTaskParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), McpError>> + Send + '_ {
) -> impl Future<Output = Result<CancelTaskResult, McpError>> + Send + '_ {
let _ = (request, context);
std::future::ready(Err(McpError::method_not_found::<CancelTaskMethod>()))
}
@ -543,7 +544,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: GetTaskInfoParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetTaskInfoResult, McpError>> + Send + '_ {
) -> impl Future<Output = Result<GetTaskResult, McpError>> + Send + '_ {
(**self).get_task_info(request, context)
}
@ -551,7 +552,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: GetTaskResultParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<TaskResult, McpError>> + Send + '_ {
) -> impl Future<Output = Result<GetTaskPayloadResult, McpError>> + Send + '_ {
(**self).get_task_result(request, context)
}
@ -559,7 +560,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: CancelTaskParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<(), McpError>> + Send + '_ {
) -> impl Future<Output = Result<CancelTaskResult, McpError>> + Send + '_ {
(**self).cancel_task(request, context)
}
}

View file

@ -2537,14 +2537,9 @@ impl RequestParamsMeta for CancelTaskParams {
/// Deprecated: Use [`CancelTaskParams`] instead (SEP-1319 compliance).
#[deprecated(since = "0.13.0", note = "Use CancelTaskParams instead")]
pub type CancelTaskParam = CancelTaskParams;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetTaskInfoResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub task: Option<crate::model::Task>,
}
/// Deprecated: Use [`GetTaskResult`] instead (spec alignment).
#[deprecated(since = "0.15.0", note = "Use GetTaskResult instead")]
pub type GetTaskInfoResult = GetTaskResult;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
@ -2720,9 +2715,10 @@ ts_union!(
| EmptyResult
| CreateTaskResult
| ListTasksResult
| GetTaskInfoResult
| TaskResult
| GetTaskResult
| CancelTaskResult
| CustomResult
| GetTaskPayloadResult
;
);

View file

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::Meta;
/// Canonical task lifecycle status as defined by SEP-1686.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
@ -19,21 +21,10 @@ pub enum TaskStatus {
Cancelled,
}
/// Final result for a succeeded task (returned from `tasks/result`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TaskResult {
/// MIME type or custom content-type identifier.
pub content_type: String,
/// The actual result payload, matching the underlying request's schema.
pub value: Value,
/// Optional short summary for UI surfaces.
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
/// Primary Task object that surfaces metadata during the task lifecycle.
///
/// Per spec, `lastUpdatedAt` and `ttl` are required fields.
/// `ttl` is nullable (`null` means unlimited retention).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@ -48,10 +39,9 @@ pub struct Task {
/// ISO-8601 creation timestamp.
pub created_at: String,
/// ISO-8601 timestamp for the most recent status change.
#[serde(skip_serializing_if = "Option::is_none")]
pub last_updated_at: Option<String>,
pub last_updated_at: String,
/// Retention window in milliseconds that the receiver agreed to honor.
#[serde(skip_serializing_if = "Option::is_none")]
/// `None` (serialized as `null`) means unlimited retention.
pub ttl: Option<u64>,
/// Suggested polling interval (milliseconds).
#[serde(skip_serializing_if = "Option::is_none")]
@ -66,6 +56,43 @@ pub struct CreateTaskResult {
pub task: Task,
}
/// Response to a `tasks/get` request.
///
/// Per spec, `GetTaskResult = allOf[Result, Task]` — the Task fields are
/// flattened at the top level, not nested under a `task` key.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetTaskResult {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
#[serde(flatten)]
pub task: Task,
}
/// Response to a `tasks/result` request.
///
/// Per spec, the result structure matches the original request type
/// (e.g., `CallToolResult` for `tools/call`). This is represented as
/// an open object. The payload is the original request's result
/// serialized as a JSON value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GetTaskPayloadResult(pub Value);
/// Response to a `tasks/cancel` request.
///
/// Per spec, `CancelTaskResult = allOf[Result, Task]` — same shape as `GetTaskResult`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CancelTaskResult {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
#[serde(flatten)]
pub task: Task,
}
/// Paginated list of tasks
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]