Skip to main content

mana/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::discovery::find_unit_file;
9use crate::index::Index;
10use crate::mcp::protocol::{ResourceContent, ResourceDefinition};
11use crate::unit::Unit;
12
13/// Return static resource definitions.
14pub fn resource_definitions() -> Vec<ResourceDefinition> {
15    vec![
16        ResourceDefinition {
17            uri: "units://status".to_string(),
18            name: "Project Status".to_string(),
19            description: Some(
20                "Current project status: claimed, ready, goals, and blocked units".to_string(),
21            ),
22            mime_type: Some("application/json".to_string()),
23        },
24        ResourceDefinition {
25            uri: "units://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, mana_dir: &Path) -> Result<Vec<ResourceContent>> {
35    if uri == "units://status" {
36        return read_status_resource(mana_dir);
37    }
38
39    if uri == "units://rules" {
40        return read_rules_resource(mana_dir);
41    }
42
43    // units://unit/{id}
44    if let Some(id) = uri.strip_prefix("units://unit/") {
45        return read_unit_resource(id, mana_dir);
46    }
47
48    anyhow::bail!("Unknown resource URI: {}", uri)
49}
50
51fn read_status_resource(mana_dir: &Path) -> Result<Vec<ResourceContent>> {
52    let index = Index::load_or_rebuild(mana_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.units {
61        match entry.status {
62            crate::unit::Status::InProgress | crate::unit::Status::AwaitingVerify => claimed += 1,
63            crate::unit::Status::Closed => closed += 1,
64            crate::unit::Status::Open => {
65                if entry.has_verify {
66                    // Check if blocked
67                    let is_blocked = entry.dependencies.iter().any(|dep_id| {
68                        index
69                            .units
70                            .iter()
71                            .find(|e| &e.id == dep_id)
72                            .is_none_or(|e| e.status != crate::unit::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.units.len(),
88        "claimed": claimed,
89        "ready": ready,
90        "goals": goals,
91        "blocked": blocked,
92        "closed": closed,
93    }))?;
94
95    Ok(vec![ResourceContent {
96        uri: "units://status".to_string(),
97        mime_type: Some("application/json".to_string()),
98        text,
99    }])
100}
101
102fn read_rules_resource(mana_dir: &Path) -> Result<Vec<ResourceContent>> {
103    let project_dir = mana_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: "units://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: "units://rules".to_string(),
120        mime_type: Some("text/markdown".to_string()),
121        text,
122    }])
123}
124
125fn read_unit_resource(id: &str, mana_dir: &Path) -> Result<Vec<ResourceContent>> {
126    crate::util::validate_unit_id(id)?;
127    let unit_path = find_unit_file(mana_dir, id)?;
128    let unit = Unit::from_file(&unit_path)?;
129
130    let text = serde_json::to_string_pretty(&unit).context("Failed to serialize unit")?;
131
132    Ok(vec![ResourceContent {
133        uri: format!("units://unit/{}", id),
134        mime_type: Some("application/json".to_string()),
135        text,
136    }])
137}