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 "build": crate::core::integrity::build().to_json(),
48 "persona": active_persona.name,
49 },
50 "plane": "personal",
51 "transports": ["stdio-mcp", "http-mcp", "rest", "sse"],
52 "presets": crate::core::persona::Persona::builtin_names(),
53 "read_modes": read_modes,
54 "tools": {
55 "total": tool_names.len(),
56 "names": tool_names,
57 },
58 "features": features(),
59 "extensions": extensions(),
60 "contracts": versions_kv(),
61 "contract_status": status_kv(),
65 })
66}
67
68fn tool_names(manifest: &Value) -> Vec<String> {
69 manifest
70 .get("tools")
71 .and_then(|t| t.get("granular"))
72 .and_then(|g| g.as_array())
73 .map(|arr| {
74 arr.iter()
75 .filter_map(|t| t.get("name").and_then(|n| n.as_str()).map(String::from))
76 .collect()
77 })
78 .unwrap_or_default()
79}
80
81pub const COMMUNITY_ALWAYS_ON_FEATURES: &[&str] = &[
84 "compression",
85 "caching",
86 "knowledge",
87 "session",
88 "gateway",
89 "sensitivity_floor",
90 "savings_ledger",
91 "audit_trail",
92 "routing",
93 "team_seat_value_v1",
94];
95
96pub const COMMUNITY_OPTIONAL_FEATURES: &[&str] = &[
98 "ast_compression",
99 "semantic_search",
100 "http_server",
101 "wasm_runtime",
102 "shape_translation",
104];
105
106fn features() -> Value {
109 json!({
110 "compression": true,
111 "caching": true,
112 "knowledge": true,
113 "session": true,
114 "gateway": true,
115 "sensitivity_floor": true,
116 "savings_ledger": true,
117 "audit_trail": true,
118 "routing": true,
121 "team_seat_value_v1": true,
122 "ast_compression": cfg!(feature = "tree-sitter"),
123 "semantic_search": cfg!(feature = "embeddings"),
124 "http_server": cfg!(feature = "http-server"),
125 "wasm_runtime": cfg!(feature = "wasm"),
126 "shape_translation": cfg!(feature = "shape-xlat"),
127 })
128}
129
130fn extensions() -> Value {
134 let (read_modes, compressors, chunkers) = crate::core::extension_registry::global()
135 .read()
136 .map(|r| (r.read_mode_names(), r.compressor_names(), r.chunker_names()))
137 .unwrap_or_default();
138
139 json!({
140 "plugins": [],
141 "tools": [],
142 "read_modes": read_modes,
143 "compressors": compressors,
144 "chunkers": chunkers,
145 })
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn payload_has_exactly_documented_top_level_keys() {
154 let v = capabilities_value();
155 let obj = v.as_object().expect("capabilities is an object");
156 let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
157 keys.sort_unstable();
158 let mut expected: Vec<&str> = TOP_LEVEL_KEYS.to_vec();
159 expected.sort_unstable();
160 assert_eq!(keys, expected, "top-level keys drifted from TOP_LEVEL_KEYS");
161 }
162
163 #[test]
164 fn contract_version_matches_constant() {
165 let v = capabilities_value();
166 assert_eq!(v["contract_version"], json!(CAPABILITIES_CONTRACT_VERSION));
167 }
168
169 #[test]
170 fn extensions_expose_registry_builtins() {
171 let v = capabilities_value();
172 let ext = &v["extensions"];
173 assert!(ext["plugins"].is_array());
174 let compressors = ext["compressors"].as_array().expect("compressors array");
175 assert!(compressors.iter().any(|c| c == "identity"));
176 assert!(
177 ext["read_modes"]
178 .as_array()
179 .is_some_and(|a| a.iter().any(|m| m == "full"))
180 );
181 assert!(
182 ext["chunkers"]
183 .as_array()
184 .is_some_and(|a| a.iter().any(|c| c == "lines"))
185 );
186 }
187
188 #[test]
189 fn every_advertised_feature_has_an_explicit_product_record() {
190 let v = capabilities_value();
191 for key in v["features"].as_object().expect("features object").keys() {
192 assert!(
193 crate::core::product_capabilities::registry()
194 .find(key)
195 .is_some(),
196 "server feature '{key}' lacks product classification"
197 );
198 }
199 }
200
201 #[test]
202 fn community_always_on_features_are_available_without_an_account() {
203 let v = capabilities_value();
204 for key in COMMUNITY_ALWAYS_ON_FEATURES {
205 assert_eq!(
206 v["features"][key],
207 json!(true),
208 "Community capability '{key}' must be available"
209 );
210 let capability = crate::core::product_capabilities::registry()
211 .find(key)
212 .expect("feature classified");
213 assert_eq!(
214 capability.minimum_plan(),
215 crate::core::product_capabilities::Plan::Community
216 );
217 assert!(!capability.account_required);
218 }
219 }
220
221 #[test]
222 fn reports_compiled_features() {
223 let v = capabilities_value();
224 assert_eq!(v["features"]["compression"], json!(true));
226 assert_eq!(v["features"]["savings_ledger"], json!(true));
227 assert_eq!(v["features"]["team_seat_value_v1"], json!(true));
228 assert_eq!(
229 v["contracts"]["leanctx.contract.team_seat_value_v1.schema_version"],
230 json!(1)
231 );
232 assert_eq!(
234 v["features"]["semantic_search"],
235 json!(cfg!(feature = "embeddings"))
236 );
237 }
238}