1use serde::{Deserialize, Serialize};
10
11use super::{InputRequests, JsonObject, MetaObject, ResultType};
12
13pub const TASKS_EXTENSION_ID: &str = "io.modelcontextprotocol/tasks";
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
20#[non_exhaustive]
21pub enum TaskStatus {
22 #[default]
24 Working,
25 InputRequired,
27 Completed,
30 Failed,
32 Cancelled,
34}
35
36impl TaskStatus {
37 pub fn is_terminal(&self) -> bool {
39 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
45#[serde(rename_all = "camelCase")]
46#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
47#[non_exhaustive]
48pub struct Task {
49 pub task_id: String,
51 pub status: TaskStatus,
53 #[serde(skip_serializing_if = "Option::is_none")]
56 pub status_message: Option<String>,
57 pub created_at: String,
59 pub last_updated_at: String,
61 pub ttl_ms: Option<u64>,
65 #[serde(skip_serializing_if = "Option::is_none")]
69 pub poll_interval_ms: Option<u64>,
70}
71
72impl Task {
73 pub fn new(
75 task_id: impl Into<String>,
76 status: TaskStatus,
77 created_at: impl Into<String>,
78 last_updated_at: impl Into<String>,
79 ) -> Self {
80 Self {
81 task_id: task_id.into(),
82 status,
83 status_message: None,
84 created_at: created_at.into(),
85 last_updated_at: last_updated_at.into(),
86 ttl_ms: None,
87 poll_interval_ms: None,
88 }
89 }
90
91 pub fn with_status_message(mut self, status_message: impl Into<String>) -> Self {
93 self.status_message = Some(status_message.into());
94 self
95 }
96
97 pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
99 self.ttl_ms = Some(ttl_ms);
100 self
101 }
102
103 pub fn with_poll_interval_ms(mut self, poll_interval_ms: u64) -> Self {
105 self.poll_interval_ms = Some(poll_interval_ms);
106 self
107 }
108}
109
110#[derive(Debug, Clone, PartialEq)]
117#[non_exhaustive]
118pub enum TaskPayload {
119 Working,
121 InputRequired {
123 input_requests: InputRequests,
127 },
128 Completed {
131 result: JsonObject,
133 },
134 Failed {
136 error: JsonObject,
138 },
139 Cancelled,
141}
142
143impl TaskPayload {
144 pub fn status(&self) -> TaskStatus {
146 match self {
147 Self::Working => TaskStatus::Working,
148 Self::InputRequired { .. } => TaskStatus::InputRequired,
149 Self::Completed { .. } => TaskStatus::Completed,
150 Self::Failed { .. } => TaskStatus::Failed,
151 Self::Cancelled => TaskStatus::Cancelled,
152 }
153 }
154}
155
156#[derive(Debug, Clone, PartialEq)]
164#[non_exhaustive]
165pub struct DetailedTask {
166 pub task: Task,
168 pub payload: TaskPayload,
170}
171
172impl DetailedTask {
173 pub fn new(mut task: Task, payload: TaskPayload) -> Self {
175 task.status = payload.status();
176 Self { task, payload }
177 }
178
179 pub fn status(&self) -> TaskStatus {
181 self.task.status
182 }
183}
184
185#[derive(Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
189struct DetailedTaskWire {
190 #[serde(flatten)]
191 task: Task,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 input_requests: Option<InputRequests>,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 result: Option<JsonObject>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 error: Option<JsonObject>,
198}
199
200impl From<DetailedTask> for DetailedTaskWire {
201 fn from(value: DetailedTask) -> Self {
202 let DetailedTask { task, payload } = value;
203 let (input_requests, result, error) = match payload {
204 TaskPayload::Working | TaskPayload::Cancelled => (None, None, None),
205 TaskPayload::InputRequired { input_requests } => (Some(input_requests), None, None),
206 TaskPayload::Completed { result } => (None, Some(result), None),
207 TaskPayload::Failed { error } => (None, None, Some(error)),
208 };
209 Self {
210 task,
211 input_requests,
212 result,
213 error,
214 }
215 }
216}
217
218impl TryFrom<DetailedTaskWire> for DetailedTask {
219 type Error = String;
220 fn try_from(wire: DetailedTaskWire) -> Result<Self, String> {
221 let payload = match wire.task.status {
222 TaskStatus::Working => TaskPayload::Working,
223 TaskStatus::Cancelled => TaskPayload::Cancelled,
224 TaskStatus::InputRequired => TaskPayload::InputRequired {
225 input_requests: wire.input_requests.ok_or_else(|| {
226 "task with status \"input_required\" is missing `inputRequests`".to_owned()
227 })?,
228 },
229 TaskStatus::Completed => TaskPayload::Completed {
230 result: wire.result.ok_or_else(|| {
231 "task with status \"completed\" is missing `result`".to_owned()
232 })?,
233 },
234 TaskStatus::Failed => TaskPayload::Failed {
235 error: wire
236 .error
237 .ok_or_else(|| "task with status \"failed\" is missing `error`".to_owned())?,
238 },
239 };
240 Ok(DetailedTask {
241 task: wire.task,
242 payload,
243 })
244 }
245}
246
247impl Serialize for DetailedTask {
248 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
249 DetailedTaskWire::from(self.clone()).serialize(serializer)
250 }
251}
252
253impl<'de> Deserialize<'de> for DetailedTask {
254 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
255 let wire = DetailedTaskWire::deserialize(deserializer)?;
256 Self::try_from(wire).map_err(serde::de::Error::custom)
257 }
258}
259
260#[cfg(feature = "schemars")]
261impl schemars::JsonSchema for DetailedTask {
262 fn schema_name() -> std::borrow::Cow<'static, str> {
263 "DetailedTask".into()
264 }
265 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
266 <DetailedTaskWire as schemars::JsonSchema>::json_schema(generator)
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize)]
279#[serde(rename_all = "camelCase")]
280#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
281#[non_exhaustive]
282pub struct CreateTaskResult {
283 pub result_type: ResultType,
285 #[serde(flatten)]
287 pub task: Task,
288 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
289 pub meta: Option<MetaObject>,
290}
291
292impl<'de> Deserialize<'de> for CreateTaskResult {
298 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
299 where
300 D: serde::Deserializer<'de>,
301 {
302 #[derive(Deserialize)]
303 #[serde(rename_all = "camelCase")]
304 struct Helper {
305 result_type: ResultType,
306 #[serde(flatten)]
307 task: Task,
308 #[serde(rename = "_meta", default)]
309 meta: Option<MetaObject>,
310 }
311 let helper = Helper::deserialize(deserializer)?;
312 if !helper.result_type.is_task() {
313 return Err(serde::de::Error::custom(
314 "CreateTaskResult requires resultType to be \"task\"",
315 ));
316 }
317 Ok(CreateTaskResult {
318 result_type: helper.result_type,
319 task: helper.task,
320 meta: helper.meta,
321 })
322 }
323}
324
325impl CreateTaskResult {
326 pub fn new(task: Task) -> Self {
328 Self {
329 result_type: ResultType::TASK,
330 task,
331 meta: None,
332 }
333 }
334
335 pub fn with_meta(mut self, meta: MetaObject) -> Self {
337 self.meta = Some(meta);
338 self
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
349#[non_exhaustive]
350pub struct GetTaskResult {
351 #[serde(default)]
354 pub result_type: ResultType,
355 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
356 pub meta: Option<MetaObject>,
357 #[serde(flatten)]
359 pub task: DetailedTask,
360}
361
362impl GetTaskResult {
363 pub fn new(task: DetailedTask) -> Self {
364 Self {
365 result_type: ResultType::COMPLETE,
366 meta: None,
367 task,
368 }
369 }
370}
371
372#[derive(Debug, Clone, PartialEq, Serialize)]
378#[serde(rename_all = "camelCase")]
379#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
380#[non_exhaustive]
381pub struct TaskAckResult {
382 pub result_type: ResultType,
384 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
385 pub meta: Option<MetaObject>,
386}
387
388impl<'de> Deserialize<'de> for TaskAckResult {
393 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
394 where
395 D: serde::Deserializer<'de>,
396 {
397 #[derive(Deserialize)]
398 #[serde(rename_all = "camelCase", deny_unknown_fields)]
399 struct Helper {
400 result_type: ResultType,
401 #[serde(rename = "_meta", default)]
402 meta: Option<MetaObject>,
403 }
404 let helper = Helper::deserialize(deserializer)?;
405 if !helper.result_type.is_complete() {
406 return Err(serde::de::Error::custom(
407 "TaskAckResult requires resultType to be \"complete\"",
408 ));
409 }
410 Ok(TaskAckResult {
411 result_type: helper.result_type,
412 meta: helper.meta,
413 })
414 }
415}
416
417impl Default for TaskAckResult {
418 fn default() -> Self {
419 Self {
420 result_type: ResultType::COMPLETE,
421 meta: None,
422 }
423 }
424}
425
426impl TaskAckResult {
427 pub fn new() -> Self {
428 Self::default()
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use serde_json::json;
435
436 use super::*;
437
438 fn base_task(status: TaskStatus) -> Task {
439 Task::new(
440 "task-1",
441 status,
442 "2025-11-25T10:30:00Z",
443 "2025-11-25T10:40:00Z",
444 )
445 .with_ttl_ms(60000)
446 .with_poll_interval_ms(5000)
447 }
448
449 #[test]
450 fn create_task_result_wire_shape() {
451 let result = CreateTaskResult::new(base_task(TaskStatus::Working));
452 let value = serde_json::to_value(&result).unwrap();
453 assert_eq!(
454 value,
455 json!({
456 "resultType": "task",
457 "taskId": "task-1",
458 "status": "working",
459 "createdAt": "2025-11-25T10:30:00Z",
460 "lastUpdatedAt": "2025-11-25T10:40:00Z",
461 "ttlMs": 60000,
462 "pollIntervalMs": 5000
463 })
464 );
465 let roundtrip: CreateTaskResult = serde_json::from_value(value).unwrap();
466 assert_eq!(roundtrip, result);
467 }
468
469 #[test]
470 fn ttl_ms_null_means_unlimited() {
471 let mut task = base_task(TaskStatus::Working);
472 task.ttl_ms = None;
473 let value = serde_json::to_value(&task).unwrap();
474 assert_eq!(value["ttlMs"], serde_json::Value::Null);
475 let roundtrip: Task = serde_json::from_value(value).unwrap();
476 assert_eq!(roundtrip.ttl_ms, None);
477 }
478
479 #[test]
480 fn detailed_task_completed_roundtrip() {
481 let detailed = DetailedTask::new(
482 base_task(TaskStatus::Working),
483 TaskPayload::Completed {
484 result: serde_json::from_value(json!({
485 "content": [{"type": "text", "text": "ok"}],
486 "isError": false
487 }))
488 .unwrap(),
489 },
490 );
491 assert_eq!(detailed.status(), TaskStatus::Completed);
493 let value = serde_json::to_value(&detailed).unwrap();
494 assert_eq!(value["status"], "completed");
495 assert_eq!(value["result"]["isError"], false);
496 let roundtrip: DetailedTask = serde_json::from_value(value).unwrap();
497 assert_eq!(roundtrip, detailed);
498 }
499
500 #[test]
501 fn detailed_task_input_required_requires_input_requests() {
502 let err = serde_json::from_value::<DetailedTask>(json!({
503 "taskId": "task-1",
504 "status": "input_required",
505 "createdAt": "2025-11-25T10:30:00Z",
506 "lastUpdatedAt": "2025-11-25T10:40:00Z",
507 "ttlMs": null
508 }))
509 .unwrap_err();
510 assert!(err.to_string().contains("inputRequests"));
511 }
512
513 #[test]
514 fn detailed_task_failed_roundtrip() {
515 let detailed = DetailedTask::new(
516 base_task(TaskStatus::Failed),
517 TaskPayload::Failed {
518 error: serde_json::from_value(json!({
519 "code": -32603,
520 "message": "boom"
521 }))
522 .unwrap(),
523 },
524 );
525 let value = serde_json::to_value(&detailed).unwrap();
526 assert_eq!(value["status"], "failed");
527 assert_eq!(value["error"]["code"], -32603);
528 let roundtrip: DetailedTask = serde_json::from_value(value).unwrap();
529 assert_eq!(roundtrip, detailed);
530 }
531
532 #[test]
533 fn get_task_result_flattens_detailed_task() {
534 let result = GetTaskResult::new(DetailedTask::new(
535 base_task(TaskStatus::Working),
536 TaskPayload::Working,
537 ));
538 let value = serde_json::to_value(&result).unwrap();
539 assert_eq!(value["taskId"], "task-1");
540 assert_eq!(value["status"], "working");
541 let roundtrip: GetTaskResult = serde_json::from_value(value).unwrap();
542 assert_eq!(roundtrip, result);
543 }
544}