Skip to main content

task_graph_mcp/resources/
mod.rs

1//! MCP resource implementations.
2
3pub mod agents;
4pub mod config;
5pub mod docs;
6pub mod files;
7pub mod skills;
8pub mod stats;
9pub mod tasks;
10pub mod workflows;
11
12use crate::config::workflows::WorkflowsConfig;
13use crate::config::{DependenciesConfig, PhasesConfig, StatesConfig, TagsConfig};
14use crate::db::Database;
15use anyhow::Result;
16use rmcp::model::{Annotated, RawResource, RawResourceTemplate, Resource, ResourceTemplate};
17use serde_json::Value;
18use std::sync::Arc;
19
20/// Resource handler that processes MCP resource requests.
21pub struct ResourceHandler {
22    pub db: Arc<Database>,
23    pub states_config: Arc<StatesConfig>,
24    pub phases_config: Arc<PhasesConfig>,
25    pub deps_config: Arc<DependenciesConfig>,
26    pub tags_config: Arc<TagsConfig>,
27    pub workflows_config: Arc<WorkflowsConfig>,
28    /// Directory for skill overrides (e.g., `.task-graph/skills/`)
29    pub skills_dir: Option<std::path::PathBuf>,
30}
31
32impl ResourceHandler {
33    pub fn new(
34        db: Arc<Database>,
35        states_config: Arc<StatesConfig>,
36        phases_config: Arc<PhasesConfig>,
37        deps_config: Arc<DependenciesConfig>,
38        tags_config: Arc<TagsConfig>,
39        workflows_config: Arc<WorkflowsConfig>,
40    ) -> Self {
41        Self {
42            db,
43            states_config,
44            phases_config,
45            deps_config,
46            tags_config,
47            workflows_config,
48            skills_dir: None,
49        }
50    }
51
52    /// Set the skills override directory.
53    pub fn with_skills_dir(mut self, dir: std::path::PathBuf) -> Self {
54        self.skills_dir = Some(dir);
55        self
56    }
57
58    /// Get all available resource templates.
59    pub fn get_resource_templates(&self) -> Vec<ResourceTemplate> {
60        vec![
61            Annotated::new(
62                RawResourceTemplate {
63                    uri_template: "tasks://all".into(),
64                    name: "All Tasks".into(),
65                    title: None,
66                    description: Some("Full task graph with dependencies".into()),
67                    mime_type: Some("application/json".into()),
68                    icons: None,
69                },
70                None,
71            ),
72            Annotated::new(
73                RawResourceTemplate {
74                    uri_template: "tasks://ready".into(),
75                    name: "Ready Tasks".into(),
76                    title: None,
77                    description: Some("Tasks ready to claim".into()),
78                    mime_type: Some("application/json".into()),
79                    icons: None,
80                },
81                None,
82            ),
83            Annotated::new(
84                RawResourceTemplate {
85                    uri_template: "tasks://blocked".into(),
86                    name: "Blocked Tasks".into(),
87                    title: None,
88                    description: Some("Tasks blocked by dependencies".into()),
89                    mime_type: Some("application/json".into()),
90                    icons: None,
91                },
92                None,
93            ),
94            Annotated::new(
95                RawResourceTemplate {
96                    uri_template: "tasks://claimed".into(),
97                    name: "Claimed Tasks".into(),
98                    title: None,
99                    description: Some("All claimed tasks".into()),
100                    mime_type: Some("application/json".into()),
101                    icons: None,
102                },
103                None,
104            ),
105            Annotated::new(
106                RawResourceTemplate {
107                    uri_template: "tasks://agent/{agent_id}".into(),
108                    name: "Agent Tasks".into(),
109                    title: None,
110                    description: Some("Tasks owned by an agent".into()),
111                    mime_type: Some("application/json".into()),
112                    icons: None,
113                },
114                None,
115            ),
116            Annotated::new(
117                RawResourceTemplate {
118                    uri_template: "tasks://tree/{task_id}".into(),
119                    name: "Task Tree".into(),
120                    title: None,
121                    description: Some("Task with all descendants".into()),
122                    mime_type: Some("application/json".into()),
123                    icons: None,
124                },
125                None,
126            ),
127            Annotated::new(
128                RawResourceTemplate {
129                    uri_template: "files://marks".into(),
130                    name: "File Marks".into(),
131                    title: None,
132                    description: Some("All advisory file marks".into()),
133                    mime_type: Some("application/json".into()),
134                    icons: None,
135                },
136                None,
137            ),
138            Annotated::new(
139                RawResourceTemplate {
140                    uri_template: "agents://all".into(),
141                    name: "All Agents".into(),
142                    title: None,
143                    description: Some("Registered agents".into()),
144                    mime_type: Some("application/json".into()),
145                    icons: None,
146                },
147                None,
148            ),
149            Annotated::new(
150                RawResourceTemplate {
151                    uri_template: "plan://acp".into(),
152                    name: "ACP Plan".into(),
153                    title: None,
154                    description: Some("ACP-compatible plan export".into()),
155                    mime_type: Some("application/json".into()),
156                    icons: None,
157                },
158                None,
159            ),
160            Annotated::new(
161                RawResourceTemplate {
162                    uri_template: "stats://summary".into(),
163                    name: "Stats Summary".into(),
164                    title: None,
165                    description: Some("Aggregate statistics".into()),
166                    mime_type: Some("application/json".into()),
167                    icons: None,
168                },
169                None,
170            ),
171            // Skills resources
172            Annotated::new(
173                RawResourceTemplate {
174                    uri_template: "skills://list".into(),
175                    name: "Available Skills".into(),
176                    title: None,
177                    description: Some("List all bundled task-graph skills".into()),
178                    mime_type: Some("application/json".into()),
179                    icons: None,
180                },
181                None,
182            ),
183            Annotated::new(
184                RawResourceTemplate {
185                    uri_template: "skills://{name}".into(),
186                    name: "Skill Content".into(),
187                    title: None,
188                    description: Some("Get a specific skill (basics, coordinator, worker, reporting, migration, repair)".into()),
189                    mime_type: Some("text/markdown".into()),
190                    icons: None,
191                },
192                None,
193            ),
194            // Workflow resources
195            Annotated::new(
196                RawResourceTemplate {
197                    uri_template: "workflows://list".into(),
198                    name: "Available Workflows".into(),
199                    title: None,
200                    description: Some("List all available workflow topologies with descriptions".into()),
201                    mime_type: Some("application/json".into()),
202                    icons: None,
203                },
204                None,
205            ),
206            Annotated::new(
207                RawResourceTemplate {
208                    uri_template: "workflows://{name}".into(),
209                    name: "Workflow Details".into(),
210                    title: None,
211                    description: Some("Get detailed information about a specific workflow (states, phases, settings)".into()),
212                    mime_type: Some("application/json".into()),
213                    icons: None,
214                },
215                None,
216            ),
217            // Config resources
218            Annotated::new(
219                RawResourceTemplate {
220                    uri_template: "config://current".into(),
221                    name: "Current Configuration".into(),
222                    title: None,
223                    description: Some("All configuration (states, phases, dependencies, tags) in one response".into()),
224                    mime_type: Some("application/json".into()),
225                    icons: None,
226                },
227                None,
228            ),
229            Annotated::new(
230                RawResourceTemplate {
231                    uri_template: "config://states".into(),
232                    name: "States Configuration".into(),
233                    title: None,
234                    description: Some("Task state definitions and transitions".into()),
235                    mime_type: Some("application/json".into()),
236                    icons: None,
237                },
238                None,
239            ),
240            Annotated::new(
241                RawResourceTemplate {
242                    uri_template: "config://phases".into(),
243                    name: "Phases Configuration".into(),
244                    title: None,
245                    description: Some("Work phase definitions".into()),
246                    mime_type: Some("application/json".into()),
247                    icons: None,
248                },
249                None,
250            ),
251            Annotated::new(
252                RawResourceTemplate {
253                    uri_template: "config://dependencies".into(),
254                    name: "Dependencies Configuration".into(),
255                    title: None,
256                    description: Some("Dependency type definitions".into()),
257                    mime_type: Some("application/json".into()),
258                    icons: None,
259                },
260                None,
261            ),
262            Annotated::new(
263                RawResourceTemplate {
264                    uri_template: "config://tags".into(),
265                    name: "Tags Configuration".into(),
266                    title: None,
267                    description: Some("Tag definitions and categories".into()),
268                    mime_type: Some("application/json".into()),
269                    icons: None,
270                },
271                None,
272            ),
273        ]
274    }
275
276    /// Get all concrete resources (those without template parameters).
277    /// These are resources that can be directly accessed without any parameters.
278    pub fn get_resources(&self) -> Vec<Resource> {
279        vec![
280            Annotated::new(
281                RawResource {
282                    uri: "tasks://all".into(),
283                    name: "All Tasks".into(),
284                    title: None,
285                    description: Some("Full task graph with dependencies".into()),
286                    mime_type: Some("application/json".into()),
287                    size: None,
288                    icons: None,
289                    meta: None,
290                },
291                None,
292            ),
293            Annotated::new(
294                RawResource {
295                    uri: "tasks://ready".into(),
296                    name: "Ready Tasks".into(),
297                    title: None,
298                    description: Some("Tasks ready to claim".into()),
299                    mime_type: Some("application/json".into()),
300                    size: None,
301                    icons: None,
302                    meta: None,
303                },
304                None,
305            ),
306            Annotated::new(
307                RawResource {
308                    uri: "tasks://blocked".into(),
309                    name: "Blocked Tasks".into(),
310                    title: None,
311                    description: Some("Tasks blocked by dependencies".into()),
312                    mime_type: Some("application/json".into()),
313                    size: None,
314                    icons: None,
315                    meta: None,
316                },
317                None,
318            ),
319            Annotated::new(
320                RawResource {
321                    uri: "tasks://claimed".into(),
322                    name: "Claimed Tasks".into(),
323                    title: None,
324                    description: Some("All claimed tasks".into()),
325                    mime_type: Some("application/json".into()),
326                    size: None,
327                    icons: None,
328                    meta: None,
329                },
330                None,
331            ),
332            Annotated::new(
333                RawResource {
334                    uri: "files://marks".into(),
335                    name: "File Marks".into(),
336                    title: None,
337                    description: Some("All advisory file marks".into()),
338                    mime_type: Some("application/json".into()),
339                    size: None,
340                    icons: None,
341                    meta: None,
342                },
343                None,
344            ),
345            Annotated::new(
346                RawResource {
347                    uri: "agents://all".into(),
348                    name: "All Agents".into(),
349                    title: None,
350                    description: Some("Registered agents".into()),
351                    mime_type: Some("application/json".into()),
352                    size: None,
353                    icons: None,
354                    meta: None,
355                },
356                None,
357            ),
358            Annotated::new(
359                RawResource {
360                    uri: "plan://acp".into(),
361                    name: "ACP Plan".into(),
362                    title: None,
363                    description: Some("ACP-compatible plan export".into()),
364                    mime_type: Some("application/json".into()),
365                    size: None,
366                    icons: None,
367                    meta: None,
368                },
369                None,
370            ),
371            Annotated::new(
372                RawResource {
373                    uri: "stats://summary".into(),
374                    name: "Stats Summary".into(),
375                    title: None,
376                    description: Some("Aggregate statistics".into()),
377                    mime_type: Some("application/json".into()),
378                    size: None,
379                    icons: None,
380                    meta: None,
381                },
382                None,
383            ),
384            Annotated::new(
385                RawResource {
386                    uri: "skills://list".into(),
387                    name: "Available Skills".into(),
388                    title: None,
389                    description: Some("List all bundled task-graph skills".into()),
390                    mime_type: Some("application/json".into()),
391                    size: None,
392                    icons: None,
393                    meta: None,
394                },
395                None,
396            ),
397            Annotated::new(
398                RawResource {
399                    uri: "workflows://list".into(),
400                    name: "Available Workflows".into(),
401                    title: None,
402                    description: Some(
403                        "List all available workflow topologies with descriptions".into(),
404                    ),
405                    mime_type: Some("application/json".into()),
406                    size: None,
407                    icons: None,
408                    meta: None,
409                },
410                None,
411            ),
412            Annotated::new(
413                RawResource {
414                    uri: "config://current".into(),
415                    name: "Current Configuration".into(),
416                    title: None,
417                    description: Some(
418                        "All configuration (states, phases, dependencies, tags) in one response"
419                            .into(),
420                    ),
421                    mime_type: Some("application/json".into()),
422                    size: None,
423                    icons: None,
424                    meta: None,
425                },
426                None,
427            ),
428            Annotated::new(
429                RawResource {
430                    uri: "config://states".into(),
431                    name: "States Configuration".into(),
432                    title: None,
433                    description: Some("Task state definitions and transitions".into()),
434                    mime_type: Some("application/json".into()),
435                    size: None,
436                    icons: None,
437                    meta: None,
438                },
439                None,
440            ),
441            Annotated::new(
442                RawResource {
443                    uri: "config://phases".into(),
444                    name: "Phases Configuration".into(),
445                    title: None,
446                    description: Some("Work phase definitions".into()),
447                    mime_type: Some("application/json".into()),
448                    size: None,
449                    icons: None,
450                    meta: None,
451                },
452                None,
453            ),
454            Annotated::new(
455                RawResource {
456                    uri: "config://dependencies".into(),
457                    name: "Dependencies Configuration".into(),
458                    title: None,
459                    description: Some("Dependency type definitions".into()),
460                    mime_type: Some("application/json".into()),
461                    size: None,
462                    icons: None,
463                    meta: None,
464                },
465                None,
466            ),
467            Annotated::new(
468                RawResource {
469                    uri: "config://tags".into(),
470                    name: "Tags Configuration".into(),
471                    title: None,
472                    description: Some("Tag definitions and categories".into()),
473                    mime_type: Some("application/json".into()),
474                    size: None,
475                    icons: None,
476                    meta: None,
477                },
478                None,
479            ),
480        ]
481    }
482
483    /// Read a resource by URI.
484    pub async fn read_resource(&self, uri: &str) -> Result<Value> {
485        // Parse the URI
486        if uri.starts_with("tasks://") {
487            self.read_tasks_resource(uri).await
488        } else if uri.starts_with("files://") {
489            self.read_files_resource(uri).await
490        } else if uri.starts_with("agents://") {
491            self.read_agents_resource(uri).await
492        } else if uri.starts_with("plan://") {
493            self.read_plan_resource(uri).await
494        } else if uri.starts_with("stats://") {
495            self.read_stats_resource(uri).await
496        } else if uri.starts_with("skills://") {
497            self.read_skills_resource(uri).await
498        } else if uri.starts_with("config://") {
499            self.read_config_resource(uri).await
500        } else if uri.starts_with("workflows://") {
501            self.read_workflows_resource(uri).await
502        } else {
503            Err(anyhow::anyhow!("Unknown resource URI: {}", uri))
504        }
505    }
506
507    async fn read_tasks_resource(&self, uri: &str) -> Result<Value> {
508        let path = uri.strip_prefix("tasks://").unwrap_or("");
509
510        match path {
511            "all" => tasks::get_all_tasks(&self.db),
512            "ready" => tasks::get_ready_tasks(&self.db, &self.states_config, &self.deps_config),
513            "blocked" => tasks::get_blocked_tasks(&self.db, &self.states_config, &self.deps_config),
514            "claimed" => tasks::get_claimed_tasks(&self.db, None),
515            _ if path.starts_with("agent/") => {
516                let agent_id = path.strip_prefix("agent/").unwrap();
517                tasks::get_claimed_tasks(&self.db, Some(agent_id))
518            }
519            _ if path.starts_with("tree/") => {
520                let task_id = path.strip_prefix("tree/").unwrap();
521                tasks::get_task_tree(&self.db, task_id)
522            }
523            _ => Err(anyhow::anyhow!("Unknown tasks resource: {}", path)),
524        }
525    }
526
527    async fn read_files_resource(&self, uri: &str) -> Result<Value> {
528        let path = uri.strip_prefix("files://").unwrap_or("");
529
530        match path {
531            "marks" => files::get_all_file_locks(&self.db),
532            _ => Err(anyhow::anyhow!("Unknown files resource: {}", path)),
533        }
534    }
535
536    async fn read_agents_resource(&self, uri: &str) -> Result<Value> {
537        let path = uri.strip_prefix("agents://").unwrap_or("");
538
539        match path {
540            "all" => agents::get_all_workers(&self.db),
541            _ => Err(anyhow::anyhow!("Unknown agents resource: {}", path)),
542        }
543    }
544
545    async fn read_plan_resource(&self, uri: &str) -> Result<Value> {
546        let path = uri.strip_prefix("plan://").unwrap_or("");
547
548        match path {
549            "acp" => stats::get_acp_plan(&self.db),
550            _ => Err(anyhow::anyhow!("Unknown plan resource: {}", path)),
551        }
552    }
553
554    async fn read_stats_resource(&self, uri: &str) -> Result<Value> {
555        let path = uri.strip_prefix("stats://").unwrap_or("");
556
557        match path {
558            "summary" => stats::get_stats_summary(&self.db, &self.states_config),
559            _ => Err(anyhow::anyhow!("Unknown stats resource: {}", path)),
560        }
561    }
562
563    async fn read_skills_resource(&self, uri: &str) -> Result<Value> {
564        let path = uri.strip_prefix("skills://").unwrap_or("");
565        let skills_dir = self.skills_dir.as_deref();
566
567        match path {
568            "list" => skills::list_skills(skills_dir),
569            name => skills::get_skill_resource(skills_dir, name),
570        }
571    }
572
573    async fn read_config_resource(&self, uri: &str) -> Result<Value> {
574        let path = uri.strip_prefix("config://").unwrap_or("");
575
576        match path {
577            "current" => {
578                // Return all configuration in one response
579                let states = config::get_states_config(&self.states_config)?;
580                let phases = config::get_phases_config(&self.phases_config)?;
581                let dependencies = config::get_dependencies_config(&self.deps_config)?;
582                let tags = config::get_tags_config(&self.tags_config)?;
583
584                Ok(serde_json::json!({
585                    "states": states,
586                    "phases": phases,
587                    "dependencies": dependencies,
588                    "tags": tags,
589                }))
590            }
591            "states" => config::get_states_config(&self.states_config),
592            "phases" => config::get_phases_config(&self.phases_config),
593            "dependencies" => config::get_dependencies_config(&self.deps_config),
594            "tags" => config::get_tags_config(&self.tags_config),
595            _ => Err(anyhow::anyhow!("Unknown config resource: {}", path)),
596        }
597    }
598
599    async fn read_workflows_resource(&self, uri: &str) -> Result<Value> {
600        let path = uri.strip_prefix("workflows://").unwrap_or("");
601
602        match path {
603            "list" => workflows::list_workflows(&self.workflows_config),
604            name => workflows::get_workflow(&self.workflows_config, name),
605        }
606    }
607}