1use std::path::Path;
4
5use anyhow::{Context, Result};
6use serde_json::json;
7
8use crate::bean::Bean;
9use crate::discovery::find_bean_file;
10use crate::index::Index;
11use crate::mcp::protocol::{ResourceContent, ResourceDefinition};
12
13pub fn resource_definitions() -> Vec<ResourceDefinition> {
15 vec![
16 ResourceDefinition {
17 uri: "beans://status".to_string(),
18 name: "Project Status".to_string(),
19 description: Some(
20 "Current project status: claimed, ready, goals, and blocked beans".to_string(),
21 ),
22 mime_type: Some("application/json".to_string()),
23 },
24 ResourceDefinition {
25 uri: "beans://rules".to_string(),
26 name: "Project Rules".to_string(),
27 description: Some("Project rules from RULES.md (if it exists)".to_string()),
28 mime_type: Some("text/markdown".to_string()),
29 },
30 ]
31}
32
33pub fn handle_resource_read(uri: &str, beans_dir: &Path) -> Result<Vec<ResourceContent>> {
35 if uri == "beans://status" {
36 return read_status_resource(beans_dir);
37 }
38
39 if uri == "beans://rules" {
40 return read_rules_resource(beans_dir);
41 }
42
43 if let Some(id) = uri.strip_prefix("beans://bean/") {
45 return read_bean_resource(id, beans_dir);
46 }
47
48 anyhow::bail!("Unknown resource URI: {}", uri)
49}
50
51fn read_status_resource(beans_dir: &Path) -> Result<Vec<ResourceContent>> {
52 let index = Index::load_or_rebuild(beans_dir)?;
53
54 let mut claimed = 0u32;
55 let mut ready = 0u32;
56 let mut goals = 0u32;
57 let mut blocked = 0u32;
58 let mut closed = 0u32;
59
60 for entry in &index.beans {
61 match entry.status {
62 crate::bean::Status::InProgress => claimed += 1,
63 crate::bean::Status::Closed => closed += 1,
64 crate::bean::Status::Open => {
65 if entry.has_verify {
66 let is_blocked = entry.dependencies.iter().any(|dep_id| {
68 index
69 .beans
70 .iter()
71 .find(|e| &e.id == dep_id)
72 .is_none_or(|e| e.status != crate::bean::Status::Closed)
73 });
74 if is_blocked {
75 blocked += 1;
76 } else {
77 ready += 1;
78 }
79 } else {
80 goals += 1;
81 }
82 }
83 }
84 }
85
86 let text = serde_json::to_string_pretty(&json!({
87 "total": index.beans.len(),
88 "claimed": claimed,
89 "ready": ready,
90 "goals": goals,
91 "blocked": blocked,
92 "closed": closed,
93 }))?;
94
95 Ok(vec![ResourceContent {
96 uri: "beans://status".to_string(),
97 mime_type: Some("application/json".to_string()),
98 text,
99 }])
100}
101
102fn read_rules_resource(beans_dir: &Path) -> Result<Vec<ResourceContent>> {
103 let project_dir = beans_dir
104 .parent()
105 .ok_or_else(|| anyhow::anyhow!("Cannot determine project root"))?;
106
107 let rules_path = project_dir.join("RULES.md");
108 if !rules_path.exists() {
109 return Ok(vec![ResourceContent {
110 uri: "beans://rules".to_string(),
111 mime_type: Some("text/markdown".to_string()),
112 text: "No RULES.md found in project root.".to_string(),
113 }]);
114 }
115
116 let text = std::fs::read_to_string(&rules_path).context("Failed to read RULES.md")?;
117
118 Ok(vec![ResourceContent {
119 uri: "beans://rules".to_string(),
120 mime_type: Some("text/markdown".to_string()),
121 text,
122 }])
123}
124
125fn read_bean_resource(id: &str, beans_dir: &Path) -> Result<Vec<ResourceContent>> {
126 crate::util::validate_bean_id(id)?;
127 let bean_path = find_bean_file(beans_dir, id)?;
128 let bean = Bean::from_file(&bean_path)?;
129
130 let text = serde_json::to_string_pretty(&bean).context("Failed to serialize bean")?;
131
132 Ok(vec![ResourceContent {
133 uri: format!("beans://bean/{}", id),
134 mime_type: Some("application/json".to_string()),
135 text,
136 }])
137}