1use 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 LOCAL_ALWAYS_ON_FEATURES: &[&str] = &[
83 "compression",
84 "caching",
85 "knowledge",
86 "session",
87 "gateway",
88 "sensitivity_floor",
89 "savings_ledger",
90 "audit_trail",
91 "routing",
95];
96
97pub const LOCAL_OPTIONAL_FEATURES: &[&str] = &[
100 "ast_compression",
101 "semantic_search",
102 "http_server",
103 "wasm_runtime",
104 "gateway_server",
107 "shape_translation",
109];
110
111pub const COMMERCIAL_PLANE_FEATURES: &[&str] = &["team_server", "cloud_server"];
114
115fn features() -> Value {
118 json!({
119 "compression": true,
120 "caching": true,
121 "knowledge": true,
122 "session": true,
123 "gateway": true,
124 "sensitivity_floor": true,
125 "savings_ledger": true,
126 "audit_trail": true,
127 "routing": true,
128 "ast_compression": cfg!(feature = "tree-sitter"),
129 "semantic_search": cfg!(feature = "embeddings"),
130 "http_server": cfg!(feature = "http-server"),
131 "wasm_runtime": cfg!(feature = "wasm"),
132 "gateway_server": cfg!(feature = "gateway-server"),
133 "shape_translation": cfg!(feature = "shape-xlat"),
134 "team_server": cfg!(feature = "team-server"),
135 "cloud_server": cfg!(feature = "cloud-server"),
136 })
137}
138
139fn extensions() -> Value {
143 let plugins = crate::core::plugins::PluginManager::with_registry(|reg| {
144 reg.enabled_plugins()
145 .iter()
146 .map(|p| {
147 json!({
148 "name": p.manifest.plugin.name,
149 "version": p.manifest.plugin.version,
150 "permissions": p.manifest.trust.policy().declared_permissions(),
151 })
152 })
153 .collect::<Vec<_>>()
154 })
155 .unwrap_or_default();
156
157 let (read_modes, compressors, chunkers) = crate::core::extension_registry::global()
158 .read()
159 .map(|r| (r.read_mode_names(), r.compressor_names(), r.chunker_names()))
160 .unwrap_or_default();
161
162 let tools: Vec<Value> = crate::core::plugins::PluginManager::tool_specs()
163 .iter()
164 .map(|t| json!({ "name": t.name, "plugin": t.plugin_name }))
165 .collect();
166
167 json!({
168 "plugins": plugins,
169 "tools": tools,
170 "read_modes": read_modes,
171 "compressors": compressors,
172 "chunkers": chunkers,
173 })
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn payload_has_exactly_documented_top_level_keys() {
182 let v = capabilities_value();
183 let obj = v.as_object().expect("capabilities is an object");
184 let mut keys: Vec<&str> = obj.keys().map(String::as_str).collect();
185 keys.sort_unstable();
186 let mut expected: Vec<&str> = TOP_LEVEL_KEYS.to_vec();
187 expected.sort_unstable();
188 assert_eq!(keys, expected, "top-level keys drifted from TOP_LEVEL_KEYS");
189 }
190
191 #[test]
192 fn contract_version_matches_constant() {
193 let v = capabilities_value();
194 assert_eq!(v["contract_version"], json!(CAPABILITIES_CONTRACT_VERSION));
195 }
196
197 #[test]
198 fn lists_real_tools_and_read_modes() {
199 let v = capabilities_value();
200 assert!(
201 v["tools"]["total"].as_u64().unwrap_or(0) > 0,
202 "expected at least one tool"
203 );
204 assert!(v["read_modes"]["modes"].is_array());
205 }
206
207 #[test]
208 fn extensions_expose_registry_builtins() {
209 let v = capabilities_value();
210 let ext = &v["extensions"];
211 assert!(ext["plugins"].is_array());
212 let compressors = ext["compressors"].as_array().expect("compressors array");
213 assert!(compressors.iter().any(|c| c == "identity"));
214 assert!(
215 ext["read_modes"]
216 .as_array()
217 .is_some_and(|a| a.iter().any(|m| m == "full"))
218 );
219 assert!(
220 ext["chunkers"]
221 .as_array()
222 .is_some_and(|a| a.iter().any(|c| c == "lines"))
223 );
224 }
225
226 #[test]
227 fn feature_keys_partition_into_local_and_commercial() {
228 let v = capabilities_value();
232 let keys: std::collections::BTreeSet<String> = v["features"]
233 .as_object()
234 .expect("features object")
235 .keys()
236 .cloned()
237 .collect();
238 let mut classified: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
239 for k in LOCAL_ALWAYS_ON_FEATURES
240 .iter()
241 .chain(LOCAL_OPTIONAL_FEATURES)
242 .chain(COMMERCIAL_PLANE_FEATURES)
243 {
244 classified.insert((*k).to_string());
245 }
246 assert_eq!(
247 keys, classified,
248 "every feature must be classified local vs commercial (Local-Free Invariant)"
249 );
250 }
251
252 #[test]
253 fn local_always_on_features_are_unconditionally_true() {
254 let v = capabilities_value();
255 for key in LOCAL_ALWAYS_ON_FEATURES {
256 assert_eq!(
257 v["features"][key],
258 json!(true),
259 "local capability '{key}' must be free + always on"
260 );
261 }
262 }
263
264 #[test]
265 fn reports_compiled_features() {
266 let v = capabilities_value();
267 assert_eq!(v["features"]["compression"], json!(true));
269 assert_eq!(v["features"]["savings_ledger"], json!(true));
270 assert_eq!(
272 v["features"]["semantic_search"],
273 json!(cfg!(feature = "embeddings"))
274 );
275 }
276}