1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct AgentCard {
5 pub name: String,
6 pub description: String,
7 pub url: Option<String>,
8 pub version: String,
9 pub protocol_version: String,
10 pub capabilities: AgentCapabilities,
11 pub skills: Vec<AgentSkill>,
12 pub authentication: AuthenticationInfo,
13 pub default_input_modes: Vec<String>,
14 pub default_output_modes: Vec<String>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AgentCapabilities {
19 pub streaming: bool,
20 pub push_notifications: bool,
21 pub state_transition_history: bool,
22 pub tools: Vec<String>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AgentSkill {
27 pub id: String,
28 pub name: String,
29 pub description: String,
30 pub tags: Vec<String>,
31 pub examples: Vec<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct AuthenticationInfo {
36 pub schemes: Vec<String>,
37}
38
39pub fn generate_agent_card(
43 tools: &[String],
44 version: &str,
45 port: Option<u16>,
46 auth_required: bool,
47) -> AgentCard {
48 let url = port.map(|p| format!("http://127.0.0.1:{p}"));
49 let scheme = if auth_required { "bearer" } else { "none" };
50
51 AgentCard {
52 name: "lean-ctx".to_string(),
53 description: "Context Engineering Infrastructure Layer — intelligent compression, \
54 code knowledge graph, structured agent memory, and semantic code search \
55 via MCP"
56 .to_string(),
57 url,
58 version: version.to_string(),
59 protocol_version: "0.1.0".to_string(),
60 capabilities: AgentCapabilities {
61 streaming: false,
62 push_notifications: false,
63 state_transition_history: true,
64 tools: tools.to_vec(),
65 },
66 skills: build_skills(),
67 authentication: AuthenticationInfo {
68 schemes: vec![scheme.to_string()],
69 },
70 default_input_modes: vec!["text/plain".to_string(), "application/json".to_string()],
71 default_output_modes: vec!["text/plain".to_string(), "application/json".to_string()],
72 }
73}
74
75fn build_skills() -> Vec<AgentSkill> {
76 vec![
77 AgentSkill {
78 id: "context-compression".to_string(),
79 name: "Context Compression".to_string(),
80 description: "Intelligent file and shell output compression with 8 modes, \
81 neural token pipeline, and LITM-aware positioning"
82 .to_string(),
83 tags: vec![
84 "compression".to_string(),
85 "tokens".to_string(),
86 "optimization".to_string(),
87 ],
88 examples: vec![
89 "Read a file with automatic mode selection".to_string(),
90 "Compress shell output preserving key information".to_string(),
91 ],
92 },
93 AgentSkill {
94 id: "code-knowledge-graph".to_string(),
95 name: "Code Knowledge Graph".to_string(),
96 description: "Property graph with tree-sitter deep queries, import resolution, \
97 impact analysis, and architecture detection"
98 .to_string(),
99 tags: vec![
100 "graph".to_string(),
101 "analysis".to_string(),
102 "architecture".to_string(),
103 ],
104 examples: vec![
105 "What breaks if I change function X?".to_string(),
106 "Show me the architecture clusters".to_string(),
107 ],
108 },
109 AgentSkill {
110 id: "agent-memory".to_string(),
111 name: "Structured Agent Memory".to_string(),
112 description: "Semantic project knowledge with cross-session persistence, \
113 embedding-based recall, and lifecycle management"
114 .to_string(),
115 tags: vec![
116 "memory".to_string(),
117 "knowledge".to_string(),
118 "persistence".to_string(),
119 ],
120 examples: vec![
121 "Remember a project pattern for future sessions".to_string(),
122 "Recall what happened last time I deployed".to_string(),
123 ],
124 },
125 AgentSkill {
126 id: "semantic-search".to_string(),
127 name: "Semantic Code Search".to_string(),
128 description: "Hybrid BM25 + dense embedding search with tree-sitter AST-aware \
129 chunking and reciprocal rank fusion"
130 .to_string(),
131 tags: vec![
132 "search".to_string(),
133 "embeddings".to_string(),
134 "code".to_string(),
135 ],
136 examples: vec![
137 "Find code related to authentication handling".to_string(),
138 "Search for error recovery patterns".to_string(),
139 ],
140 },
141 ]
142}
143
144pub fn build_agent_card(project_root: &str, auth_required: bool) -> serde_json::Value {
146 let version = env!("CARGO_PKG_VERSION");
147 let card = generate_agent_card(&default_tool_list(), version, None, auth_required);
148
149 serde_json::json!({
150 "name": card.name,
151 "description": card.description,
152 "url": card.url,
153 "version": card.version,
154 "protocolVersion": card.protocol_version,
155 "provider": {
156 "organization": "LeanCTX",
157 "url": "https://leanctx.com"
158 },
159 "documentationUrl": "https://leanctx.com/docs",
160 "capabilities": {
161 "streaming": card.capabilities.streaming,
162 "pushNotifications": card.capabilities.push_notifications,
163 "stateTransitionHistory": card.capabilities.state_transition_history,
164 "tools": card.capabilities.tools,
165 },
166 "skills": card.skills.iter().map(|s| serde_json::json!({
167 "id": s.id,
168 "name": s.name,
169 "description": s.description,
170 "tags": s.tags,
171 "examples": s.examples,
172 "inputModes": ["text/plain", "application/json"],
173 "outputModes": ["text/plain", "application/json"],
174 })).collect::<Vec<_>>(),
175 "authentication": {
176 "schemes": card.authentication.schemes,
177 },
178 "defaultInputModes": card.default_input_modes,
179 "defaultOutputModes": card.default_output_modes,
180 "supportsAuthenticatedExtendedCard": false,
181 "projectRoot": project_root,
182 })
183}
184
185pub fn advertised_tools(names: &[&str]) -> Vec<String> {
191 let registered = crate::core::tool_catalog::registered_tool_names();
192 names
193 .iter()
194 .filter(|name| {
195 registered.iter().any(|r| r == *name)
196 && crate::core::dynamic_tools::is_publicly_advertised_tool(name)
197 })
198 .map(|name| (*name).to_string())
199 .collect()
200}
201
202fn default_tool_list() -> Vec<String> {
203 advertised_tools(&[
204 "ctx_read",
205 "ctx_shell",
206 "ctx_search",
207 "ctx_tree",
208 "ctx_session",
209 "ctx_knowledge",
210 "ctx_compose",
211 "ctx_graph",
212 "ctx_pack",
213 ])
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn generates_valid_card() {
222 let tools = vec!["ctx_read".to_string(), "ctx_shell".to_string()];
223 let card = generate_agent_card(&tools, "3.0.0", Some(3344), false);
224
225 assert_eq!(card.name, "lean-ctx");
226 assert_eq!(card.capabilities.tools.len(), 2);
227 assert_eq!(card.skills.len(), 4);
228 assert_eq!(card.url, Some("http://127.0.0.1:3344".to_string()));
229 assert_eq!(card.authentication.schemes, vec!["none"]);
230 }
231
232 #[test]
233 fn card_serializes_to_valid_json() {
234 let card = generate_agent_card(&["ctx_read".to_string()], "3.0.0", None, false);
235 let json = serde_json::to_string_pretty(&card).unwrap();
236 assert!(json.contains("lean-ctx"));
237 assert!(json.contains("context-compression"));
238
239 let parsed: AgentCard = serde_json::from_str(&json).unwrap();
240 assert_eq!(parsed.name, card.name);
241 }
242
243 #[test]
245 fn auth_scheme_follows_the_server_token() {
246 assert_eq!(
247 build_agent_card("p", true)["authentication"]["schemes"],
248 serde_json::json!(["bearer"])
249 );
250 assert_eq!(
251 build_agent_card("p", false)["authentication"]["schemes"],
252 serde_json::json!(["none"])
253 );
254 }
255
256 }