Skip to main content

task_graph_mcp/resources/
mod.rs

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