Skip to main content

tower_mcp/
async_task.rs

1//! Async task management for long-running MCP operations
2//!
3//! This module provides task lifecycle management for operations that may take
4//! longer than a typical request/response cycle. Legacy clients request task
5//! augmentation explicitly; final-protocol servers elect tasks after extension
6//! negotiation. Tasks can be tracked, polled, updated with input, and cancelled.
7//!
8//! Task state lives behind the pluggable [`TaskStore`] trait, mirroring the
9//! shape of [`crate::session_store`] and [`crate::event_store`]: a trait, an
10//! error enum, and an in-memory default. By default routers use
11//! [`MemoryTaskStore`], which keeps tasks in an in-process map (behavior
12//! identical to earlier versions). External stores (Redis, Postgres, etc.) can
13//! be plugged in so `tasks/get` works on any instance behind a load balancer
14//! in the sessionless 2026-07-28 flows (SEP-2663).
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! use std::sync::Arc;
20//! use tower_mcp::async_task::{MemoryTaskStore, TaskStore};
21//! use tower_mcp::McpRouter;
22//!
23//! let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::new());
24//! let router = McpRouter::new().task_store(store);
25//! ```
26//!
27//! See `examples/tasks.rs` for a runnable server.
28//!
29//! # Authorization
30//!
31//! SEP-2663 requires servers to authorize every task request, and warns that a
32//! task ID can act as a bearer token: whoever holds it can poll, update, or
33//! cancel the task. This module answers that in two layers.
34//!
35//! [`generate_task_id`] draws 128 bits from the system CSPRNG, so IDs cannot
36//! be enumerated or guessed. That only protects IDs nobody has seen, so each
37//! task also records the principal that created it (see [`TaskOwner`]), and
38//! every later operation must match under [`owner_matches`].
39//!
40//! Matching is equality, not "protect owned tasks and leave unowned ones
41//! open":
42//!
43//! | Task owner | Caller  | Result                                |
44//! |------------|---------|---------------------------------------|
45//! | none       | none    | allowed, no authentication configured |
46//! | `alice`    | `alice` | allowed                               |
47//! | `alice`    | `bob`   | denied                                |
48//! | `alice`    | none    | denied                                |
49//! | none       | `alice` | denied                                |
50//!
51//! The last row is deliberate. An unowned task can only exist if it was
52//! created with no authenticated context, so a request that now carries a
53//! principal is a different security context rather than an upgrade of the
54//! same one. Servers mixing public and authenticated paths (see
55//! [`AuthConfig::public_path`](crate::auth::AuthConfig::public_path)) should
56//! expect a task created anonymously to be unreachable once a token is
57//! presented.
58//!
59//! The principal comes from the OAuth `sub` claim that the HTTP and WebSocket
60//! transports bridge into request extensions. Without the `oauth` feature
61//! there is no principal, so every task is unowned and servers with no
62//! authentication behave as they did before ownership existed.
63//!
64//! ## Why a denial looks like a missing task
65//!
66//! A refused operation returns exactly what an unknown task returns: `-32602`
67//! with "Task not found".
68//!
69//! SEP-2663 mandates `-32602` for an invalid or nonexistent task ID, but
70//! leaves the authorization failure to the server: tasks should be bound to
71//! "some sort of authorization context, the implementation of which is left to
72//! individual servers according to their existing bespoke permission models".
73//! Reusing `-32602` is therefore tower-mcp policy, not a spec requirement.
74//!
75//! The reasoning is that answering "forbidden" would confirm the ID is real,
76//! which is what unguessable IDs exist to prevent. The same SEP notes that
77//! where binding is impossible "the task ID becomes the only line of defense
78//! against contamination". A server that prefers a distinguishable error can
79//! wrap the router and translate.
80//!
81//! Expiry follows the same rule: [`Task::is_expired`] runs from creation, and
82//! an expired task reads as absent rather than as expired, so a retention
83//! window cannot be probed either.
84//!
85//! # Status notifications
86//!
87//! A client may watch a task instead of polling it, by naming its ID in the
88//! `taskIds` filter of a `subscriptions/listen` stream. Each
89//! `notifications/tasks` carries the complete task, identical to the
90//! `tasks/get` response at that moment, so a client that hears about a
91//! completion already holds the result.
92//!
93//! The router announces the transitions it drives. A server that drives one
94//! itself, most commonly [`TaskStore::require_input`], announces it with
95//! [`McpRouter::notify_task_status_changed`](crate::McpRouter::notify_task_status_changed).
96//!
97//! Notifications are best effort and `tasks/get` stays authoritative: a task
98//! outlives the request that created it, so there may be no subscriber at the
99//! moment a transition happens, and a client that missed one loses nothing but
100//! time.
101
102use std::collections::{BTreeSet, HashMap};
103use std::fmt::Write as _;
104use std::sync::atomic::{AtomicBool, Ordering};
105use std::sync::{Arc, RwLock};
106use std::time::{Duration, Instant};
107
108use async_trait::async_trait;
109
110use crate::error::JsonRpcError;
111use crate::protocol::{CallToolResult, InputRequests, InputResponses, TaskObject, TaskStatus};
112
113/// Default time-to-live for a task (5 minutes, in milliseconds).
114///
115/// Per SEP-2663 the TTL runs from task creation, not from the moment the task
116/// reaches a terminal state.
117const DEFAULT_TTL_MS: u64 = 300_000;
118
119/// Default poll interval suggestion (2 seconds, in milliseconds)
120const DEFAULT_POLL_INTERVAL_MS: u64 = 2_000;
121
122/// Internal task representation with full state
123#[derive(Debug)]
124pub struct Task {
125    /// Unique task identifier
126    pub id: String,
127    /// Name of the tool being executed
128    pub tool_name: String,
129    /// Arguments passed to the tool
130    pub arguments: serde_json::Value,
131    /// Current task status
132    pub status: TaskStatus,
133    /// When the task was created
134    pub created_at: Instant,
135    /// ISO 8601 timestamp string
136    pub created_at_str: String,
137    /// ISO 8601 timestamp of last state change
138    pub last_updated_at_str: String,
139    /// Time-to-live in milliseconds (for cleanup after completion)
140    pub ttl: u64,
141    /// Suggested polling interval in milliseconds
142    pub poll_interval: u64,
143    /// Human-readable status message
144    pub status_message: Option<String>,
145    /// Protocol metadata retained across every task view.
146    pub meta: Option<serde_json::Value>,
147    /// The result of the tool call (when completed)
148    pub result: Option<CallToolResult>,
149    /// Structured execution error (when failed).
150    ///
151    /// SEP-2663 requires `tasks/get` to surface a JSON-RPC error object, not a
152    /// message string. A tool that returns `CallToolResult { isError: true }`
153    /// is a *completed* task carrying an error result, so it never sets this.
154    pub error: Option<JsonRpcError>,
155    /// Principal that created the task, or `None` when it was created
156    /// without an authenticated context.
157    ///
158    /// Never serialized: ownership is an authorization fact, not wire state.
159    pub owner: TaskOwner,
160    /// Input requests currently awaiting a client response, keyed as sent.
161    pub input_requests: InputRequests,
162    /// Keys answered by a previous `tasks/update`.
163    pub answered_input_keys: BTreeSet<String>,
164    /// Keys displaced by a later [`TaskStore::require_input`] before being
165    /// answered.
166    pub superseded_input_keys: BTreeSet<String>,
167    /// Cancellation token for aborting the task
168    pub cancellation_token: CancellationToken,
169    /// When the task reached terminal status (for TTL tracking)
170    pub completed_at: Option<Instant>,
171    /// Notified when task reaches a terminal state
172    pub completion_notify: Arc<tokio::sync::Notify>,
173}
174
175impl Task {
176    /// Create a new task
177    fn new(
178        id: String,
179        tool_name: String,
180        arguments: serde_json::Value,
181        ttl: Option<u64>,
182        owner: TaskOwner,
183    ) -> Self {
184        let cancelled = Arc::new(AtomicBool::new(false));
185        let now_str = chrono_now_iso8601();
186        Self {
187            id,
188            tool_name,
189            arguments,
190            status: TaskStatus::Working,
191            created_at: Instant::now(),
192            created_at_str: now_str.clone(),
193            last_updated_at_str: now_str,
194            ttl: ttl.unwrap_or(DEFAULT_TTL_MS),
195            poll_interval: DEFAULT_POLL_INTERVAL_MS,
196            status_message: Some("Task started".to_string()),
197            meta: None,
198            result: None,
199            error: None,
200            owner,
201            input_requests: InputRequests::new(),
202            answered_input_keys: BTreeSet::new(),
203            superseded_input_keys: BTreeSet::new(),
204            cancellation_token: CancellationToken { cancelled },
205            completed_at: None,
206            completion_notify: Arc::new(tokio::sync::Notify::new()),
207        }
208    }
209
210    /// Convert to TaskObject for API responses
211    pub fn to_task_object(&self) -> TaskObject {
212        TaskObject {
213            task_id: self.id.clone(),
214            status: self.status,
215            status_message: self.status_message.clone(),
216            created_at: self.created_at_str.clone(),
217            last_updated_at: self.last_updated_at_str.clone(),
218            ttl: Some(self.ttl),
219            poll_interval: Some(self.poll_interval),
220            result: None,
221            error: None,
222            meta: self.meta.clone(),
223        }
224    }
225
226    /// Check if this task should be cleaned up (TTL expired).
227    ///
228    /// The clock runs from creation, per SEP-2663. A long-running task can
229    /// therefore expire while still working, which is the intended behavior:
230    /// `ttlMs` bounds how long the server retains the task, not how long it
231    /// lingers after finishing.
232    pub fn is_expired(&self) -> bool {
233        self.created_at.elapsed() > Duration::from_millis(self.ttl)
234    }
235
236    /// Outstanding input requests, if the task is waiting on the client.
237    pub fn outstanding_input_requests(&self) -> &InputRequests {
238        &self.input_requests
239    }
240
241    /// Check if the task has been cancelled
242    pub fn is_cancelled(&self) -> bool {
243        self.cancellation_token.is_cancelled()
244    }
245}
246
247/// Generate an unguessable task identifier.
248///
249/// SEP-2663 notes that a task ID can function as a bearer token: anything that
250/// knows the ID can poll, update, or cancel the task. Identifiers are therefore
251/// 128 random bits from the system CSPRNG, rendered as hex, rather than a
252/// sequential counter.
253///
254/// # Panics
255///
256/// Panics if the operating system entropy source is unavailable. A server that
257/// cannot generate unguessable identifiers must not fall back to guessable
258/// ones.
259pub fn generate_task_id() -> String {
260    let mut bytes = [0u8; 16];
261    getrandom::fill(&mut bytes).expect("system entropy source unavailable for task ID generation");
262    let mut id = String::with_capacity(2 * bytes.len());
263    for byte in bytes {
264        let _ = write!(id, "{byte:02x}");
265    }
266    id
267}
268
269/// The principal a task belongs to.
270///
271/// `None` means the task was created without an authenticated context, which
272/// is the normal case for a server with no authentication configured.
273///
274/// SEP-2663 notes that a task ID can behave as a bearer token. Recording the
275/// owner is what stops the ID from being sufficient authority on its own once
276/// a second principal learns it.
277pub type TaskOwner = Option<String>;
278
279/// Whether `principal` may act on a task owned by `owner`.
280///
281/// Matching is equality, not "protect owned tasks and leave unowned ones
282/// open". An unowned task can only exist if it was created with no
283/// authenticated context, so a request that now carries a principal is a
284/// different security context and is refused.
285pub fn owner_matches(owner: &TaskOwner, principal: Option<&str>) -> bool {
286    owner.as_deref() == principal
287}
288
289/// Outcome of applying `tasks/update.inputResponses` to a task.
290///
291/// SEP-2663 requires partial responses to be honored: keys that match an
292/// outstanding request are consumed, everything else is ignored rather than
293/// rejected, and any request left unanswered stays outstanding.
294#[derive(Debug, Clone, Default, PartialEq, Eq)]
295pub struct AppliedInputResponses {
296    /// Keys matched to an outstanding request and consumed.
297    pub accepted: BTreeSet<String>,
298    /// Keys ignored because they were never issued, were already answered, or
299    /// were superseded by a later request.
300    pub ignored: BTreeSet<String>,
301    /// Requests still awaiting a response after this update.
302    pub still_outstanding: BTreeSet<String>,
303}
304
305impl AppliedInputResponses {
306    /// Whether every outstanding request has now been answered.
307    pub fn is_complete(&self) -> bool {
308        self.still_outstanding.is_empty()
309    }
310}
311
312/// A shareable cancellation token for task management
313#[derive(Debug, Clone)]
314pub struct CancellationToken {
315    cancelled: Arc<AtomicBool>,
316}
317
318impl CancellationToken {
319    /// Check if cancellation has been requested
320    pub fn is_cancelled(&self) -> bool {
321        self.cancelled.load(Ordering::Relaxed)
322    }
323
324    /// Request cancellation
325    pub fn cancel(&self) {
326        self.cancelled.store(true, Ordering::Relaxed);
327    }
328}
329
330/// Errors returned by [`TaskStore`] implementations.
331///
332/// Mirrors the three-variant shape of
333/// [`SessionStoreError`](crate::session_store::SessionStoreError): encode and
334/// decode errors from (de)serializing task state, and catch-all backend errors
335/// from the storage layer. [`MemoryTaskStore`] never returns errors; the
336/// variants exist for external implementations.
337#[derive(Debug, thiserror::Error)]
338#[non_exhaustive]
339pub enum TaskStoreError {
340    /// Failed to encode task state (e.g. serde serialization error).
341    #[error("encode error: {0}")]
342    Encode(String),
343    /// Failed to decode task state (e.g. corrupt data in the backend).
344    #[error("decode error: {0}")]
345    Decode(String),
346    /// Backend error (e.g. connection failure, transient storage error).
347    #[error("backend error: {0}")]
348    Backend(String),
349}
350
351/// Result alias for task store operations.
352pub type Result<T> = std::result::Result<T, TaskStoreError>;
353
354/// A task's current snapshot: the task object plus any result or error
355/// captured so far.
356///
357/// The error is a structured [`JsonRpcError`] because SEP-2663 requires
358/// `tasks/get` on a failed task to return a JSON-RPC error object.
359pub type TaskSnapshot = (TaskObject, Option<CallToolResult>, Option<JsonRpcError>);
360
361/// Storage backend for async task state.
362///
363/// Implementations persist task lifecycle state keyed by task ID. The default
364/// implementation is [`MemoryTaskStore`]; external stores (Redis, Postgres,
365/// etc.) typically live in separate crates.
366///
367/// # Semantics
368///
369/// - Terminal states ([`TaskStatus::is_terminal`]) are immutable: once a task
370///   is completed, failed, or cancelled, further transitions must be rejected
371///   (`Ok(false)` from the transition methods).
372/// - An expired task is indistinguishable from an unknown one. Reads return
373///   `None` once `ttlMs` has elapsed since creation, whether or not the entry
374///   has actually been reclaimed, so callers cannot probe for the existence of
375///   a task whose retention window has closed.
376/// - [`cancel_task`](Self::cancel_task) must signal the task's
377///   [`CancellationToken`] even if the task is already terminal.
378/// - [`wait_for_completion`](Self::wait_for_completion) blocks until the task
379///   reaches a terminal state; how an implementation waits (notification,
380///   polling, pub/sub) is an implementation detail and must not leak into the
381///   trait.
382#[async_trait]
383pub trait TaskStore: Send + Sync + 'static {
384    /// Create and store a new task owned by `owner`.
385    ///
386    /// Returns the task ID and a cancellation token for the spawned work.
387    /// `owner` is the authenticated principal responsible for the task, or
388    /// `None` when the request carried no authenticated context.
389    async fn create_task(
390        &self,
391        tool_name: &str,
392        arguments: serde_json::Value,
393        ttl: Option<u64>,
394        owner: TaskOwner,
395    ) -> Result<(String, CancellationToken)>;
396
397    /// Read a task's owner.
398    ///
399    /// The outer `Option` distinguishes a known task from an unknown or
400    /// expired one; the inner [`TaskOwner`] distinguishes an owned task from
401    /// one created without an authenticated principal.
402    async fn task_owner(&self, task_id: &str) -> Result<Option<TaskOwner>>;
403
404    /// Get task object by ID. Returns `None` if unknown.
405    async fn get_task(&self, task_id: &str) -> Result<Option<TaskObject>>;
406
407    /// Persist protocol `_meta` for a task.
408    ///
409    /// The default preserves source compatibility for external stores. Stores
410    /// that want to support task preparation metadata must override it.
411    async fn set_task_meta(&self, task_id: &str, meta: serde_json::Value) -> Result<bool> {
412        let _ = (task_id, meta);
413        Ok(false)
414    }
415
416    /// Remove a task that could not finish initialization.
417    ///
418    /// The default preserves source compatibility for external stores. Stores
419    /// used with preparation callbacks should override it.
420    async fn discard_task(&self, task_id: &str) -> Result<bool> {
421        let _ = task_id;
422        Ok(false)
423    }
424
425    /// Get a task's full snapshot (task object, result, error) by ID.
426    async fn get_task_result(&self, task_id: &str) -> Result<Option<TaskSnapshot>>;
427
428    /// Wait for a task to reach a terminal state, then return its snapshot.
429    ///
430    /// If the task is already terminal, returns immediately. Otherwise blocks
431    /// until the task completes, fails, or is cancelled. Returns `None` if
432    /// the task is unknown.
433    async fn wait_for_completion(&self, task_id: &str) -> Result<Option<TaskSnapshot>>;
434
435    /// List all tasks, optionally filtered by status.
436    async fn list_tasks(&self, status_filter: Option<TaskStatus>) -> Result<Vec<TaskObject>>;
437
438    /// Mark a task as requiring input, recording the requests to be answered.
439    ///
440    /// `requests` replaces the outstanding set. Any key that was outstanding
441    /// and is not re-issued becomes superseded; a re-issued key is a fresh
442    /// question and becomes outstanding again even if previously answered.
443    ///
444    /// Returns `Ok(false)` if the task is unknown, expired, or already
445    /// terminal.
446    async fn require_input(
447        &self,
448        task_id: &str,
449        requests: InputRequests,
450        message: Option<&str>,
451    ) -> Result<bool>;
452
453    /// Read the requests a task is currently waiting on.
454    ///
455    /// Returns an empty map when the task is not `input_required`, and `None`
456    /// when the task is unknown or expired.
457    async fn outstanding_input_requests(&self, task_id: &str) -> Result<Option<InputRequests>>;
458
459    /// Apply `tasks/update.inputResponses` to a task.
460    ///
461    /// Consumes the keys that match an outstanding request and ignores the
462    /// rest. When the last outstanding request is answered the task returns to
463    /// [`TaskStatus::Working`].
464    ///
465    /// Returns `None` if the task is unknown, expired, or already terminal.
466    async fn apply_input_responses(
467        &self,
468        task_id: &str,
469        responses: InputResponses,
470    ) -> Result<Option<AppliedInputResponses>>;
471
472    /// Update a task's time-to-live, measured from creation.
473    ///
474    /// SEP-2663 allows `ttlMs` to change over a task's lifetime. Returns
475    /// `Ok(false)` if the task is unknown or already expired.
476    async fn set_ttl(&self, task_id: &str, ttl_ms: u64) -> Result<bool>;
477
478    /// Mark a task as completed with a result.
479    ///
480    /// A result carrying `isError: true` still completes the task: the tool
481    /// ran and produced a domain error, which SEP-2663 distinguishes from an
482    /// execution failure.
483    ///
484    /// Returns `Ok(false)` if the task is unknown, expired, or already
485    /// terminal.
486    async fn complete_task(&self, task_id: &str, result: CallToolResult) -> Result<bool>;
487
488    /// Mark a task as failed with a structured execution error.
489    ///
490    /// Returns `Ok(false)` if the task is unknown, expired, or already
491    /// terminal.
492    async fn fail_task(&self, task_id: &str, error: JsonRpcError) -> Result<bool>;
493
494    /// Cancel a task.
495    ///
496    /// Signals the task's [`CancellationToken`] and, if the task is not
497    /// already terminal, marks it cancelled. Returns the updated task object,
498    /// or `None` if the task is unknown.
499    async fn cancel_task(&self, task_id: &str, reason: Option<&str>) -> Result<Option<TaskObject>>;
500}
501
502/// In-memory [`TaskStore`] backed by a `HashMap`.
503///
504/// This is the default store. Suitable for single-instance deployments. For
505/// horizontal scaling, use an external store that shares state across
506/// instances. Completion wakeups for
507/// [`wait_for_completion`](TaskStore::wait_for_completion) use a per-task
508/// [`tokio::sync::Notify`], which is an implementation detail of this store.
509#[derive(Debug, Clone)]
510pub struct MemoryTaskStore {
511    tasks: Arc<RwLock<HashMap<String, Task>>>,
512}
513
514impl Default for MemoryTaskStore {
515    fn default() -> Self {
516        Self::new()
517    }
518}
519
520impl MemoryTaskStore {
521    /// Create a new task store
522    pub fn new() -> Self {
523        Self {
524            tasks: Arc::new(RwLock::new(HashMap::new())),
525        }
526    }
527
528    /// Remove expired tasks (call periodically for cleanup).
529    ///
530    /// Returns the number removed. Not part of the [`TaskStore`] trait;
531    /// external backends typically expire entries natively (e.g. Redis TTL).
532    ///
533    /// Calling this is an optimization, not a correctness requirement: reads
534    /// already treat an expired task as absent.
535    pub fn cleanup_expired(&self) -> usize {
536        if let Ok(mut tasks) = self.tasks.write() {
537            let before = tasks.len();
538            tasks.retain(|_, t| !t.is_expired());
539            before - tasks.len()
540        } else {
541            0
542        }
543    }
544
545    /// Get the number of tasks in the store
546    #[cfg(test)]
547    pub fn len(&self) -> usize {
548        if let Ok(tasks) = self.tasks.read() {
549            tasks.len()
550        } else {
551            0
552        }
553    }
554
555    /// Check if the store is empty
556    #[cfg(test)]
557    pub fn is_empty(&self) -> bool {
558        self.len() == 0
559    }
560}
561
562#[async_trait]
563impl TaskStore for MemoryTaskStore {
564    async fn create_task(
565        &self,
566        tool_name: &str,
567        arguments: serde_json::Value,
568        ttl: Option<u64>,
569        owner: TaskOwner,
570    ) -> Result<(String, CancellationToken)> {
571        let id = generate_task_id();
572        let task = Task::new(id.clone(), tool_name.to_string(), arguments, ttl, owner);
573        let token = task.cancellation_token.clone();
574
575        if let Ok(mut tasks) = self.tasks.write() {
576            tasks.insert(id.clone(), task);
577        }
578
579        Ok((id, token))
580    }
581
582    async fn get_task(&self, task_id: &str) -> Result<Option<TaskObject>> {
583        Ok(if let Ok(tasks) = self.tasks.read() {
584            tasks
585                .get(task_id)
586                .filter(|t| !t.is_expired())
587                .map(|t| t.to_task_object())
588        } else {
589            None
590        })
591    }
592
593    async fn set_task_meta(&self, task_id: &str, meta: serde_json::Value) -> Result<bool> {
594        let Ok(mut tasks) = self.tasks.write() else {
595            return Ok(false);
596        };
597        let Some(task) = tasks.get_mut(task_id).filter(|task| !task.is_expired()) else {
598            return Ok(false);
599        };
600        task.meta = Some(meta);
601        Ok(true)
602    }
603
604    async fn discard_task(&self, task_id: &str) -> Result<bool> {
605        Ok(self
606            .tasks
607            .write()
608            .ok()
609            .and_then(|mut tasks| tasks.remove(task_id))
610            .is_some())
611    }
612
613    async fn task_owner(&self, task_id: &str) -> Result<Option<TaskOwner>> {
614        Ok(if let Ok(tasks) = self.tasks.read() {
615            tasks
616                .get(task_id)
617                .filter(|t| !t.is_expired())
618                .map(|t| t.owner.clone())
619        } else {
620            None
621        })
622    }
623
624    async fn get_task_result(&self, task_id: &str) -> Result<Option<TaskSnapshot>> {
625        Ok(if let Ok(tasks) = self.tasks.read() {
626            tasks
627                .get(task_id)
628                .filter(|t| !t.is_expired())
629                .map(|t| (t.to_task_object(), t.result.clone(), t.error.clone()))
630        } else {
631            None
632        })
633    }
634
635    async fn wait_for_completion(&self, task_id: &str) -> Result<Option<TaskSnapshot>> {
636        // First check if already terminal and get the notify handle
637        let notify = {
638            let Ok(tasks) = self.tasks.read() else {
639                return Ok(None);
640            };
641            let Some(task) = tasks.get(task_id).filter(|t| !t.is_expired()) else {
642                return Ok(None);
643            };
644            if task.status.is_terminal() {
645                return Ok(Some((
646                    task.to_task_object(),
647                    task.result.clone(),
648                    task.error.clone(),
649                )));
650            }
651            task.completion_notify.clone()
652        };
653
654        // Wait for completion notification
655        notify.notified().await;
656
657        // Read the result
658        self.get_task_result(task_id).await
659    }
660
661    async fn list_tasks(&self, status_filter: Option<TaskStatus>) -> Result<Vec<TaskObject>> {
662        Ok(if let Ok(tasks) = self.tasks.read() {
663            tasks
664                .values()
665                .filter(|t| !t.is_expired())
666                .filter(|t| status_filter.is_none() || status_filter == Some(t.status))
667                .map(|t| t.to_task_object())
668                .collect()
669        } else {
670            vec![]
671        })
672    }
673
674    async fn require_input(
675        &self,
676        task_id: &str,
677        requests: InputRequests,
678        message: Option<&str>,
679    ) -> Result<bool> {
680        let Ok(mut tasks) = self.tasks.write() else {
681            return Ok(false);
682        };
683        let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
684            return Ok(false);
685        };
686        if task.status.is_terminal() {
687            return Ok(false);
688        }
689
690        // Outstanding requests the server did not re-issue are superseded.
691        for key in std::mem::take(&mut task.input_requests).into_keys() {
692            if !requests.contains_key(&key) {
693                task.superseded_input_keys.insert(key);
694            }
695        }
696        // A re-issued key is a fresh question, whatever its prior fate.
697        for key in requests.keys() {
698            task.answered_input_keys.remove(key);
699            task.superseded_input_keys.remove(key);
700        }
701
702        task.input_requests = requests;
703        task.status = TaskStatus::InputRequired;
704        task.status_message = Some(
705            message
706                .map(str::to_string)
707                .unwrap_or_else(|| "Awaiting client input".to_string()),
708        );
709        task.last_updated_at_str = chrono_now_iso8601();
710        Ok(true)
711    }
712
713    async fn outstanding_input_requests(&self, task_id: &str) -> Result<Option<InputRequests>> {
714        Ok(if let Ok(tasks) = self.tasks.read() {
715            tasks
716                .get(task_id)
717                .filter(|t| !t.is_expired())
718                .map(|t| t.input_requests.clone())
719        } else {
720            None
721        })
722    }
723
724    async fn apply_input_responses(
725        &self,
726        task_id: &str,
727        responses: InputResponses,
728    ) -> Result<Option<AppliedInputResponses>> {
729        let Ok(mut tasks) = self.tasks.write() else {
730            return Ok(None);
731        };
732        let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
733            return Ok(None);
734        };
735        if task.status.is_terminal() {
736            return Ok(None);
737        }
738
739        let mut applied = AppliedInputResponses::default();
740        for key in responses.into_keys() {
741            if task.input_requests.remove(&key).is_some() {
742                task.answered_input_keys.insert(key.clone());
743                applied.accepted.insert(key);
744            } else {
745                // Never issued, already answered, or superseded. All three are
746                // ignored rather than rejected, so a client replaying a stale
747                // update does not fail the task.
748                applied.ignored.insert(key);
749            }
750        }
751        applied.still_outstanding = task.input_requests.keys().cloned().collect();
752
753        if !applied.accepted.is_empty() {
754            task.last_updated_at_str = chrono_now_iso8601();
755        }
756        if applied.is_complete() && task.status == TaskStatus::InputRequired {
757            task.status = TaskStatus::Working;
758            task.status_message = Some("Task resumed".to_string());
759        }
760        Ok(Some(applied))
761    }
762
763    async fn set_ttl(&self, task_id: &str, ttl_ms: u64) -> Result<bool> {
764        let Ok(mut tasks) = self.tasks.write() else {
765            return Ok(false);
766        };
767        let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
768            return Ok(false);
769        };
770        task.ttl = ttl_ms;
771        task.last_updated_at_str = chrono_now_iso8601();
772        Ok(true)
773    }
774
775    async fn complete_task(&self, task_id: &str, result: CallToolResult) -> Result<bool> {
776        let Ok(mut tasks) = self.tasks.write() else {
777            return Ok(false);
778        };
779        let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
780            return Ok(false);
781        };
782        if task.status.is_terminal() {
783            return Ok(false);
784        }
785        task.status = TaskStatus::Completed;
786        task.status_message = Some("Task completed".to_string());
787        task.result = Some(result);
788        task.input_requests.clear();
789        task.completed_at = Some(Instant::now());
790        task.last_updated_at_str = chrono_now_iso8601();
791        task.completion_notify.notify_waiters();
792        Ok(true)
793    }
794
795    async fn fail_task(&self, task_id: &str, error: JsonRpcError) -> Result<bool> {
796        let Ok(mut tasks) = self.tasks.write() else {
797            return Ok(false);
798        };
799        let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
800            return Ok(false);
801        };
802        if task.status.is_terminal() {
803            return Ok(false);
804        }
805        task.status = TaskStatus::Failed;
806        task.status_message = Some(format!("Task failed: {}", error.message));
807        task.error = Some(error);
808        task.input_requests.clear();
809        task.completed_at = Some(Instant::now());
810        task.last_updated_at_str = chrono_now_iso8601();
811        task.completion_notify.notify_waiters();
812        Ok(true)
813    }
814
815    async fn cancel_task(&self, task_id: &str, reason: Option<&str>) -> Result<Option<TaskObject>> {
816        let Ok(mut tasks) = self.tasks.write() else {
817            return Ok(None);
818        };
819        let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
820            return Ok(None);
821        };
822
823        // Signal cancellation
824        task.cancellation_token.cancel();
825
826        // If not already terminal, mark as cancelled
827        if !task.status.is_terminal() {
828            task.input_requests.clear();
829            task.status = TaskStatus::Cancelled;
830            task.status_message = Some(
831                reason
832                    .map(|r| format!("Cancelled: {}", r))
833                    .unwrap_or_else(|| "Task cancelled".to_string()),
834            );
835            task.completed_at = Some(Instant::now());
836            task.last_updated_at_str = chrono_now_iso8601();
837            task.completion_notify.notify_waiters();
838        }
839        Ok(Some(task.to_task_object()))
840    }
841}
842
843/// Build the validated extension declaration for the final Tasks extension.
844///
845/// The SEP-2663 capability shape is an empty object: support is declared by
846/// the identifier's presence, with no settings to negotiate.
847pub fn tasks_extension() -> crate::ExtensionDeclaration {
848    crate::ExtensionDeclaration::empty(crate::protocol::TASKS_EXTENSION_ID)
849        .expect("the built-in Tasks extension declaration is valid")
850}
851
852impl crate::McpRouter {
853    /// Advertise final Tasks support (SEP-2663) from this server.
854    ///
855    /// Compiling the task APIs does not advertise them. A server opts in here,
856    /// and only then does the final protocol path advertise
857    /// `io.modelcontextprotocol/tasks`, elect to return tasks from ordinary
858    /// `tools/call` requests, or serve the final task methods. Legacy
859    /// 2025-11-25 task behavior is unaffected either way.
860    pub fn with_tasks(self) -> Self {
861        self.with_protocol_extension(tasks_extension())
862    }
863}
864
865impl crate::McpClientBuilder {
866    /// Declare final Tasks support (SEP-2663) from this client.
867    pub fn with_tasks(self) -> Self {
868        self.with_protocol_extension(tasks_extension())
869    }
870}
871
872impl crate::RequestContext {
873    /// Whether both peers negotiated the final Tasks extension.
874    ///
875    /// Task dispatch keys off this rather than off the protocol version: a
876    /// 2026-07-28 request from a client that did not declare the extension
877    /// must not receive a task.
878    pub fn supports_tasks(&self) -> bool {
879        self.negotiated_extensions()
880            .is_some_and(|extensions| extensions.contains(crate::protocol::TASKS_EXTENSION_ID))
881    }
882}
883
884/// Generate ISO 8601 timestamp for current time
885fn chrono_now_iso8601() -> String {
886    use std::time::SystemTime;
887
888    let now = SystemTime::now();
889    let duration = now
890        .duration_since(SystemTime::UNIX_EPOCH)
891        .unwrap_or_default();
892
893    let secs = duration.as_secs();
894    let millis = duration.subsec_millis();
895
896    // Simple ISO 8601 format (UTC)
897    // Calculate date/time components
898    let days = secs / 86400;
899    let remaining = secs % 86400;
900    let hours = remaining / 3600;
901    let remaining = remaining % 3600;
902    let minutes = remaining / 60;
903    let seconds = remaining % 60;
904
905    // Calculate year/month/day from days since epoch (1970-01-01)
906    // This is a simplified calculation that handles leap years
907    let mut year = 1970i32;
908    let mut remaining_days = days as i32;
909
910    loop {
911        let days_in_year = if is_leap_year(year) { 366 } else { 365 };
912        if remaining_days < days_in_year {
913            break;
914        }
915        remaining_days -= days_in_year;
916        year += 1;
917    }
918
919    let days_in_months: [i32; 12] = if is_leap_year(year) {
920        [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
921    } else {
922        [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
923    };
924
925    let mut month = 1;
926    for days_in_month in days_in_months.iter() {
927        if remaining_days < *days_in_month {
928            break;
929        }
930        remaining_days -= days_in_month;
931        month += 1;
932    }
933
934    let day = remaining_days + 1;
935
936    format!(
937        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
938        year, month, day, hours, minutes, seconds, millis
939    )
940}
941
942fn is_leap_year(year: i32) -> bool {
943    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949    use crate::protocol::{
950        ElicitAction, ElicitResult, InputRequest, InputResponse, ListRootsParams,
951    };
952
953    #[tokio::test]
954    async fn test_create_task() {
955        let store = MemoryTaskStore::new();
956        let (id, token) = store
957            .create_task("test-tool", serde_json::json!({"a": 1}), None, None)
958            .await
959            .unwrap();
960
961        assert!(!id.is_empty());
962        assert!(!token.is_cancelled());
963
964        let info = store
965            .get_task(&id)
966            .await
967            .unwrap()
968            .expect("task should exist");
969        assert_eq!(info.task_id, id);
970        assert_eq!(info.status, TaskStatus::Working);
971    }
972
973    #[tokio::test]
974    async fn test_task_lifecycle() {
975        let store = MemoryTaskStore::new();
976        let (id, _) = store
977            .create_task("test-tool", serde_json::json!({}), None, None)
978            .await
979            .unwrap();
980
981        // Complete task
982        assert!(
983            store
984                .complete_task(&id, CallToolResult::text("Done"))
985                .await
986                .unwrap()
987        );
988
989        let info = store.get_task(&id).await.unwrap().unwrap();
990        assert_eq!(info.status, TaskStatus::Completed);
991    }
992
993    #[tokio::test]
994    async fn test_task_cancellation() {
995        let store = MemoryTaskStore::new();
996        let (id, token) = store
997            .create_task("test-tool", serde_json::json!({}), None, None)
998            .await
999            .unwrap();
1000
1001        assert!(!token.is_cancelled());
1002
1003        let task_obj = store
1004            .cancel_task(&id, Some("User requested"))
1005            .await
1006            .unwrap();
1007        assert!(task_obj.is_some());
1008        assert_eq!(task_obj.unwrap().status, TaskStatus::Cancelled);
1009        assert!(token.is_cancelled());
1010
1011        let info = store.get_task(&id).await.unwrap().unwrap();
1012        assert_eq!(info.status, TaskStatus::Cancelled);
1013    }
1014
1015    #[tokio::test]
1016    async fn test_task_failure() {
1017        let store = MemoryTaskStore::new();
1018        let (id, _) = store
1019            .create_task("test-tool", serde_json::json!({}), None, None)
1020            .await
1021            .unwrap();
1022
1023        assert!(
1024            store
1025                .fail_task(&id, JsonRpcError::internal_error("Something went wrong"))
1026                .await
1027                .unwrap()
1028        );
1029
1030        let info = store.get_task(&id).await.unwrap().unwrap();
1031        assert_eq!(info.status, TaskStatus::Failed);
1032        assert!(info.status_message.as_ref().unwrap().contains("failed"));
1033    }
1034
1035    #[tokio::test]
1036    async fn test_list_tasks() {
1037        let store = MemoryTaskStore::new();
1038        store
1039            .create_task("tool1", serde_json::json!({}), None, None)
1040            .await
1041            .unwrap();
1042        store
1043            .create_task("tool2", serde_json::json!({}), None, None)
1044            .await
1045            .unwrap();
1046        let (id3, _) = store
1047            .create_task("tool3", serde_json::json!({}), None, None)
1048            .await
1049            .unwrap();
1050
1051        // Complete one task
1052        store
1053            .complete_task(&id3, CallToolResult::text("Done"))
1054            .await
1055            .unwrap();
1056
1057        // List all tasks
1058        let all = store.list_tasks(None).await.unwrap();
1059        assert_eq!(all.len(), 3);
1060
1061        // List only working tasks
1062        let working = store.list_tasks(Some(TaskStatus::Working)).await.unwrap();
1063        assert_eq!(working.len(), 2);
1064
1065        // List only completed tasks
1066        let completed = store.list_tasks(Some(TaskStatus::Completed)).await.unwrap();
1067        assert_eq!(completed.len(), 1);
1068    }
1069
1070    #[tokio::test]
1071    async fn test_terminal_state_immutable() {
1072        let store = MemoryTaskStore::new();
1073        let (id, _) = store
1074            .create_task("test-tool", serde_json::json!({}), None, None)
1075            .await
1076            .unwrap();
1077
1078        // Complete the task
1079        store
1080            .complete_task(&id, CallToolResult::text("Done"))
1081            .await
1082            .unwrap();
1083
1084        // Try to fail - should fail
1085        assert!(
1086            !store
1087                .fail_task(&id, JsonRpcError::internal_error("Error"))
1088                .await
1089                .unwrap()
1090        );
1091
1092        // Status should still be completed
1093        let info = store.get_task(&id).await.unwrap().unwrap();
1094        assert_eq!(info.status, TaskStatus::Completed);
1095    }
1096
1097    #[tokio::test]
1098    async fn test_task_ids_unique() {
1099        let store = MemoryTaskStore::new();
1100        let (id1, _) = store
1101            .create_task("tool", serde_json::json!({}), None, None)
1102            .await
1103            .unwrap();
1104        let (id2, _) = store
1105            .create_task("tool", serde_json::json!({}), None, None)
1106            .await
1107            .unwrap();
1108        let (id3, _) = store
1109            .create_task("tool", serde_json::json!({}), None, None)
1110            .await
1111            .unwrap();
1112
1113        assert_ne!(id1, id2);
1114        assert_ne!(id2, id3);
1115        assert_ne!(id1, id3);
1116    }
1117
1118    #[tokio::test]
1119    async fn test_get_task_result() {
1120        let store = MemoryTaskStore::new();
1121        let (id, _) = store
1122            .create_task("test-tool", serde_json::json!({}), None, None)
1123            .await
1124            .unwrap();
1125
1126        // Complete with result
1127        let result = CallToolResult::text("The result");
1128        store.complete_task(&id, result).await.unwrap();
1129
1130        let (task_obj, result, error) = store.get_task_result(&id).await.unwrap().unwrap();
1131        assert_eq!(task_obj.status, TaskStatus::Completed);
1132        assert!(result.is_some());
1133        assert!(error.is_none());
1134    }
1135
1136    #[tokio::test]
1137    async fn test_wait_for_completion_returns_terminal_snapshot() {
1138        let store = MemoryTaskStore::new();
1139        let (id, _) = store
1140            .create_task("test-tool", serde_json::json!({}), None, None)
1141            .await
1142            .unwrap();
1143
1144        // Complete the task from another task while a waiter is blocked.
1145        let waiter_store = store.clone();
1146        let waiter_id = id.clone();
1147        let waiter =
1148            tokio::spawn(async move { waiter_store.wait_for_completion(&waiter_id).await });
1149
1150        tokio::time::sleep(Duration::from_millis(10)).await;
1151        store
1152            .complete_task(&id, CallToolResult::text("Done"))
1153            .await
1154            .unwrap();
1155
1156        let (task_obj, result, error) = waiter.await.unwrap().unwrap().unwrap();
1157        assert_eq!(task_obj.status, TaskStatus::Completed);
1158        assert!(result.is_some());
1159        assert!(error.is_none());
1160    }
1161
1162    #[tokio::test]
1163    async fn dyn_task_store_object_safe() {
1164        // Compile-time check that TaskStore is object-safe.
1165        let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::new());
1166        let (id, _) = store
1167            .create_task("tool", serde_json::json!({}), None, None)
1168            .await
1169            .unwrap();
1170        assert!(store.get_task(&id).await.unwrap().is_some());
1171    }
1172
1173    #[test]
1174    fn test_iso8601_timestamp() {
1175        let ts = chrono_now_iso8601();
1176        // Basic format check
1177        assert!(ts.ends_with('Z'));
1178        assert!(ts.contains('T'));
1179        assert_eq!(ts.len(), 24); // YYYY-MM-DDTHH:MM:SS.mmmZ
1180    }
1181
1182    #[test]
1183    fn test_task_status_display() {
1184        assert_eq!(TaskStatus::Working.to_string(), "working");
1185        assert_eq!(TaskStatus::InputRequired.to_string(), "input_required");
1186        assert_eq!(TaskStatus::Completed.to_string(), "completed");
1187        assert_eq!(TaskStatus::Failed.to_string(), "failed");
1188        assert_eq!(TaskStatus::Cancelled.to_string(), "cancelled");
1189    }
1190
1191    #[test]
1192    fn test_task_status_is_terminal() {
1193        assert!(!TaskStatus::Working.is_terminal());
1194        assert!(!TaskStatus::InputRequired.is_terminal());
1195        assert!(TaskStatus::Completed.is_terminal());
1196        assert!(TaskStatus::Failed.is_terminal());
1197        assert!(TaskStatus::Cancelled.is_terminal());
1198    }
1199
1200    fn requests(keys: &[&str]) -> InputRequests {
1201        keys.iter()
1202            .map(|k| {
1203                (
1204                    k.to_string(),
1205                    InputRequest::ListRoots(ListRootsParams { meta: None }),
1206                )
1207            })
1208            .collect()
1209    }
1210
1211    fn accept(key: &str) -> (String, InputResponse) {
1212        (
1213            key.to_string(),
1214            InputResponse::Elicit(ElicitResult {
1215                action: ElicitAction::Accept,
1216                content: None,
1217                meta: None,
1218            }),
1219        )
1220    }
1221
1222    async fn working_task(store: &MemoryTaskStore, ttl: Option<u64>) -> String {
1223        store
1224            .create_task("tool", serde_json::json!({}), ttl, None)
1225            .await
1226            .unwrap()
1227            .0
1228    }
1229
1230    #[tokio::test]
1231    async fn task_ids_are_unguessable_not_sequential() {
1232        let store = MemoryTaskStore::new();
1233        let mut ids = BTreeSet::new();
1234        for _ in 0..64 {
1235            ids.insert(working_task(&store, None).await);
1236        }
1237        assert_eq!(ids.len(), 64, "task IDs collided");
1238
1239        for id in &ids {
1240            assert_eq!(id.len(), 32, "expected 128 bits of hex: {id}");
1241            assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "{id}");
1242            assert!(!id.starts_with("task-"), "sequential-looking ID: {id}");
1243        }
1244
1245        // A counter would make every ID a near-neighbor of the last. Require
1246        // the set to span a wide range of leading bytes instead.
1247        let leading: BTreeSet<&str> = ids.iter().map(|id| &id[..2]).collect();
1248        assert!(
1249            leading.len() > 32,
1250            "only {} distinct leading bytes across 64 IDs",
1251            leading.len()
1252        );
1253    }
1254
1255    #[tokio::test]
1256    async fn ttl_runs_from_creation_and_expired_tasks_read_as_absent() {
1257        let store = MemoryTaskStore::new();
1258        let id = working_task(&store, Some(0)).await;
1259
1260        // TTL of 0 expires immediately, while the task is still working, so
1261        // the clock plainly is not waiting for a terminal state.
1262        tokio::time::sleep(Duration::from_millis(5)).await;
1263
1264        assert!(store.get_task(&id).await.unwrap().is_none());
1265        assert!(store.get_task_result(&id).await.unwrap().is_none());
1266        assert!(store.list_tasks(None).await.unwrap().is_empty());
1267        assert!(
1268            store
1269                .outstanding_input_requests(&id)
1270                .await
1271                .unwrap()
1272                .is_none()
1273        );
1274        assert!(store.cancel_task(&id, None).await.unwrap().is_none());
1275        assert!(!store.set_ttl(&id, 60_000).await.unwrap());
1276        assert!(
1277            !store
1278                .complete_task(&id, CallToolResult::text("late"))
1279                .await
1280                .unwrap()
1281        );
1282    }
1283
1284    #[tokio::test]
1285    async fn ttl_is_mutable_over_the_task_lifetime() {
1286        let store = MemoryTaskStore::new();
1287        let id = working_task(&store, Some(60_000)).await;
1288
1289        assert!(store.set_ttl(&id, 120_000).await.unwrap());
1290        let task = store.get_task(&id).await.unwrap().unwrap();
1291        assert_eq!(task.ttl, Some(120_000));
1292
1293        // Shortening the window to zero retires the task immediately.
1294        assert!(store.set_ttl(&id, 0).await.unwrap());
1295        tokio::time::sleep(Duration::from_millis(5)).await;
1296        assert!(store.get_task(&id).await.unwrap().is_none());
1297    }
1298
1299    #[tokio::test]
1300    async fn require_input_records_requests_and_exposes_them() {
1301        let store = MemoryTaskStore::new();
1302        let id = working_task(&store, None).await;
1303
1304        assert!(
1305            store
1306                .require_input(&id, requests(&["approval", "region"]), Some("need input"))
1307                .await
1308                .unwrap()
1309        );
1310
1311        let task = store.get_task(&id).await.unwrap().unwrap();
1312        assert_eq!(task.status, TaskStatus::InputRequired);
1313        assert_eq!(task.status_message.as_deref(), Some("need input"));
1314
1315        let outstanding = store
1316            .outstanding_input_requests(&id)
1317            .await
1318            .unwrap()
1319            .unwrap();
1320        assert_eq!(
1321            outstanding.keys().collect::<Vec<_>>(),
1322            vec!["approval", "region"],
1323            "every outstanding request must be exposed, not just the newest"
1324        );
1325    }
1326
1327    #[tokio::test]
1328    async fn partial_input_responses_leave_the_rest_outstanding() {
1329        let store = MemoryTaskStore::new();
1330        let id = working_task(&store, None).await;
1331        store
1332            .require_input(&id, requests(&["approval", "region"]), None)
1333            .await
1334            .unwrap();
1335
1336        let applied = store
1337            .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1338            .await
1339            .unwrap()
1340            .unwrap();
1341
1342        assert_eq!(applied.accepted, ["approval".to_string()].into());
1343        assert!(applied.ignored.is_empty());
1344        assert_eq!(applied.still_outstanding, ["region".to_string()].into());
1345        assert!(!applied.is_complete());
1346
1347        // The task stays blocked while anything is unanswered.
1348        let task = store.get_task(&id).await.unwrap().unwrap();
1349        assert_eq!(task.status, TaskStatus::InputRequired);
1350        assert_eq!(
1351            store
1352                .outstanding_input_requests(&id)
1353                .await
1354                .unwrap()
1355                .unwrap()
1356                .keys()
1357                .collect::<Vec<_>>(),
1358            vec!["region"]
1359        );
1360
1361        // Answering the last one resumes the task.
1362        let applied = store
1363            .apply_input_responses(&id, [accept("region")].into_iter().collect())
1364            .await
1365            .unwrap()
1366            .unwrap();
1367        assert!(applied.is_complete());
1368        assert_eq!(
1369            store.get_task(&id).await.unwrap().unwrap().status,
1370            TaskStatus::Working
1371        );
1372    }
1373
1374    #[tokio::test]
1375    async fn unknown_answered_and_superseded_response_keys_are_ignored() {
1376        let store = MemoryTaskStore::new();
1377        let id = working_task(&store, None).await;
1378        store
1379            .require_input(&id, requests(&["approval", "stale"]), None)
1380            .await
1381            .unwrap();
1382
1383        // Answer one, then re-issue a set that drops `stale`, superseding it.
1384        store
1385            .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1386            .await
1387            .unwrap()
1388            .unwrap();
1389        store
1390            .require_input(&id, requests(&["region"]), None)
1391            .await
1392            .unwrap();
1393
1394        let applied = store
1395            .apply_input_responses(
1396                &id,
1397                [accept("never-issued"), accept("approval"), accept("stale")]
1398                    .into_iter()
1399                    .collect(),
1400            )
1401            .await
1402            .unwrap()
1403            .unwrap();
1404
1405        assert!(
1406            applied.accepted.is_empty(),
1407            "none of these keys are outstanding"
1408        );
1409        assert_eq!(
1410            applied.ignored,
1411            [
1412                "never-issued".to_string(),
1413                "approval".to_string(),
1414                "stale".to_string()
1415            ]
1416            .into(),
1417            "unknown, already-answered, and superseded keys are all ignored"
1418        );
1419        assert_eq!(applied.still_outstanding, ["region".to_string()].into());
1420        assert_eq!(
1421            store.get_task(&id).await.unwrap().unwrap().status,
1422            TaskStatus::InputRequired,
1423            "ignoring a stale update must not resume or fail the task"
1424        );
1425    }
1426
1427    #[tokio::test]
1428    async fn reissued_key_becomes_a_fresh_question() {
1429        let store = MemoryTaskStore::new();
1430        let id = working_task(&store, None).await;
1431        store
1432            .require_input(&id, requests(&["approval"]), None)
1433            .await
1434            .unwrap();
1435        store
1436            .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1437            .await
1438            .unwrap()
1439            .unwrap();
1440
1441        // The server asks the same key again: the earlier answer must not
1442        // satisfy it.
1443        store
1444            .require_input(&id, requests(&["approval"]), None)
1445            .await
1446            .unwrap();
1447        let applied = store
1448            .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1449            .await
1450            .unwrap()
1451            .unwrap();
1452        assert_eq!(applied.accepted, ["approval".to_string()].into());
1453        assert!(applied.is_complete());
1454    }
1455
1456    #[tokio::test]
1457    async fn failed_tasks_preserve_the_structured_error() {
1458        let store = MemoryTaskStore::new();
1459        let id = working_task(&store, None).await;
1460
1461        let mut error = JsonRpcError::invalid_params("bad region");
1462        error.data = Some(serde_json::json!({"field": "region"}));
1463        assert!(store.fail_task(&id, error).await.unwrap());
1464
1465        let (_, result, error) = store.get_task_result(&id).await.unwrap().unwrap();
1466        assert!(result.is_none());
1467        let error = error.expect("structured error must survive the store");
1468        assert_eq!(
1469            error.code, -32602,
1470            "the original code must not be flattened"
1471        );
1472        assert_eq!(error.message, "bad region");
1473        assert_eq!(error.data.unwrap()["field"], "region");
1474    }
1475
1476    #[tokio::test]
1477    async fn tool_error_results_complete_the_task() {
1478        let store = MemoryTaskStore::new();
1479        let id = working_task(&store, None).await;
1480
1481        let mut result = CallToolResult::text("domain failure");
1482        result.is_error = true;
1483        assert!(store.complete_task(&id, result).await.unwrap());
1484
1485        let (task, result, error) = store.get_task_result(&id).await.unwrap().unwrap();
1486        assert_eq!(
1487            task.status,
1488            TaskStatus::Completed,
1489            "isError is a domain error, not an execution failure"
1490        );
1491        assert!(result.unwrap().is_error);
1492        assert!(error.is_none(), "no JSON-RPC error accompanies isError");
1493    }
1494
1495    #[tokio::test]
1496    async fn tasks_record_their_creating_principal() {
1497        let store = MemoryTaskStore::new();
1498        let (owned, _) = store
1499            .create_task("tool", serde_json::json!({}), None, Some("alice".into()))
1500            .await
1501            .unwrap();
1502        let (unowned, _) = store
1503            .create_task("tool", serde_json::json!({}), None, None)
1504            .await
1505            .unwrap();
1506
1507        assert_eq!(
1508            store.task_owner(&owned).await.unwrap(),
1509            Some(Some("alice".to_string()))
1510        );
1511        assert_eq!(store.task_owner(&unowned).await.unwrap(), Some(None));
1512        assert_eq!(
1513            store.task_owner("does-not-exist").await.unwrap(),
1514            None,
1515            "an unknown task has no owner record at all"
1516        );
1517
1518        // Ownership is an authorization fact and must not reach the wire.
1519        let wire = serde_json::to_value(store.get_task(&owned).await.unwrap().unwrap()).unwrap();
1520        assert!(
1521            wire.get("owner").is_none(),
1522            "owner leaked to the wire: {wire}"
1523        );
1524        assert!(!wire.to_string().contains("alice"));
1525    }
1526
1527    #[test]
1528    fn owner_matching_is_equality_not_leniency() {
1529        assert!(owner_matches(&None, None), "no auth configured");
1530        assert!(owner_matches(&Some("alice".into()), Some("alice")));
1531
1532        assert!(
1533            !owner_matches(&Some("alice".into()), Some("bob")),
1534            "a different principal must not inherit the task"
1535        );
1536        assert!(
1537            !owner_matches(&Some("alice".into()), None),
1538            "dropping the token must not grant access"
1539        );
1540        assert!(
1541            !owner_matches(&None, Some("alice")),
1542            "an unowned task belongs to a different security context"
1543        );
1544    }
1545
1546    #[tokio::test]
1547    async fn terminal_states_clear_outstanding_requests() {
1548        for (label, terminate) in [("completed", true), ("cancelled", false)] {
1549            let store = MemoryTaskStore::new();
1550            let id = working_task(&store, None).await;
1551            store
1552                .require_input(&id, requests(&["approval"]), None)
1553                .await
1554                .unwrap();
1555
1556            if terminate {
1557                store
1558                    .complete_task(&id, CallToolResult::text("done"))
1559                    .await
1560                    .unwrap();
1561            } else {
1562                store.cancel_task(&id, None).await.unwrap();
1563            }
1564
1565            assert!(
1566                store
1567                    .outstanding_input_requests(&id)
1568                    .await
1569                    .unwrap()
1570                    .unwrap()
1571                    .is_empty(),
1572                "{label} task still advertises outstanding input requests"
1573            );
1574            assert!(
1575                store
1576                    .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1577                    .await
1578                    .unwrap()
1579                    .is_none(),
1580                "{label} task accepted a late input response"
1581            );
1582        }
1583    }
1584}