Skip to main content

task_graph_mcp/resources/
mod.rs

1//! MCP resource implementations.
2
3pub mod agents;
4pub mod files;
5pub mod skills;
6pub mod stats;
7pub mod tasks;
8
9use crate::config::{DependenciesConfig, StatesConfig};
10use crate::db::Database;
11use anyhow::Result;
12use rmcp::model::{Annotated, RawResourceTemplate, ResourceTemplate};
13use serde_json::Value;
14use std::sync::Arc;
15
16/// Resource handler that processes MCP resource requests.
17pub struct ResourceHandler {
18    pub db: Arc<Database>,
19    pub states_config: Arc<StatesConfig>,
20    pub deps_config: Arc<DependenciesConfig>,
21    /// Directory for skill overrides (e.g., `.task-graph/skills/`)
22    pub skills_dir: Option<std::path::PathBuf>,
23}
24
25impl ResourceHandler {
26    pub fn new(
27        db: Arc<Database>,
28        states_config: Arc<StatesConfig>,
29        deps_config: Arc<DependenciesConfig>,
30    ) -> Self {
31        Self {
32            db,
33            states_config,
34            deps_config,
35            skills_dir: None,
36        }
37    }
38
39    /// Set the skills override directory.
40    pub fn with_skills_dir(mut self, dir: std::path::PathBuf) -> Self {
41        self.skills_dir = Some(dir);
42        self
43    }
44
45    /// Get all available resource templates.
46    pub fn get_resource_templates(&self) -> Vec<ResourceTemplate> {
47        vec![
48            Annotated::new(
49                RawResourceTemplate {
50                    uri_template: "tasks://all".into(),
51                    name: "All Tasks".into(),
52                    title: None,
53                    description: Some("Full task graph with dependencies".into()),
54                    mime_type: Some("application/json".into()),
55                    icons: None,
56                },
57                None,
58            ),
59            Annotated::new(
60                RawResourceTemplate {
61                    uri_template: "tasks://ready".into(),
62                    name: "Ready Tasks".into(),
63                    title: None,
64                    description: Some("Tasks ready to claim".into()),
65                    mime_type: Some("application/json".into()),
66                    icons: None,
67                },
68                None,
69            ),
70            Annotated::new(
71                RawResourceTemplate {
72                    uri_template: "tasks://blocked".into(),
73                    name: "Blocked Tasks".into(),
74                    title: None,
75                    description: Some("Tasks blocked by dependencies".into()),
76                    mime_type: Some("application/json".into()),
77                    icons: None,
78                },
79                None,
80            ),
81            Annotated::new(
82                RawResourceTemplate {
83                    uri_template: "tasks://claimed".into(),
84                    name: "Claimed Tasks".into(),
85                    title: None,
86                    description: Some("All claimed tasks".into()),
87                    mime_type: Some("application/json".into()),
88                    icons: None,
89                },
90                None,
91            ),
92            Annotated::new(
93                RawResourceTemplate {
94                    uri_template: "tasks://agent/{agent_id}".into(),
95                    name: "Agent Tasks".into(),
96                    title: None,
97                    description: Some("Tasks owned by an agent".into()),
98                    mime_type: Some("application/json".into()),
99                    icons: None,
100                },
101                None,
102            ),
103            Annotated::new(
104                RawResourceTemplate {
105                    uri_template: "tasks://tree/{task_id}".into(),
106                    name: "Task Tree".into(),
107                    title: None,
108                    description: Some("Task with all descendants".into()),
109                    mime_type: Some("application/json".into()),
110                    icons: None,
111                },
112                None,
113            ),
114            Annotated::new(
115                RawResourceTemplate {
116                    uri_template: "files://marks".into(),
117                    name: "File Marks".into(),
118                    title: None,
119                    description: Some("All advisory file marks".into()),
120                    mime_type: Some("application/json".into()),
121                    icons: None,
122                },
123                None,
124            ),
125            Annotated::new(
126                RawResourceTemplate {
127                    uri_template: "agents://all".into(),
128                    name: "All Agents".into(),
129                    title: None,
130                    description: Some("Registered agents".into()),
131                    mime_type: Some("application/json".into()),
132                    icons: None,
133                },
134                None,
135            ),
136            Annotated::new(
137                RawResourceTemplate {
138                    uri_template: "plan://acp".into(),
139                    name: "ACP Plan".into(),
140                    title: None,
141                    description: Some("ACP-compatible plan export".into()),
142                    mime_type: Some("application/json".into()),
143                    icons: None,
144                },
145                None,
146            ),
147            Annotated::new(
148                RawResourceTemplate {
149                    uri_template: "stats://summary".into(),
150                    name: "Stats Summary".into(),
151                    title: None,
152                    description: Some("Aggregate statistics".into()),
153                    mime_type: Some("application/json".into()),
154                    icons: None,
155                },
156                None,
157            ),
158            // Skills resources
159            Annotated::new(
160                RawResourceTemplate {
161                    uri_template: "skills://list".into(),
162                    name: "Available Skills".into(),
163                    title: None,
164                    description: Some("List all bundled task-graph skills".into()),
165                    mime_type: Some("application/json".into()),
166                    icons: None,
167                },
168                None,
169            ),
170            Annotated::new(
171                RawResourceTemplate {
172                    uri_template: "skills://{name}".into(),
173                    name: "Skill Content".into(),
174                    title: None,
175                    description: Some("Get a specific skill (basics, coordinator, worker, reporting, migration, repair)".into()),
176                    mime_type: Some("text/markdown".into()),
177                    icons: None,
178                },
179                None,
180            ),
181        ]
182    }
183
184    /// Read a resource by URI.
185    pub async fn read_resource(&self, uri: &str) -> Result<Value> {
186        // Parse the URI
187        if uri.starts_with("tasks://") {
188            self.read_tasks_resource(uri).await
189        } else if uri.starts_with("files://") {
190            self.read_files_resource(uri).await
191        } else if uri.starts_with("agents://") {
192            self.read_agents_resource(uri).await
193        } else if uri.starts_with("plan://") {
194            self.read_plan_resource(uri).await
195        } else if uri.starts_with("stats://") {
196            self.read_stats_resource(uri).await
197        } else if uri.starts_with("skills://") {
198            self.read_skills_resource(uri).await
199        } else {
200            Err(anyhow::anyhow!("Unknown resource URI: {}", uri))
201        }
202    }
203
204    async fn read_tasks_resource(&self, uri: &str) -> Result<Value> {
205        let path = uri.strip_prefix("tasks://").unwrap_or("");
206
207        match path {
208            "all" => tasks::get_all_tasks(&self.db),
209            "ready" => tasks::get_ready_tasks(&self.db, &self.states_config, &self.deps_config),
210            "blocked" => tasks::get_blocked_tasks(&self.db, &self.states_config, &self.deps_config),
211            "claimed" => tasks::get_claimed_tasks(&self.db, None),
212            _ if path.starts_with("agent/") => {
213                let agent_id = path.strip_prefix("agent/").unwrap();
214                tasks::get_claimed_tasks(&self.db, Some(agent_id))
215            }
216            _ if path.starts_with("tree/") => {
217                let task_id = path.strip_prefix("tree/").unwrap();
218                tasks::get_task_tree(&self.db, task_id)
219            }
220            _ => Err(anyhow::anyhow!("Unknown tasks resource: {}", path)),
221        }
222    }
223
224    async fn read_files_resource(&self, uri: &str) -> Result<Value> {
225        let path = uri.strip_prefix("files://").unwrap_or("");
226
227        match path {
228            "marks" => files::get_all_file_locks(&self.db),
229            _ => Err(anyhow::anyhow!("Unknown files resource: {}", path)),
230        }
231    }
232
233    async fn read_agents_resource(&self, uri: &str) -> Result<Value> {
234        let path = uri.strip_prefix("agents://").unwrap_or("");
235
236        match path {
237            "all" => agents::get_all_workers(&self.db),
238            _ => Err(anyhow::anyhow!("Unknown agents resource: {}", path)),
239        }
240    }
241
242    async fn read_plan_resource(&self, uri: &str) -> Result<Value> {
243        let path = uri.strip_prefix("plan://").unwrap_or("");
244
245        match path {
246            "acp" => stats::get_acp_plan(&self.db),
247            _ => Err(anyhow::anyhow!("Unknown plan resource: {}", path)),
248        }
249    }
250
251    async fn read_stats_resource(&self, uri: &str) -> Result<Value> {
252        let path = uri.strip_prefix("stats://").unwrap_or("");
253
254        match path {
255            "summary" => stats::get_stats_summary(&self.db, &self.states_config),
256            _ => Err(anyhow::anyhow!("Unknown stats resource: {}", path)),
257        }
258    }
259
260    async fn read_skills_resource(&self, uri: &str) -> Result<Value> {
261        let path = uri.strip_prefix("skills://").unwrap_or("");
262        let skills_dir = self.skills_dir.as_deref();
263
264        match path {
265            "list" => skills::list_skills(skills_dir),
266            name => skills::get_skill_resource(skills_dir, name),
267        }
268    }
269}