1use std::collections::HashMap;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Condvar, Mutex};
12use std::time::Duration;
13
14use super::{json_escape, json_rpc, McpContent, McpContext, McpServer};
15use crate::mcp::modern::InputRequired;
16
17#[doc(hidden)]
19pub const EXTENSION: &str = "io.modelcontextprotocol/tasks";
20const MAX_TASKS: usize = 10_000;
22const POLL_INTERVAL_MS: u64 = 1000;
23pub const DEFAULT_MAX_RUNNING_TASKS: usize = 256;
25
26struct RunningSlot(Arc<std::sync::atomic::AtomicUsize>);
28
29impl Drop for RunningSlot {
30 fn drop(&mut self) {
31 self.0.fetch_sub(1, Ordering::AcqRel);
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum TaskError {
38 Tool(String),
41 Failed { code: i32, message: String },
44 Cancelled,
46}
47
48impl From<String> for TaskError {
49 fn from(message: String) -> Self {
50 TaskError::Tool(message)
51 }
52}
53
54impl From<&str> for TaskError {
55 fn from(message: &str) -> Self {
56 TaskError::Tool(message.to_string())
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61enum Status {
62 Working,
63 InputRequired,
64 Completed,
65 Failed,
66 Cancelled,
67}
68
69impl Status {
70 fn as_str(self) -> &'static str {
71 match self {
72 Status::Working => "working",
73 Status::InputRequired => "input_required",
74 Status::Completed => "completed",
75 Status::Failed => "failed",
76 Status::Cancelled => "cancelled",
77 }
78 }
79
80 fn is_terminal(self) -> bool {
81 matches!(self, Status::Completed | Status::Failed | Status::Cancelled)
82 }
83}
84
85#[derive(Debug)]
86struct State {
87 status: Status,
88 status_message: Option<String>,
89 updated_ms: u64,
90 pending: Vec<(String, String)>,
92 answers: HashMap<String, String>,
94 issued: Vec<String>,
96 payload: Option<String>,
98}
99
100#[derive(Clone)]
102#[doc(hidden)]
103pub struct TaskHandle {
104 #[doc(hidden)]
105 pub server: McpServer,
106 #[doc(hidden)]
107 pub principal: String,
108}
109
110impl std::fmt::Debug for TaskHandle {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("TaskHandle").field("principal", &self.principal).finish()
113 }
114}
115
116#[derive(Debug)]
118#[doc(hidden)]
119pub struct Task {
120 id: String,
121 principal: String,
122 created_ms: u64,
123 ttl_ms: u64,
124 state: Mutex<State>,
125 changed: Condvar,
126 cancelled: Arc<AtomicBool>,
127}
128
129#[derive(Debug, Default)]
131#[doc(hidden)]
132pub struct TaskStore {
133 tasks: Mutex<HashMap<String, Arc<Task>>>,
134}
135
136fn now_ms() -> u64 {
137 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_millis() as u64)
138}
139
140#[doc(hidden)]
142pub fn iso8601(ms: u64) -> String {
143 let (days, rest) = (ms / 86_400_000, ms % 86_400_000);
144 let z = days as i64 + 719_468;
145 let era = z.div_euclid(146_097);
146 let doe = z - era * 146_097;
147 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
148 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
149 let mp = (5 * doy + 2) / 153;
150 let day = doy - (153 * mp + 2) / 5 + 1;
151 let month = if mp < 10 { mp + 3 } else { mp - 9 };
152 let year = yoe + era * 400 + i64::from(month <= 2);
153 let secs = rest / 1000;
154 format!("{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}.{:03}Z", secs / 3600, secs % 3600 / 60, secs % 60, rest % 1000)
155}
156
157fn task_id() -> String {
158 super::modern::random_key()[..16].iter().map(|b| format!("{b:02x}")).collect()
159}
160
161impl Task {
162 fn detail_json(&self) -> String {
164 let state = self.state.lock().unwrap_or_else(|e| e.into_inner());
165 let mut json = format!(
166 r#""taskId":"{}","status":"{}","createdAt":"{}","lastUpdatedAt":"{}","ttlMs":{},"pollIntervalMs":{POLL_INTERVAL_MS}"#,
167 self.id,
168 state.status.as_str(),
169 iso8601(self.created_ms),
170 iso8601(state.updated_ms),
171 self.ttl_ms,
172 );
173 if let Some(message) = &state.status_message {
174 json.push_str(&format!(r#","statusMessage":"{}""#, json_escape(message)));
175 }
176 match state.status {
177 Status::InputRequired => {
178 let requests: Vec<String> = state.pending.iter().map(|(key, request)| format!(r#""{}":{request}"#, json_escape(key))).collect();
179 json.push_str(&format!(r#","inputRequests":{{{}}}"#, requests.join(",")));
180 }
181 Status::Completed => json.push_str(&format!(r#","result":{}"#, state.payload.as_deref().unwrap_or("{}"))),
182 Status::Failed => json.push_str(&format!(r#","error":{}"#, state.payload.as_deref().unwrap_or(r#"{"code":-32603,"message":"failed"}"#))),
183 Status::Working | Status::Cancelled => {}
184 }
185 json
186 }
187
188 fn set(&self, store: &McpServer, change: impl FnOnce(&mut State)) {
189 {
190 let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
191 if state.status.is_terminal() {
192 return;
193 }
194 change(&mut state);
195 state.updated_ms = now_ms();
196 }
197 self.changed.notify_all();
198 let params = format!("{{{}}}", self.detail_json());
199 store.deliver_task_notification(&self.id, ¶ms);
200 }
201}
202
203pub struct TaskContext {
206 task: Option<(Arc<Task>, McpServer)>,
207}
208
209impl TaskContext {
210 pub fn is_task(&self) -> bool {
213 self.task.is_some()
214 }
215
216 pub fn is_cancelled(&self) -> bool {
218 self.task.as_ref().is_some_and(|(task, _)| task.cancelled.load(Ordering::Relaxed))
219 }
220
221 pub fn status_message(&self, message: &str) {
223 if let Some((task, server)) = &self.task {
224 task.set(server, |state| state.status_message = Some(message.to_string()));
225 }
226 }
227
228 pub fn input(&self, request: InputRequired) -> Result<Vec<(String, String)>, TaskError> {
234 let Some((task, server)) = &self.task else {
235 return Err(TaskError::Tool("this tool asks for input during a task, which needs the client's io.modelcontextprotocol/tasks extension".to_string()));
236 };
237 let requests = request.requests_json();
238 {
239 let state = task.state.lock().unwrap_or_else(|e| e.into_inner());
240 if let Some((key, _)) = requests.iter().find(|(key, _)| state.issued.contains(key)) {
241 return Err(TaskError::Failed { code: json_rpc::INTERNAL_ERROR, message: format!("input key {key:?} was already used by this task") });
242 }
243 }
244 let keys: Vec<String> = requests.iter().map(|(key, _)| key.clone()).collect();
245 task.set(server, |state| {
246 state.issued.extend(keys.iter().cloned());
247 state.answers.clear();
248 state.pending = requests;
249 state.status = Status::InputRequired;
250 });
251 let mut state = task.state.lock().unwrap_or_else(|e| e.into_inner());
252 loop {
253 if task.cancelled.load(Ordering::Relaxed) || state.status == Status::Cancelled {
254 return Err(TaskError::Cancelled);
255 }
256 if state.pending.is_empty() && state.status == Status::Working {
257 return Ok(keys.iter().map(|key| (key.clone(), state.answers.get(key).cloned().unwrap_or_default())).collect());
258 }
259 let (next, _) = task.changed.wait_timeout(state, Duration::from_secs(1)).unwrap_or_else(|e| e.into_inner());
260 state = next;
261 }
262 }
263}
264
265impl McpContext {
266 pub fn start_task<F>(&self, work: F) -> Result<McpContent, String>
288 where
289 F: FnOnce(TaskContext) -> Result<McpContent, TaskError> + Send + 'static,
290 {
291 let Some(TaskHandle { server, principal }) = self.tasks.clone() else {
292 return match work(TaskContext { task: None }) {
293 Ok(content) => Ok(content),
294 Err(TaskError::Tool(message)) | Err(TaskError::Failed { message, .. }) => Err(message),
295 Err(TaskError::Cancelled) => Err("cancelled".to_string()),
296 };
297 };
298 let running = Arc::clone(&server.running_tasks);
300 if running.fetch_add(1, Ordering::AcqRel) >= server.max_running_tasks {
301 running.fetch_sub(1, Ordering::AcqRel);
302 return Err(format!("too many tasks running (at most {}); try again later", server.max_running_tasks));
303 }
304 let slot = RunningSlot(running);
305 let now = now_ms();
306 let task = Arc::new(Task {
307 id: task_id(),
308 principal,
309 created_ms: now,
310 ttl_ms: server.task_ttl_ms,
311 state: Mutex::new(State {
312 status: Status::Working,
313 status_message: None,
314 updated_ms: now,
315 pending: Vec::new(),
316 answers: HashMap::new(),
317 issued: Vec::new(),
318 payload: None,
319 }),
320 changed: Condvar::new(),
321 cancelled: Arc::new(AtomicBool::new(false)),
322 });
323 server.task_store.insert(Arc::clone(&task));
324 let id = task.id.clone();
325 std::thread::spawn(move || {
326 let _slot = slot;
327 let context = TaskContext { task: Some((Arc::clone(&task), server.clone())) };
328 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(context)));
329 task.set(&server, |state| {
330 state.pending.clear();
331 let (status, payload) = match outcome {
332 Ok(Ok(content)) if content.kind == "input_required" || content.kind == "task" => (
333 Status::Failed,
334 format!(r#"{{"code":{},"message":"a task's work can't return {}"}}"#, json_rpc::INTERNAL_ERROR, content.kind),
335 ),
336 Ok(Ok(content)) => (Status::Completed, McpServer::format_tool_result(Ok(content))),
337 Ok(Err(TaskError::Tool(message))) => (Status::Completed, McpServer::format_tool_result(Err(message))),
338 Ok(Err(TaskError::Failed { code, message })) => {
339 state.status_message = Some(message.clone());
340 (Status::Failed, format!(r#"{{"code":{code},"message":"{}"}}"#, json_escape(&message)))
341 }
342 Ok(Err(TaskError::Cancelled)) => (Status::Cancelled, String::new()),
343 Err(_) => {
344 state.status_message = Some("the task's work panicked".to_string());
345 (Status::Failed, format!(r#"{{"code":{},"message":"the task's work panicked"}}"#, json_rpc::INTERNAL_ERROR))
346 }
347 };
348 state.status = status;
349 state.payload = (!payload.is_empty()).then_some(payload);
350 });
351 });
352 Ok(McpContent { kind: "task", text: id, mime_type: None, uri: None })
353 }
354
355 pub fn supports_tasks(&self) -> bool {
357 self.tasks.is_some()
358 }
359}
360
361impl TaskStore {
362 fn insert(&self, task: Arc<Task>) {
363 let mut tasks = self.tasks.lock().unwrap_or_else(|e| e.into_inner());
364 let now = now_ms();
365 tasks.retain(|_, t| now <= t.created_ms + t.ttl_ms);
366 if tasks.len() >= MAX_TASKS {
367 let finished = |t: &&Arc<Task>| t.state.lock().map(|s| s.status.is_terminal()).unwrap_or(true);
369 let oldest = tasks
370 .values()
371 .filter(finished)
372 .min_by_key(|t| t.created_ms)
373 .or_else(|| tasks.values().min_by_key(|t| t.created_ms))
374 .map(|t| t.id.clone());
375 if let Some(id) = oldest {
376 tasks.remove(&id);
377 }
378 }
379 tasks.insert(task.id.clone(), task);
380 }
381
382 fn get(&self, id: &str, principal: &str) -> Option<Arc<Task>> {
384 let tasks = self.tasks.lock().unwrap_or_else(|e| e.into_inner());
385 tasks.get(id).filter(|t| t.principal == principal && now_ms() <= t.created_ms + t.ttl_ms).cloned()
386 }
387}
388
389impl McpServer {
390 pub fn task_ttl(mut self, ttl: Duration) -> Self {
392 self.task_ttl_ms = ttl.as_millis() as u64;
393 self
394 }
395
396 pub fn max_running_tasks(mut self, n: usize) -> Self {
400 self.max_running_tasks = n;
401 self
402 }
403
404 pub fn task_required(self, name: &str) -> Self {
407 self.task_required_tools.write().unwrap().push(name.to_string());
408 self
409 }
410
411 #[doc(hidden)]
412 pub fn is_task_required(&self, name: &str) -> bool {
413 self.task_required_tools.read().unwrap().iter().any(|n| n == name)
414 }
415
416 #[doc(hidden)]
418 pub fn create_task_fields(&self, id: &str, principal: &str) -> Option<String> {
419 self.task_store.get(id, principal).map(|task| task.detail_json())
420 }
421
422 #[doc(hidden)]
424 pub fn task_get(&self, id: &str, principal: &str) -> Result<String, (i32, String)> {
425 self.task_store.get(id, principal).map(|task| task.detail_json()).ok_or_else(not_found)
426 }
427
428 #[doc(hidden)]
431 pub fn task_update(&self, id: &str, principal: &str, responses: &str) -> Result<(), (i32, String)> {
432 let task = self.task_store.get(id, principal).ok_or_else(not_found)?;
433 let answers = json_rpc::object_members(responses).ok_or((json_rpc::INVALID_PARAMS, "inputResponses must be an object".to_string()))?;
434 task.set(self, |state| {
435 for (key, answer) in answers {
436 if let Some(at) = state.pending.iter().position(|(k, _)| *k == key) {
437 state.pending.remove(at);
438 state.answers.insert(key, answer);
439 }
440 }
441 if state.pending.is_empty() && state.status == Status::InputRequired {
442 state.status = Status::Working;
443 }
444 });
445 Ok(())
446 }
447
448 #[doc(hidden)]
451 pub fn task_cancel(&self, id: &str, principal: &str) -> Result<(), (i32, String)> {
452 let task = self.task_store.get(id, principal).ok_or_else(not_found)?;
453 task.cancelled.store(true, Ordering::Relaxed);
454 task.set(self, |state| {
455 state.status = Status::Cancelled;
456 state.pending.clear();
457 state.status_message = Some("cancelled by the client".to_string());
458 });
459 Ok(())
460 }
461}
462
463fn not_found() -> (i32, String) {
464 (json_rpc::INVALID_PARAMS, "Failed to retrieve task: Task not found".to_string())
465}
466
467#[cfg(test)]
468mod tests;