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://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 pub fn get_resources(&self) -> Vec<Resource> {
300 vec![
301 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 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 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 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/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/marks" => files::get_all_file_locks(&self.db),
543 "agents/all" => agents::get_all_workers(&self.db),
545 "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 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/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/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 "index" => docs::list_docs(docs_dir),
597 _ if path.starts_with("search/") => {
598 let query = path.strip_prefix("search/").unwrap_or("");
599 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 doc_path => docs::get_doc_resource(docs_dir, doc_path),
607 }
608 }
609}