Skip to main content

lean_ctx/server/
resources.rs

1use rmcp::model::{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    Resource::new(uri, name)
55        .with_description(desc)
56        .with_mime_type("text/plain")
57}
58
59fn build_summary(ledger: &crate::core::context_ledger::ContextLedger) -> String {
60    let pressure = ledger.pressure();
61    let adjusted = ledger.adjusted_total_saved();
62    format!(
63        "files:{} | sent:{} | saved:{} (adj:{}) | pressure:{:.0}% | action:{:?}",
64        ledger.entries.len(),
65        ledger.total_tokens_sent,
66        ledger.total_tokens_saved,
67        adjusted,
68        pressure.utilization * 100.0,
69        pressure.recommendation,
70    )
71}
72
73fn build_pressure(ledger: &crate::core::context_ledger::ContextLedger) -> String {
74    let p = ledger.pressure();
75    let mut lines = vec![
76        format!("utilization: {:.1}%", p.utilization * 100.0),
77        format!("remaining: {} tokens", p.remaining_tokens),
78        format!("entries: {}", p.entries_count),
79        format!("action: {:?}", p.recommendation),
80    ];
81
82    if p.utilization > 0.8 {
83        let evict = ledger.eviction_candidates_by_phi(3);
84        if !evict.is_empty() {
85            let names: Vec<_> = evict
86                .iter()
87                .take(5)
88                .map(|p| crate::core::protocol::shorten_path(p))
89                .collect();
90            lines.push(format!("eviction_candidates: {}", names.join(", ")));
91        }
92    }
93
94    lines.join("\n")
95}
96
97fn build_plan(ledger: &crate::core::context_ledger::ContextLedger) -> String {
98    let mut lines = Vec::new();
99    for entry in &ledger.entries {
100        let short = crate::core::protocol::shorten_path(&entry.path);
101        let phi_str = entry.phi.map_or("?".to_string(), |p| format!("{p:.2}"));
102        let state_str = entry.state.as_ref().map_or("?", |s| match s {
103            crate::core::context_field::ContextState::Included => "incl",
104            crate::core::context_field::ContextState::Pinned => "pin",
105            crate::core::context_field::ContextState::Excluded => "excl",
106            crate::core::context_field::ContextState::Candidate => "cand",
107            crate::core::context_field::ContextState::Stale => "stale",
108            crate::core::context_field::ContextState::Shadowed => "shadow",
109        });
110        lines.push(format!(
111            "{short} mode={} tok={} phi={phi_str} state={state_str}",
112            entry.mode, entry.sent_tokens,
113        ));
114    }
115    if lines.is_empty() {
116        "No context items tracked yet.".to_string()
117    } else {
118        lines.join("\n")
119    }
120}
121
122fn build_pinned(ledger: &crate::core::context_ledger::ContextLedger) -> String {
123    let pinned: Vec<_> = ledger
124        .entries
125        .iter()
126        .filter(|e| e.state == Some(crate::core::context_field::ContextState::Pinned))
127        .collect();
128    if pinned.is_empty() {
129        return "No pinned items.".to_string();
130    }
131    let mut lines = Vec::new();
132    for entry in pinned {
133        let short = crate::core::protocol::shorten_path(&entry.path);
134        lines.push(format!(
135            "{short} mode={} tok={}",
136            entry.mode, entry.sent_tokens
137        ));
138    }
139    lines.join("\n")
140}
141
142fn build_bounce() -> String {
143    match crate::core::bounce_tracker::global().lock() {
144        Ok(bt) => bt.format_summary(),
145        _ => "Bounce tracker unavailable.".to_string(),
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn list_returns_five_resources() {
155        let resources = list_resources();
156        assert_eq!(resources.len(), 5);
157    }
158
159    #[test]
160    fn read_summary_returns_content() {
161        let ledger = crate::core::context_ledger::ContextLedger::new();
162        let result = read_resource(URI_SUMMARY, &ledger);
163        assert!(result.is_some());
164    }
165
166    #[test]
167    fn read_unknown_uri_returns_none() {
168        let ledger = crate::core::context_ledger::ContextLedger::new();
169        let result = read_resource("lean-ctx://unknown", &ledger);
170        assert!(result.is_none());
171    }
172
173    #[test]
174    fn read_pressure_returns_content() {
175        let ledger = crate::core::context_ledger::ContextLedger::new();
176        let result = read_resource(URI_PRESSURE, &ledger);
177        assert!(result.is_some());
178    }
179
180    #[test]
181    fn read_bounce_returns_content() {
182        let ledger = crate::core::context_ledger::ContextLedger::new();
183        let result = read_resource(URI_BOUNCE, &ledger);
184        assert!(result.is_some());
185    }
186}