Skip to main content

lean_ctx/core/terse/
mcp_compress.rs

1//! Layer 4: MCP tool description compression.
2//!
3//! Compresses lean-ctx's 56+ tool descriptions to reduce the token overhead
4//! of the initial `tools/list` response. Two modes:
5//! - Terse: Natural-language descriptions shortened via abbreviations
6//! - Lazy: Only tool name + 1-line summary, full description on-demand
7
8use super::dictionaries::{self, DictLevel};
9use crate::core::config::CompressionLevel;
10
11/// Compression mode for tool descriptions.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum DescriptionMode {
14    Full,
15    Terse,
16    Lazy,
17}
18
19impl DescriptionMode {
20    pub fn from_compression_level(level: &CompressionLevel) -> Self {
21        match level {
22            CompressionLevel::Off | CompressionLevel::Lite => Self::Full,
23            CompressionLevel::Standard => Self::Terse,
24            CompressionLevel::Max => Self::Lazy,
25        }
26    }
27}
28
29/// Compresses a single tool description according to the mode.
30pub fn compress_description(name: &str, description: &str, mode: DescriptionMode) -> String {
31    match mode {
32        DescriptionMode::Full => description.to_string(),
33        DescriptionMode::Terse => terse_description(description),
34        DescriptionMode::Lazy => lazy_description(name, description),
35    }
36}
37
38fn terse_description(desc: &str) -> String {
39    let abbreviated = dictionaries::apply_dictionaries(desc, DictLevel::General);
40
41    let mut lines: Vec<&str> = abbreviated.lines().collect();
42
43    lines.retain(|line| {
44        let trimmed = line.trim();
45        !trimmed.is_empty()
46            && !trimmed.starts_with("Example")
47            && !trimmed.starts_with("Note:")
48            && !trimmed.starts_with("See also")
49    });
50
51    if lines.len() > 3 {
52        lines.truncate(3);
53    }
54
55    lines.join("\n")
56}
57
58fn lazy_description(name: &str, desc: &str) -> String {
59    let first_line = desc.lines().next().unwrap_or(name);
60    let summary = if first_line.len() > 80 {
61        format!("{}…", &first_line[..77])
62    } else {
63        first_line.to_string()
64    };
65    format!("{summary} (use ctx_discover_tools for full docs)")
66}
67
68/// Estimates token savings from compressing all tool descriptions.
69pub fn estimate_savings(descriptions: &[(&str, &str)], mode: DescriptionMode) -> (u32, u32) {
70    let mut total_before = 0u32;
71    let mut total_after = 0u32;
72
73    for (name, desc) in descriptions {
74        let before = super::counter::count(desc);
75        let compressed = compress_description(name, desc, mode);
76        let after = super::counter::count(&compressed);
77        total_before += before;
78        total_after += after;
79    }
80
81    (total_before, total_after)
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn full_mode_unchanged() {
90        let desc = "Read a file from disk with caching.";
91        assert_eq!(
92            compress_description("ctx_read", desc, DescriptionMode::Full),
93            desc
94        );
95    }
96
97    #[test]
98    fn terse_mode_abbreviates() {
99        let desc = "Read a configuration file from the directory.";
100        let result = compress_description("ctx_read", desc, DescriptionMode::Terse);
101        assert!(
102            result.contains("cfg") || result.contains("dir"),
103            "should abbreviate: {result}"
104        );
105    }
106
107    #[test]
108    fn lazy_mode_short() {
109        let desc = "Read a file from disk with intelligent caching and compression modes.\nSupports 10 different read modes for optimal token efficiency.";
110        let result = compress_description("ctx_read", desc, DescriptionMode::Lazy);
111        assert!(
112            result.contains("ctx_discover_tools"),
113            "lazy should reference ctx_discover_tools"
114        );
115        assert!(result.lines().count() == 1, "lazy should be 1 line");
116    }
117
118    #[test]
119    fn mode_from_compression_level() {
120        assert_eq!(
121            DescriptionMode::from_compression_level(&CompressionLevel::Off),
122            DescriptionMode::Full
123        );
124        assert_eq!(
125            DescriptionMode::from_compression_level(&CompressionLevel::Standard),
126            DescriptionMode::Terse
127        );
128        assert_eq!(
129            DescriptionMode::from_compression_level(&CompressionLevel::Max),
130            DescriptionMode::Lazy
131        );
132    }
133
134    #[test]
135    fn estimate_savings_returns_values() {
136        let descs = vec![
137            (
138                "ctx_read",
139                "Read a configuration file from the directory with caching.",
140            ),
141            (
142                "ctx_shell",
143                "Execute a shell command with pattern compression.",
144            ),
145        ];
146        let (before, after) = estimate_savings(&descs, DescriptionMode::Terse);
147        assert!(before > 0);
148        assert!(after > 0);
149        assert!(after <= before);
150    }
151}