1pub mod agents;
4pub mod config;
5pub mod files;
6pub mod skills;
7pub mod stats;
8pub mod tasks;
9pub mod workflows;
10
11use crate::config::workflows::WorkflowsConfig;
12use crate::config::{DependenciesConfig, PhasesConfig, StatesConfig, TagsConfig};
13use crate::db::Database;
14use anyhow::Result;
15use rmcp::model::{Annotated, RawResourceTemplate, ResourceTemplate};
16use serde_json::Value;
17use std::sync::Arc;
18
19pub struct ResourceHandler {
21 pub db: Arc<Database>,
22 pub states_config: Arc<StatesConfig>,
23 pub phases_config: Arc<PhasesConfig>,
24 pub deps_config: Arc<DependenciesConfig>,
25 pub tags_config: Arc<TagsConfig>,
26 pub workflows_config: Arc<WorkflowsConfig>,
27 pub skills_dir: Option<std::path::PathBuf>,
29}
30
31impl ResourceHandler {
32 pub fn new(
33 db: Arc<Database>,
34 states_config: Arc<StatesConfig>,
35 phases_config: Arc<PhasesConfig>,
36 deps_config: Arc<DependenciesConfig>,
37 tags_config: Arc<TagsConfig>,
38 workflows_config: Arc<WorkflowsConfig>,
39 ) -> Self {
40 Self {
41 db,
42 states_config,
43 phases_config,
44 deps_config,
45 tags_config,
46 workflows_config,
47 skills_dir: None,
48 }
49 }
50
51 pub fn with_skills_dir(mut self, dir: std::path::PathBuf) -> Self {
53 self.skills_dir = Some(dir);
54 self
55 }
56
57 pub fn get_resource_templates(&self) -> Vec<ResourceTemplate> {
59 vec![
60 Annotated::new(
61 RawResourceTemplate {
62 uri_template: "tasks://all".into(),
63 name: "All Tasks".into(),
64 title: None,
65 description: Some("Full task graph with dependencies".into()),
66 mime_type: Some("application/json".into()),
67 icons: None,
68 },
69 None,
70 ),
71 Annotated::new(
72 RawResourceTemplate {
73 uri_template: "tasks://ready".into(),
74 name: "Ready Tasks".into(),
75 title: None,
76 description: Some("Tasks ready to claim".into()),
77 mime_type: Some("application/json".into()),
78 icons: None,
79 },
80 None,
81 ),
82 Annotated::new(
83 RawResourceTemplate {
84 uri_template: "tasks://blocked".into(),
85 name: "Blocked Tasks".into(),
86 title: None,
87 description: Some("Tasks blocked by dependencies".into()),
88 mime_type: Some("application/json".into()),
89 icons: None,
90 },
91 None,
92 ),
93 Annotated::new(
94 RawResourceTemplate {
95 uri_template: "tasks://claimed".into(),
96 name: "Claimed Tasks".into(),
97 title: None,
98 description: Some("All claimed tasks".into()),
99 mime_type: Some("application/json".into()),
100 icons: None,
101 },
102 None,
103 ),
104 Annotated::new(
105 RawResourceTemplate {
106 uri_template: "tasks://agent/{agent_id}".into(),
107 name: "Agent Tasks".into(),
108 title: None,
109 description: Some("Tasks owned by an agent".into()),
110 mime_type: Some("application/json".into()),
111 icons: None,
112 },
113 None,
114 ),
115 Annotated::new(
116 RawResourceTemplate {
117 uri_template: "tasks://tree/{task_id}".into(),
118 name: "Task Tree".into(),
119 title: None,
120 description: Some("Task with all descendants".into()),
121 mime_type: Some("application/json".into()),
122 icons: None,
123 },
124 None,
125 ),
126 Annotated::new(
127 RawResourceTemplate {
128 uri_template: "files://marks".into(),
129 name: "File Marks".into(),
130 title: None,
131 description: Some("All advisory file marks".into()),
132 mime_type: Some("application/json".into()),
133 icons: None,
134 },
135 None,
136 ),
137 Annotated::new(
138 RawResourceTemplate {
139 uri_template: "agents://all".into(),
140 name: "All Agents".into(),
141 title: None,
142 description: Some("Registered agents".into()),
143 mime_type: Some("application/json".into()),
144 icons: None,
145 },
146 None,
147 ),
148 Annotated::new(
149 RawResourceTemplate {
150 uri_template: "plan://acp".into(),
151 name: "ACP Plan".into(),
152 title: None,
153 description: Some("ACP-compatible plan export".into()),
154 mime_type: Some("application/json".into()),
155 icons: None,
156 },
157 None,
158 ),
159 Annotated::new(
160 RawResourceTemplate {
161 uri_template: "stats://summary".into(),
162 name: "Stats Summary".into(),
163 title: None,
164 description: Some("Aggregate statistics".into()),
165 mime_type: Some("application/json".into()),
166 icons: None,
167 },
168 None,
169 ),
170 Annotated::new(
172 RawResourceTemplate {
173 uri_template: "skills://list".into(),
174 name: "Available Skills".into(),
175 title: None,
176 description: Some("List all bundled task-graph skills".into()),
177 mime_type: Some("application/json".into()),
178 icons: None,
179 },
180 None,
181 ),
182 Annotated::new(
183 RawResourceTemplate {
184 uri_template: "skills://{name}".into(),
185 name: "Skill Content".into(),
186 title: None,
187 description: Some("Get a specific skill (basics, coordinator, worker, reporting, migration, repair)".into()),
188 mime_type: Some("text/markdown".into()),
189 icons: None,
190 },
191 None,
192 ),
193 Annotated::new(
195 RawResourceTemplate {
196 uri_template: "workflows://list".into(),
197 name: "Available Workflows".into(),
198 title: None,
199 description: Some("List all available workflow topologies with descriptions".into()),
200 mime_type: Some("application/json".into()),
201 icons: None,
202 },
203 None,
204 ),
205 Annotated::new(
206 RawResourceTemplate {
207 uri_template: "workflows://{name}".into(),
208 name: "Workflow Details".into(),
209 title: None,
210 description: Some("Get detailed information about a specific workflow (states, phases, settings)".into()),
211 mime_type: Some("application/json".into()),
212 icons: None,
213 },
214 None,
215 ),
216 ]
217 }
218
219 pub async fn read_resource(&self, uri: &str) -> Result<Value> {
221 if uri.starts_with("tasks://") {
223 self.read_tasks_resource(uri).await
224 } else if uri.starts_with("files://") {
225 self.read_files_resource(uri).await
226 } else if uri.starts_with("agents://") {
227 self.read_agents_resource(uri).await
228 } else if uri.starts_with("plan://") {
229 self.read_plan_resource(uri).await
230 } else if uri.starts_with("stats://") {
231 self.read_stats_resource(uri).await
232 } else if uri.starts_with("skills://") {
233 self.read_skills_resource(uri).await
234 } else if uri.starts_with("config://") {
235 self.read_config_resource(uri).await
236 } else if uri.starts_with("workflows://") {
237 self.read_workflows_resource(uri).await
238 } else {
239 Err(anyhow::anyhow!("Unknown resource URI: {}", uri))
240 }
241 }
242
243 async fn read_tasks_resource(&self, uri: &str) -> Result<Value> {
244 let path = uri.strip_prefix("tasks://").unwrap_or("");
245
246 match path {
247 "all" => tasks::get_all_tasks(&self.db),
248 "ready" => tasks::get_ready_tasks(&self.db, &self.states_config, &self.deps_config),
249 "blocked" => tasks::get_blocked_tasks(&self.db, &self.states_config, &self.deps_config),
250 "claimed" => tasks::get_claimed_tasks(&self.db, None),
251 _ if path.starts_with("agent/") => {
252 let agent_id = path.strip_prefix("agent/").unwrap();
253 tasks::get_claimed_tasks(&self.db, Some(agent_id))
254 }
255 _ if path.starts_with("tree/") => {
256 let task_id = path.strip_prefix("tree/").unwrap();
257 tasks::get_task_tree(&self.db, task_id)
258 }
259 _ => Err(anyhow::anyhow!("Unknown tasks resource: {}", path)),
260 }
261 }
262
263 async fn read_files_resource(&self, uri: &str) -> Result<Value> {
264 let path = uri.strip_prefix("files://").unwrap_or("");
265
266 match path {
267 "marks" => files::get_all_file_locks(&self.db),
268 _ => Err(anyhow::anyhow!("Unknown files resource: {}", path)),
269 }
270 }
271
272 async fn read_agents_resource(&self, uri: &str) -> Result<Value> {
273 let path = uri.strip_prefix("agents://").unwrap_or("");
274
275 match path {
276 "all" => agents::get_all_workers(&self.db),
277 _ => Err(anyhow::anyhow!("Unknown agents resource: {}", path)),
278 }
279 }
280
281 async fn read_plan_resource(&self, uri: &str) -> Result<Value> {
282 let path = uri.strip_prefix("plan://").unwrap_or("");
283
284 match path {
285 "acp" => stats::get_acp_plan(&self.db),
286 _ => Err(anyhow::anyhow!("Unknown plan resource: {}", path)),
287 }
288 }
289
290 async fn read_stats_resource(&self, uri: &str) -> Result<Value> {
291 let path = uri.strip_prefix("stats://").unwrap_or("");
292
293 match path {
294 "summary" => stats::get_stats_summary(&self.db, &self.states_config),
295 _ => Err(anyhow::anyhow!("Unknown stats resource: {}", path)),
296 }
297 }
298
299 async fn read_skills_resource(&self, uri: &str) -> Result<Value> {
300 let path = uri.strip_prefix("skills://").unwrap_or("");
301 let skills_dir = self.skills_dir.as_deref();
302
303 match path {
304 "list" => skills::list_skills(skills_dir),
305 name => skills::get_skill_resource(skills_dir, name),
306 }
307 }
308
309 async fn read_config_resource(&self, uri: &str) -> Result<Value> {
310 let path = uri.strip_prefix("config://").unwrap_or("");
311
312 match path {
313 "states" => config::get_states_config(&self.states_config),
314 "phases" => config::get_phases_config(&self.phases_config),
315 "dependencies" => config::get_dependencies_config(&self.deps_config),
316 "tags" => config::get_tags_config(&self.tags_config),
317 _ => Err(anyhow::anyhow!("Unknown config resource: {}", path)),
318 }
319 }
320
321 async fn read_workflows_resource(&self, uri: &str) -> Result<Value> {
322 let path = uri.strip_prefix("workflows://").unwrap_or("");
323
324 match path {
325 "list" => workflows::list_workflows(&self.workflows_config),
326 name => workflows::get_workflow(&self.workflows_config, name),
327 }
328 }
329}