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://index".into(),
259                    name: "Documentation Index".into(),
260                    title: None,
261                    description: Some("List all available documentation files".into()),
262                    mime_type: Some("application/json".into()),
263                    icons: None,
264                },
265                None,
266            ),
267            Annotated::new(
268                RawResourceTemplate {
269                    uri_template: "docs://search/{query}".into(),
270                    name: "Documentation Search".into(),
271                    title: None,
272                    description: Some(
273                        "Full-text search across all documentation files. \
274                         Supports multi-term queries (space-separated, all terms must match). \
275                         Case-insensitive. Returns matching files with line-level context snippets."
276                            .into(),
277                    ),
278                    mime_type: Some("application/json".into()),
279                    icons: None,
280                },
281                None,
282            ),
283            Annotated::new(
284                RawResourceTemplate {
285                    uri_template: "docs://{path}".into(),
286                    name: "Documentation File".into(),
287                    title: None,
288                    description: Some("Get content of a specific documentation file (e.g., docs://GATES.md)".into()),
289                    mime_type: Some("text/markdown".into()),
290                    icons: None,
291                },
292                None,
293            ),
294        ]
295    }
296
297    /// Get all concrete resources (those without template parameters).
298    /// These are resources that can be directly accessed without any parameters.
299    pub fn get_resources(&self) -> Vec<Resource> {
300        vec![
301            // Query resources (live DB queries)
302            Annotated::new(
303                RawResource {
304                    uri: "query://tasks/all".into(),
305                    name: "All Tasks".into(),
306                    title: None,
307                    description: Some("Full task graph with dependencies".into()),
308                    mime_type: Some("application/json".into()),
309                    size: None,
310                    icons: None,
311                    meta: None,
312                },
313                None,
314            ),
315            Annotated::new(
316                RawResource {
317                    uri: "query://tasks/ready".into(),
318                    name: "Ready Tasks".into(),
319                    title: None,
320                    description: Some("Tasks ready to claim".into()),
321                    mime_type: Some("application/json".into()),
322                    size: None,
323                    icons: None,
324                    meta: None,
325                },
326                None,
327            ),
328            Annotated::new(
329                RawResource {
330                    uri: "query://tasks/blocked".into(),
331                    name: "Blocked Tasks".into(),
332                    title: None,
333                    description: Some("Tasks blocked by dependencies".into()),
334                    mime_type: Some("application/json".into()),
335                    size: None,
336                    icons: None,
337                    meta: None,
338                },
339                None,
340            ),
341            Annotated::new(
342                RawResource {
343                    uri: "query://tasks/claimed".into(),
344                    name: "Claimed Tasks".into(),
345                    title: None,
346                    description: Some("All claimed tasks".into()),
347                    mime_type: Some("application/json".into()),
348                    size: None,
349                    icons: None,
350                    meta: None,
351                },
352                None,
353            ),
354            Annotated::new(
355                RawResource {
356                    uri: "query://files/marks".into(),
357                    name: "File Marks".into(),
358                    title: None,
359                    description: Some("All advisory file marks".into()),
360                    mime_type: Some("application/json".into()),
361                    size: None,
362                    icons: None,
363                    meta: None,
364                },
365                None,
366            ),
367            Annotated::new(
368                RawResource {
369                    uri: "query://agents/all".into(),
370                    name: "All Agents".into(),
371                    title: None,
372                    description: Some("Registered agents".into()),
373                    mime_type: Some("application/json".into()),
374                    size: None,
375                    icons: None,
376                    meta: None,
377                },
378                None,
379            ),
380            Annotated::new(
381                RawResource {
382                    uri: "query://stats/summary".into(),
383                    name: "Stats Summary".into(),
384                    title: None,
385                    description: Some("Aggregate statistics".into()),
386                    mime_type: Some("application/json".into()),
387                    size: None,
388                    icons: None,
389                    meta: None,
390                },
391                None,
392            ),
393            // Config resources
394            Annotated::new(
395                RawResource {
396                    uri: "config://current".into(),
397                    name: "Current Configuration".into(),
398                    title: None,
399                    description: Some(
400                        "All configuration (states, phases, dependencies, tags) in one response"
401                            .into(),
402                    ),
403                    mime_type: Some("application/json".into()),
404                    size: None,
405                    icons: None,
406                    meta: None,
407                },
408                None,
409            ),
410            Annotated::new(
411                RawResource {
412                    uri: "config://states".into(),
413                    name: "States Configuration".into(),
414                    title: None,
415                    description: Some("Task state definitions and transitions".into()),
416                    mime_type: Some("application/json".into()),
417                    size: None,
418                    icons: None,
419                    meta: None,
420                },
421                None,
422            ),
423            Annotated::new(
424                RawResource {
425                    uri: "config://phases".into(),
426                    name: "Phases Configuration".into(),
427                    title: None,
428                    description: Some("Work phase definitions".into()),
429                    mime_type: Some("application/json".into()),
430                    size: None,
431                    icons: None,
432                    meta: None,
433                },
434                None,
435            ),
436            Annotated::new(
437                RawResource {
438                    uri: "config://dependencies".into(),
439                    name: "Dependencies Configuration".into(),
440                    title: None,
441                    description: Some("Dependency type definitions".into()),
442                    mime_type: Some("application/json".into()),
443                    size: None,
444                    icons: None,
445                    meta: None,
446                },
447                None,
448            ),
449            Annotated::new(
450                RawResource {
451                    uri: "config://tags".into(),
452                    name: "Tags Configuration".into(),
453                    title: None,
454                    description: Some("Tag definitions and categories".into()),
455                    mime_type: Some("application/json".into()),
456                    size: None,
457                    icons: None,
458                    meta: None,
459                },
460                None,
461            ),
462            // Docs resources (reference content: docs, skills, workflows)
463            Annotated::new(
464                RawResource {
465                    uri: "docs://skills/list".into(),
466                    name: "Available Skills".into(),
467                    title: None,
468                    description: Some("List all bundled task-graph skills".into()),
469                    mime_type: Some("application/json".into()),
470                    size: None,
471                    icons: None,
472                    meta: None,
473                },
474                None,
475            ),
476            Annotated::new(
477                RawResource {
478                    uri: "docs://workflows/list".into(),
479                    name: "Available Workflows".into(),
480                    title: None,
481                    description: Some(
482                        "List all available workflow topologies with descriptions".into(),
483                    ),
484                    mime_type: Some("application/json".into()),
485                    size: None,
486                    icons: None,
487                    meta: None,
488                },
489                None,
490            ),
491            Annotated::new(
492                RawResource {
493                    uri: "docs://index".into(),
494                    name: "Documentation Index".into(),
495                    title: None,
496                    description: Some("List all available documentation files".into()),
497                    mime_type: Some("application/json".into()),
498                    size: None,
499                    icons: None,
500                    meta: None,
501                },
502                None,
503            ),
504        ]
505    }
506
507    /// Read a resource by URI.
508    pub async fn read_resource(&self, uri: &str) -> Result<Value> {
509        if uri.starts_with("query://") {
510            self.read_query_resource(uri).await
511        } else if uri.starts_with("config://") {
512            self.read_config_resource(uri).await
513        } else if uri.starts_with("docs://") {
514            self.read_docs_resource(uri).await
515        } else {
516            Err(anyhow::anyhow!("Unknown resource URI: {}", uri))
517        }
518    }
519
520    async fn read_query_resource(&self, uri: &str) -> Result<Value> {
521        let path = uri.strip_prefix("query://").unwrap_or("");
522
523        match path {
524            // Tasks
525            "tasks/all" => tasks::get_all_tasks(&self.db),
526            "tasks/ready" => {
527                tasks::get_ready_tasks(&self.db, &self.config.states, &self.config.deps)
528            }
529            "tasks/blocked" => {
530                tasks::get_blocked_tasks(&self.db, &self.config.states, &self.config.deps)
531            }
532            "tasks/claimed" => tasks::get_claimed_tasks(&self.db, None),
533            _ if path.starts_with("tasks/agent/") => {
534                let agent_id = path.strip_prefix("tasks/agent/").unwrap();
535                tasks::get_claimed_tasks(&self.db, Some(agent_id))
536            }
537            _ if path.starts_with("tasks/tree/") => {
538                let task_id = path.strip_prefix("tasks/tree/").unwrap();
539                tasks::get_task_tree(&self.db, task_id)
540            }
541            // Files
542            "files/marks" => files::get_all_file_locks(&self.db),
543            // Agents
544            "agents/all" => agents::get_all_workers(&self.db),
545            // Stats
546            "stats/summary" => stats::get_stats_summary(&self.db, &self.config.states),
547            _ => Err(anyhow::anyhow!("Unknown query resource: {}", path)),
548        }
549    }
550
551    async fn read_config_resource(&self, uri: &str) -> Result<Value> {
552        let path = uri.strip_prefix("config://").unwrap_or("");
553
554        match path {
555            "current" => {
556                // Return all configuration in one response
557                let states = config::get_states_config(&self.config.states)?;
558                let phases = config::get_phases_config(&self.config.phases)?;
559                let dependencies = config::get_dependencies_config(&self.config.deps)?;
560                let tags = config::get_tags_config(&self.config.tags)?;
561
562                Ok(serde_json::json!({
563                    "states": states,
564                    "phases": phases,
565                    "dependencies": dependencies,
566                    "tags": tags,
567                }))
568            }
569            "states" => config::get_states_config(&self.config.states),
570            "phases" => config::get_phases_config(&self.config.phases),
571            "dependencies" => config::get_dependencies_config(&self.config.deps),
572            "tags" => config::get_tags_config(&self.config.tags),
573            _ => Err(anyhow::anyhow!("Unknown config resource: {}", path)),
574        }
575    }
576
577    async fn read_docs_resource(&self, uri: &str) -> Result<Value> {
578        let path = uri.strip_prefix("docs://").unwrap_or("");
579        let skills_dir = self.skills_dir.as_deref();
580        let docs_dir = self.docs_dir.as_deref();
581
582        match path {
583            // Skills
584            "skills/list" => skills::list_skills(skills_dir),
585            _ if path.starts_with("skills/") => {
586                let name = path.strip_prefix("skills/").unwrap();
587                skills::get_skill_resource(skills_dir, name)
588            }
589            // Workflows
590            "workflows/list" => workflows::list_workflows(&self.config.workflows),
591            _ if path.starts_with("workflows/") => {
592                let name = path.strip_prefix("workflows/").unwrap();
593                workflows::get_workflow(&self.config.workflows, name)
594            }
595            // Documentation files
596            "index" => docs::list_docs(docs_dir),
597            _ if path.starts_with("search/") => {
598                let query = path.strip_prefix("search/").unwrap_or("");
599                // URL-decode the query string
600                let query = urlencoding::decode(query)
601                    .unwrap_or_else(|_| query.into())
602                    .into_owned();
603                docs::search_docs(docs_dir, &query, None, None)
604            }
605            // Individual doc file (e.g., "GATES.md" or "diagrams/README.md")
606            doc_path => docs::get_doc_resource(docs_dir, doc_path),
607        }
608    }
609}