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