lean_ctx/core/
server_capabilities.rs1use serde_json::{Value, json};
15
16use crate::core::contracts::{CAPABILITIES_CONTRACT_VERSION, status_kv, versions_kv};
17
18pub const TOP_LEVEL_KEYS: [&str; 11] = [
20 "contract_version",
21 "server",
22 "plane",
23 "transports",
24 "presets",
25 "read_modes",
26 "tools",
27 "features",
28 "extensions",
29 "contracts",
30 "contract_status",
31];
32
33pub fn capabilities_value() -> Value {
35 let manifest = crate::core::mcp_manifest::manifest_value();
36 let tool_names = tool_names(&manifest);
37 let read_modes = manifest.get("read_modes").cloned().unwrap_or(Value::Null);
38 let active_persona =
39 crate::core::persona::Persona::resolve(&crate::core::config::Config::load());
40
41 json!({
42 "contract_version": CAPABILITIES_CONTRACT_VERSION,
43 "server": {
44 "name": "lean-ctx",
45 "version": env!("CARGO_PKG_VERSION"),
46 "persona": active_persona.name,
47 },
48 "plane": "personal",
49 "transports": ["stdio-mcp", "http-mcp", "rest", "sse"],
50 "presets": crate::core::persona::Persona::builtin_names(),
51 "read_modes": read_modes,
52 "tools": {
53 "total": tool_names.len(),
54 "names": tool_names,
55 },
56 "features": features(),
57 "extensions": extensions(),
58 "contracts": versions_kv(),
59 "contract_status": status_kv(),
63 })
64}
65
66fn tool_names(manifest: &Value) -> Vec<String> {
67 manifest
68 .get("tools")
69 .and_then(|t| t.get("granular"))
70 .and_then(|g| g.as_array())
71 .map(|arr| {
72 arr.iter()
73 .filter_map(|t| t.get("name").and_then(|n| n.as_str()).map(String::from))
74 .collect()
75 })
76 .unwrap_or_default()
77}
78
79pub const COMMUNITY_ALWAYS_ON_FEATURES: &[&str] = &[
82 "compression",
83 "caching",
84 "knowledge",
85 "session",
86 "gateway",
87 "sensitivity_floor",
88 "savings_ledger",
89 "audit_trail",
90 "routing",
91 "team_seat_value_v1",
92];
93
94pub const COMMUNITY_OPTIONAL_FEATURES: &[&str] = &[
96 "ast_compression",
97 "semantic_search",
98 "http_server",
99 "wasm_runtime",
100 "shape_translation",
102];
103
104fn features() -> Value {
107 json!({
108 "compression": true,
109 "caching": true,
110 "knowledge": true,
111 "session": true,
112 "gateway": true,
113 "sensitivity_floor": true,
114 "savings_ledger": true,
115 "audit_trail": true,
116 "routing": true,
119 "team_seat_value_v1": true,
120 "ast_compression": cfg!(feature = "tree-sitter"),
121 "semantic_search": cfg!(feature = "embeddings"),
122 "http_server": cfg!(feature = "http-server"),
123 "wasm_runtime": cfg!(feature = "wasm"),
124 "shape_translation": cfg!(feature = "shape-xlat"),
125 })
126}
127
128fn extensions() -> Value {
132 let (read_modes, compressors, chunkers) = crate::core::extension_registry::global()
133 .read()
134 .map(|r| (r.read_mode_names(), r.compressor_names(), r.chunker_names()))
135 .unwrap_or_default();
136
137 json!({
138 "plugins": [],
139 "tools": [],
140 "read_modes": read_modes,
141 "compressors": compressors,
142 "chunkers": chunkers,
143 })
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn payload_has_exactly_documented_top_level_keys() {
152 let v = capabilities_value();
153 let obj = v.as_object().expect("capabilities is an object");
154 let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
155 keys.sort_unstable();
156 let mut expected: Vec<&str> = TOP_LEVEL_KEYS.to_vec();
157 expected.sort_unstable();
158 assert_eq!(keys, expected, "top-level keys drifted from TOP_LEVEL_KEYS");
159 }
160
161 #[test]
162 fn contract_version_matches_constant() {
163 let v = capabilities_value();
164 assert_eq!(v["contract_version"], json!(CAPABILITIES_CONTRACT_VERSION));
165 }
166
167 #[test]
168 fn extensions_expose_registry_builtins() {
169 let v = capabilities_value();
170 let ext = &v["extensions"];
171 assert!(ext["plugins"].is_array());
172 let compressors = ext["compressors"].as_array().expect("compressors array");
173 assert!(compressors.iter().any(|c| c == "identity"));
174 assert!(
175 ext["read_modes"]
176 .as_array()
177 .is_some_and(|a| a.iter().any(|m| m == "full"))
178 );
179 assert!(
180 ext["chunkers"]
181 .as_array()
182 .is_some_and(|a| a.iter().any(|c| c == "lines"))
183 );
184 }
185
186 #[test]
187 fn every_advertised_feature_has_an_explicit_product_record() {
188 let v = capabilities_value();
189 for key in v["features"].as_object().expect("features object").keys() {
190 assert!(
191 crate::core::product_capabilities::registry()
192 .find(key)
193 .is_some(),
194 "server feature '{key}' lacks product classification"
195 );
196 }
197 }
198
199 #[test]
200 fn community_always_on_features_are_available_without_an_account() {
201 let v = capabilities_value();
202 for key in COMMUNITY_ALWAYS_ON_FEATURES {
203 assert_eq!(
204 v["features"][key],
205 json!(true),
206 "Community capability '{key}' must be available"
207 );
208 let capability = crate::core::product_capabilities::registry()
209 .find(key)
210 .expect("feature classified");
211 assert_eq!(
212 capability.minimum_plan(),
213 crate::core::product_capabilities::Plan::Community
214 );
215 assert!(!capability.account_required);
216 }
217 }
218
219 #[test]
220 fn reports_compiled_features() {
221 let v = capabilities_value();
222 assert_eq!(v["features"]["compression"], json!(true));
224 assert_eq!(v["features"]["savings_ledger"], json!(true));
225 assert_eq!(v["features"]["team_seat_value_v1"], json!(true));
226 assert_eq!(
227 v["contracts"]["leanctx.contract.team_seat_value_v1.schema_version"],
228 json!(1)
229 );
230 assert_eq!(
232 v["features"]["semantic_search"],
233 json!(cfg!(feature = "embeddings"))
234 );
235 }
236}