Skip to main content

agentmux_mcp/tools/
cron.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Scheduled prompts (Cron*).
5
6use super::*;
7
8pub(super) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
9    let ToolCtx { local_url, auth_key, block_id, client, .. } = *cx;
10    match name {
11        "CronCreate" => {
12            require_agent_env(local_url, auth_key, block_id)?;
13            let name = arguments.get("name").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
14                .ok_or_else(|| anyhow::anyhow!("missing required parameter: name"))?;
15            let expression = arguments.get("expression").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
16                .ok_or_else(|| anyhow::anyhow!("missing required parameter: expression"))?;
17            let prompt = arguments.get("prompt").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
18                .ok_or_else(|| anyhow::anyhow!("missing required parameter: prompt"))?;
19            let target = arguments.get("to").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
20                .ok_or_else(|| anyhow::anyhow!("missing required parameter: to (target agent id)"))?;
21            let max_fires = arguments.get("max_fires").and_then(|v| v.as_i64()).filter(|&n| n > 0);
22            let max_age_secs = arguments.get("max_age_secs").and_then(|v| v.as_i64()).filter(|&n| n > 0);
23            let self_id = std::env::var("AGENTMUX_AGENT_ID").ok().filter(|s| !s.is_empty()).unwrap_or_default();
24            // Identity M3 (spec §5.4): resolve the target while the author is
25            // present; the job then fires by uid, never by resolving a name.
26            let target_uid = resolve_agent_name_at_boundary(&client, local_url, auth_key, target).await?;
27
28            let url = format!("{}/agentmux/cron", local_url.trim_end_matches('/'));
29            let body = serde_json::json!({
30                "name": name, "expression": expression, "prompt": prompt,
31                "target": target, "target_uid": target_uid.unwrap_or_default(),
32                "created_by": self_id, "max_fires": max_fires,
33                "max_age_secs": max_age_secs,
34            });
35            let resp = client.post(&url).header(AUTH_KEY_HEADER, auth_key).json(&body).send().await
36                .map_err(|e| anyhow::anyhow!("cron create request failed: {e}"))?;
37            let status = resp.status();
38            let text = resp.text().await.unwrap_or_default();
39            if !status.is_success() {
40                anyhow::bail!("CronCreate failed: HTTP {status} — {text}");
41            }
42            let v: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
43            let job = &v["job"];
44            Ok(format!(
45                "Created cron job '{}' (id={})\nExpression: {} UTC\nNext fire: {}\nTarget: {}",
46                job["name"].as_str().unwrap_or(name),
47                job["id"].as_str().unwrap_or("?"),
48                expression,
49                job["next_fire"].as_str().unwrap_or("unknown"),
50                target,
51            ))
52        }
53        "CronDelete" => {
54            require_agent_env(local_url, auth_key, block_id)?;
55            let id = arguments.get("id").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
56                .ok_or_else(|| anyhow::anyhow!("missing required parameter: id"))?;
57            let url = format!("{}/agentmux/cron/{}", local_url.trim_end_matches('/'), id);
58            let resp = client.delete(&url).header(AUTH_KEY_HEADER, auth_key).send().await
59                .map_err(|e| anyhow::anyhow!("cron delete request failed: {e}"))?;
60            let status = resp.status();
61            if status.as_u16() == 404 {
62                return Ok(format!("Job '{id}' not found (already deleted or wrong id)"));
63            }
64            if !status.is_success() {
65                let text = resp.text().await.unwrap_or_default();
66                anyhow::bail!("CronDelete failed: HTTP {status} — {text}");
67            }
68            Ok(format!("Deleted cron job {id}"))
69        }
70        "CronList" => {
71            require_agent_env(local_url, auth_key, block_id)?;
72            let url = format!("{}/agentmux/cron", local_url.trim_end_matches('/'));
73            let resp = client.get(&url).header(AUTH_KEY_HEADER, auth_key).send().await
74                .map_err(|e| anyhow::anyhow!("cron list request failed: {e}"))?;
75            let status = resp.status();
76            let text = resp.text().await.unwrap_or_default();
77            if !status.is_success() {
78                anyhow::bail!("CronList failed: HTTP {status} — {text}");
79            }
80            let v: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
81            let jobs = v["jobs"].as_array().cloned().unwrap_or_default();
82            if jobs.is_empty() {
83                return Ok("No cron jobs configured.".to_string());
84            }
85            let mut lines = vec![format!("{} cron job(s):", jobs.len())];
86            for j in &jobs {
87                let status_str = if j["enabled"].as_bool().unwrap_or(false) { "enabled" } else { "paused" };
88                let next = j["next_fire"].as_str().unwrap_or("—");
89                let fires = j["fire_count"].as_i64().unwrap_or(0);
90                let max = j["max_fires"].as_i64().map(|n| format!("/{n}")).unwrap_or_default();
91                let age_bound = j["expires_in_secs"].as_i64().map(|n| format!("  expires_in={n}s")).unwrap_or_default();
92                lines.push(format!(
93                    "  {}  {}  [{}]  fires={fires}{max}  next={}  expr='{}'{age_bound}",
94                    j["id"].as_str().unwrap_or("?"),
95                    j["name"].as_str().unwrap_or("?"),
96                    status_str,
97                    next,
98                    j["expression"].as_str().unwrap_or("?"),
99                ));
100            }
101            Ok(lines.join("\n"))
102        }
103        "CronPause" => {
104            require_agent_env(local_url, auth_key, block_id)?;
105            let id = arguments.get("id").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
106                .ok_or_else(|| anyhow::anyhow!("missing required parameter: id"))?;
107            cron_set_enabled(client, local_url, auth_key, id, "pause").await
108        }
109        "CronResume" => {
110            require_agent_env(local_url, auth_key, block_id)?;
111            let id = arguments.get("id").and_then(|v| v.as_str()).filter(|s| !s.is_empty())
112                .ok_or_else(|| anyhow::anyhow!("missing required parameter: id"))?;
113            cron_set_enabled(client, local_url, auth_key, id, "resume").await
114        }
115        _ => Err(not_in_family()),
116    }
117}