Skip to main content

lean_ctx/server/
resources.rs

1use rmcp::model::{Annotated, RawResource, Resource, ResourceContents};
2
3const URI_SUMMARY: &str = "lean-ctx://context/summary";
4const URI_PINNED: &str = "lean-ctx://context/pinned";
5const URI_PRESSURE: &str = "lean-ctx://context/pressure";
6const URI_PLAN: &str = "lean-ctx://context/plan";
7const URI_BOUNCE: &str = "lean-ctx://context/bounce";
8
9pub fn list_resources() -> Vec<Resource> {
10    vec![
11        make_resource(
12            URI_SUMMARY,
13            "Context Summary",
14            "Ledger compact: items, pressure, budget",
15        ),
16        make_resource(
17            URI_PINNED,
18            "Pinned Items",
19            "Pinned context items with compressed content",
20        ),
21        make_resource(
22            URI_PRESSURE,
23            "Context Pressure",
24            "Budget utilization and recommendations",
25        ),
26        make_resource(
27            URI_PLAN,
28            "Context Plan",
29            "Current context plan with modes per file",
30        ),
31        make_resource(
32            URI_BOUNCE,
33            "Bounce Stats",
34            "Bounce detection statistics and wasted tokens",
35        ),
36    ]
37}
38
39pub fn read_resource(
40    uri: &str,
41    ledger: &crate::core::context_ledger::ContextLedger,
42) -> Option<Vec<ResourceContents>> {
43    match uri {
44        URI_SUMMARY => Some(vec![ResourceContents::text(build_summary(ledger), uri)]),
45        URI_PRESSURE => Some(vec![ResourceContents::text(build_pressure(ledger), uri)]),
46        URI_PLAN => Some(vec![ResourceContents::text(build_plan(ledger), uri)]),
47        URI_PINNED => Some(vec![ResourceContents::text(build_pinned(ledger), uri)]),
48        URI_BOUNCE => Some(vec![ResourceContents::text(build_bounce(), uri)]),
49        _ => None,
50    }
51}
52
53fn make_resource(uri: &str, name: &str, desc: &str) -> Resource {
54    let raw = RawResource::new(uri, name)
55        .with_description(desc)
56        .with_mime_type("text/plain");
57    Annotated::new(raw, None)
58}
59
60fn build_summary(ledger: &crate::core::context_ledger::ContextLedger) -> String {
61    let pressure = ledger.pressure();
62    let adjusted = ledger.adjusted_total_saved();
63    format!(
64        "files:{} | sent:{} | saved:{} (adj:{}) | pressure:{:.0}% | action:{:?}",
65        ledger.entries.len(),
66        ledger.total_tokens_sent,
67        ledger.total_tokens_saved,
68        adjusted,
69        pressure.utilization * 100.0,
70        pressure.recommendation,
71    )
72}
73
74fn build_pressure(ledger: &crate::core::context_ledger::ContextLedger) -> String {
75    let p = ledger.pressure();
76    let mut lines = vec![
77        format!("utilization: {:.1}%", p.utilization * 100.0),
78        format!("remaining: {} tokens", p.remaining_tokens),
79        format!("entries: {}", p.entries_count),
80        format!("action: {:?}", p.recommendation),
81    ];
82
83    if p.utilization > 0.8 {
84        let evict = ledger.eviction_candidates_by_phi(3);
85        if !evict.is_empty() {
86            let names: Vec<_> = evict
87                .iter()
88                .take(5)
89                .map(|p| crate::core::protocol::shorten_path(p))
90                .collect();
91            lines.push(format!("eviction_candidates: {}", names.join(", ")));
92        }
93    }
94
95    lines.join("\n")
96}
97
98fn build_plan(ledger: &crate::core::context_ledger::ContextLedger) -> String {
99    let mut lines = Vec::new();
100    for entry in &ledger.entries {
101        let short = crate::core::protocol::shorten_path(&entry.path);
102        let phi_str = entry.phi.map_or("?".to_string(), |p| format!("{p:.2}"));
103        let state_str = entry.state.as_ref().map_or("?", |s| match s {
104            crate::core::context_field::ContextState::Included => "incl",
105            crate::core::context_field::ContextState::Pinned => "pin",
106            crate::core::context_field::ContextState::Excluded => "excl",
107            crate::core::context_field::ContextState::Candidate => "cand",
108            crate::core::context_field::ContextState::Stale => "stale",
109            crate::core::context_field::ContextState::Shadowed => "shadow",
110        });
111        lines.push(format!(
112            "{short} mode={} tok={} phi={phi_str} state={state_str}",
113            entry.mode, entry.sent_tokens,
114        ));
115    }
116    if lines.is_empty() {
117        "No context items tracked yet.".to_string()
118    } else {
119        lines.join("\n")
120    }
121}
122
123fn build_pinned(ledger: &crate::core::context_ledger::ContextLedger) -> String {
124    let pinned: Vec<_> = ledger
125        .entries
126        .iter()
127        .filter(|e| e.state == Some(crate::core::context_field::ContextState::Pinned))
128        .collect();
129    if pinned.is_empty() {
130        return "No pinned items.".to_string();
131    }
132    let mut lines = Vec::new();
133    for entry in pinned {
134        let short = crate::core::protocol::shorten_path(&entry.path);
135        lines.push(format!(
136            "{short} mode={} tok={}",
137            entry.mode, entry.sent_tokens
138        ));
139    }
140    lines.join("\n")
141}
142
143fn build_bounce() -> String {
144    if let Ok(bt) = crate::core::bounce_tracker::global().lock() {
145        bt.format_summary()
146    } else {
147        "Bounce tracker unavailable.".to_string()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn list_returns_five_resources() {
157        let resources = list_resources();
158        assert_eq!(resources.len(), 5);
159    }
160
161    #[test]
162    fn read_summary_returns_content() {
163        let ledger = crate::core::context_ledger::ContextLedger::new();
164        let result = read_resource(URI_SUMMARY, &ledger);
165        assert!(result.is_some());
166    }
167
168    #[test]
169    fn read_unknown_uri_returns_none() {
170        let ledger = crate::core::context_ledger::ContextLedger::new();
171        let result = read_resource("lean-ctx://unknown", &ledger);
172        assert!(result.is_none());
173    }
174
175    #[test]
176    fn read_pressure_returns_content() {
177        let ledger = crate::core::context_ledger::ContextLedger::new();
178        let result = read_resource(URI_PRESSURE, &ledger);
179        assert!(result.is_some());
180    }
181
182    #[test]
183    fn read_bounce_returns_content() {
184        let ledger = crate::core::context_ledger::ContextLedger::new();
185        let result = read_resource(URI_BOUNCE, &ledger);
186        assert!(result.is_some());
187    }
188}