use rmcp::model::{Annotated, RawResource, Resource, ResourceContents};
const URI_SUMMARY: &str = "lean-ctx://context/summary";
const URI_PINNED: &str = "lean-ctx://context/pinned";
const URI_PRESSURE: &str = "lean-ctx://context/pressure";
const URI_PLAN: &str = "lean-ctx://context/plan";
const URI_BOUNCE: &str = "lean-ctx://context/bounce";
pub fn list_resources() -> Vec<Resource> {
vec![
make_resource(
URI_SUMMARY,
"Context Summary",
"Ledger compact: items, pressure, budget",
),
make_resource(
URI_PINNED,
"Pinned Items",
"Pinned context items with compressed content",
),
make_resource(
URI_PRESSURE,
"Context Pressure",
"Budget utilization and recommendations",
),
make_resource(
URI_PLAN,
"Context Plan",
"Current context plan with modes per file",
),
make_resource(
URI_BOUNCE,
"Bounce Stats",
"Bounce detection statistics and wasted tokens",
),
]
}
pub fn read_resource(
uri: &str,
ledger: &crate::core::context_ledger::ContextLedger,
) -> Option<Vec<ResourceContents>> {
match uri {
URI_SUMMARY => Some(vec![ResourceContents::text(build_summary(ledger), uri)]),
URI_PRESSURE => Some(vec![ResourceContents::text(build_pressure(ledger), uri)]),
URI_PLAN => Some(vec![ResourceContents::text(build_plan(ledger), uri)]),
URI_PINNED => Some(vec![ResourceContents::text(build_pinned(ledger), uri)]),
URI_BOUNCE => Some(vec![ResourceContents::text(build_bounce(), uri)]),
_ => None,
}
}
fn make_resource(uri: &str, name: &str, desc: &str) -> Resource {
let raw = RawResource::new(uri, name)
.with_description(desc)
.with_mime_type("text/plain");
Annotated::new(raw, None)
}
fn build_summary(ledger: &crate::core::context_ledger::ContextLedger) -> String {
let pressure = ledger.pressure();
let adjusted = ledger.adjusted_total_saved();
format!(
"files:{} | sent:{} | saved:{} (adj:{}) | pressure:{:.0}% | action:{:?}",
ledger.entries.len(),
ledger.total_tokens_sent,
ledger.total_tokens_saved,
adjusted,
pressure.utilization * 100.0,
pressure.recommendation,
)
}
fn build_pressure(ledger: &crate::core::context_ledger::ContextLedger) -> String {
let p = ledger.pressure();
let mut lines = vec![
format!("utilization: {:.1}%", p.utilization * 100.0),
format!("remaining: {} tokens", p.remaining_tokens),
format!("entries: {}", p.entries_count),
format!("action: {:?}", p.recommendation),
];
if p.utilization > 0.8 {
let evict = ledger.eviction_candidates_by_phi(3);
if !evict.is_empty() {
let names: Vec<_> = evict
.iter()
.take(5)
.map(|p| crate::core::protocol::shorten_path(p))
.collect();
lines.push(format!("eviction_candidates: {}", names.join(", ")));
}
}
lines.join("\n")
}
fn build_plan(ledger: &crate::core::context_ledger::ContextLedger) -> String {
let mut lines = Vec::new();
for entry in &ledger.entries {
let short = crate::core::protocol::shorten_path(&entry.path);
let phi_str = entry.phi.map_or("?".to_string(), |p| format!("{p:.2}"));
let state_str = entry.state.as_ref().map_or("?", |s| match s {
crate::core::context_field::ContextState::Included => "incl",
crate::core::context_field::ContextState::Pinned => "pin",
crate::core::context_field::ContextState::Excluded => "excl",
crate::core::context_field::ContextState::Candidate => "cand",
crate::core::context_field::ContextState::Stale => "stale",
crate::core::context_field::ContextState::Shadowed => "shadow",
});
lines.push(format!(
"{short} mode={} tok={} phi={phi_str} state={state_str}",
entry.mode, entry.sent_tokens,
));
}
if lines.is_empty() {
"No context items tracked yet.".to_string()
} else {
lines.join("\n")
}
}
fn build_pinned(ledger: &crate::core::context_ledger::ContextLedger) -> String {
let pinned: Vec<_> = ledger
.entries
.iter()
.filter(|e| e.state == Some(crate::core::context_field::ContextState::Pinned))
.collect();
if pinned.is_empty() {
return "No pinned items.".to_string();
}
let mut lines = Vec::new();
for entry in pinned {
let short = crate::core::protocol::shorten_path(&entry.path);
lines.push(format!(
"{short} mode={} tok={}",
entry.mode, entry.sent_tokens
));
}
lines.join("\n")
}
fn build_bounce() -> String {
if let Ok(bt) = crate::core::bounce_tracker::global().lock() {
bt.format_summary()
} else {
"Bounce tracker unavailable.".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_returns_five_resources() {
let resources = list_resources();
assert_eq!(resources.len(), 5);
}
#[test]
fn read_summary_returns_content() {
let ledger = crate::core::context_ledger::ContextLedger::new();
let result = read_resource(URI_SUMMARY, &ledger);
assert!(result.is_some());
}
#[test]
fn read_unknown_uri_returns_none() {
let ledger = crate::core::context_ledger::ContextLedger::new();
let result = read_resource("lean-ctx://unknown", &ledger);
assert!(result.is_none());
}
#[test]
fn read_pressure_returns_content() {
let ledger = crate::core::context_ledger::ContextLedger::new();
let result = read_resource(URI_PRESSURE, &ledger);
assert!(result.is_some());
}
#[test]
fn read_bounce_returns_content() {
let ledger = crate::core::context_ledger::ContextLedger::new();
let result = read_resource(URI_BOUNCE, &ledger);
assert!(result.is_some());
}
}