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::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
20pub 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 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 pub fn with_skills_dir(mut self, dir: std::path::PathBuf) -> Self {
54 self.skills_dir = Some(dir);
55 self
56 }
57
58 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 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 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 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 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 pub async fn read_resource(&self, uri: &str) -> Result<Value> {
485 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 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}