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