1pub 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
19pub struct ResourceHandler {
21 pub db: Arc<Database>,
22 pub config: AppConfig,
24 pub skills_dir: Option<std::path::PathBuf>,
26 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 pub fn with_skills_dir(mut self, dir: std::path::PathBuf) -> Self {
42 self.skills_dir = Some(dir);
43 self
44 }
45
46 pub fn with_docs_dir(mut self, dir: std::path::PathBuf) -> Self {
48 self.docs_dir = Some(dir);
49 self
50 }
51
52 pub fn get_resource_templates(&self) -> Vec<ResourceTemplate> {
54 vec![
55 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 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 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 pub fn get_resources(&self) -> Vec<Resource> {
322 vec![
323 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 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 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 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/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/marks" => files::get_all_file_locks(&self.db),
580 "agents/all" => agents::get_all_workers(&self.db, &self.config.states),
582 "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 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 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/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/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/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 "index" => docs::list_docs(docs_dir),
648 _ if path.starts_with("search/") => {
649 let query = path.strip_prefix("search/").unwrap_or("");
650 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 doc_path => docs::get_doc_resource(docs_dir, doc_path),
658 }
659 }
660}