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