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