Skip to main content

bn/mcp/
resources.rs

1//! MCP resource definitions and handlers.
2
3use 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
13/// Return static resource definitions.
14pub 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
33/// Handle a resource read request.
34pub 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    // beans://bean/{id}
44    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                    // Check if blocked
67                    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}