1use anyhow::{Context, Result};
8use colored::*;
9use robonix_atlas::client::AtlasClient;
10use robonix_atlas::pb as atlas_pb;
11use serde_json::Value;
12
13fn transport_name(t: i32) -> &'static str {
14 match atlas_pb::Transport::try_from(t).unwrap_or(atlas_pb::Transport::Unspecified) {
15 atlas_pb::Transport::Grpc => "grpc",
16 atlas_pb::Transport::Ros2 => "ros2",
17 atlas_pb::Transport::Mcp => "mcp",
18 atlas_pb::Transport::Unspecified => "?",
19 }
20}
21
22fn state_name(s: i32) -> &'static str {
23 match atlas_pb::LifecycleState::try_from(s)
24 .unwrap_or(atlas_pb::LifecycleState::StateUnspecified)
25 {
26 atlas_pb::LifecycleState::StateRegistered => "REGISTERED",
27 atlas_pb::LifecycleState::StateInactive => "INACTIVE",
28 atlas_pb::LifecycleState::StateActive => "ACTIVE",
29 atlas_pb::LifecycleState::StateError => "ERROR",
30 atlas_pb::LifecycleState::StateTerminated => "TERMINATED",
31 atlas_pb::LifecycleState::StateUnspecified => "?",
32 }
33}
34
35fn state_tag(s: i32) -> colored::ColoredString {
39 let label = format!("[{}]", state_name(s));
40 match atlas_pb::LifecycleState::try_from(s)
41 .unwrap_or(atlas_pb::LifecycleState::StateUnspecified)
42 {
43 atlas_pb::LifecycleState::StateActive => label.green().bold(),
44 atlas_pb::LifecycleState::StateInactive => label.yellow(),
45 atlas_pb::LifecycleState::StateRegistered => label.blue(),
46 atlas_pb::LifecycleState::StateTerminated => label.dimmed(),
47 atlas_pb::LifecycleState::StateError => label.red().bold(),
48 atlas_pb::LifecycleState::StateUnspecified => label.dimmed(),
49 }
50}
51
52async fn connect(endpoint: &str) -> Result<AtlasClient> {
53 AtlasClient::connect(endpoint)
54 .await
55 .with_context(|| format!("connect to atlas at '{endpoint}'"))
56}
57
58pub async fn providers(endpoint: &str, json: bool, verbose: bool) -> Result<()> {
59 let mut atlas = connect(endpoint).await?;
60 let providers = atlas
61 .query_capabilities("", "", atlas_pb::Transport::Unspecified)
62 .await?;
63 if json {
64 let serialised: Vec<_> = providers
65 .iter()
66 .map(|r| {
67 serde_json::json!({
68 "provider_id": r.id,
69 "namespace": r.namespace,
70 "state": state_name(r.state),
71 "state_detail": r.state_detail,
72 "capabilities": r.capabilities.iter().map(|i| serde_json::json!({
73 "contract_id": i.contract_id,
74 "transport": transport_name(i.transport),
75 })).collect::<Vec<_>>(),
76 })
77 })
78 .collect();
79 println!("{}", serde_json::to_string_pretty(&serialised)?);
80 return Ok(());
81 }
82
83 if providers.is_empty() {
84 println!("{} no providers registered", "[providers]".yellow().bold());
85 return Ok(());
86 }
87 for provider in &providers {
90 let detail = if provider.state_detail.is_empty() {
91 String::new()
92 } else {
93 format!(" — {}", provider.state_detail)
94 };
95 let cap_count_hint = if verbose {
96 String::new()
97 } else {
98 format!(" ({} caps)", provider.capabilities.len())
99 .dimmed()
100 .to_string()
101 };
102 println!(
103 "{} {} {} {}{}{}",
104 "●".green(),
105 provider.id.bold(),
106 state_tag(provider.state),
107 provider.namespace.dimmed(),
108 cap_count_hint,
109 detail.dimmed()
110 );
111 if verbose {
112 for cap in &provider.capabilities {
113 println!(
114 " {} {} {}",
115 "└─".dimmed(),
116 cap.contract_id,
117 format!("({})", transport_name(cap.transport)).dimmed()
118 );
119 }
120 }
121 }
122 if !verbose {
123 println!(
124 "\n{} pass {} for the per-provider capability list",
125 "tip:".dimmed(),
126 "-v".bold()
127 );
128 }
129 Ok(())
130}
131
132pub async fn describe(endpoint: &str, provider_id: Option<&str>, json: bool) -> Result<()> {
133 let mut atlas = connect(endpoint).await?;
134 let cap_filter = provider_id.unwrap_or("");
135 let providers = atlas
136 .query_capabilities(cap_filter, "", atlas_pb::Transport::Unspecified)
137 .await?;
138 if providers.is_empty() {
139 println!("{} no matching providers", "[describe]".yellow().bold());
140 return Ok(());
141 }
142 for provider in &providers {
143 let md = if provider.capability_md_path.is_empty() {
146 String::new()
147 } else {
148 std::fs::read_to_string(&provider.capability_md_path).unwrap_or_default()
149 };
150 if json {
151 let value = serde_json::json!({
152 "provider_id": provider.id,
153 "namespace": provider.namespace,
154 "state": state_name(provider.state),
155 "capabilities": provider.capabilities.iter().map(|i| serde_json::json!({
156 "contract_id": i.contract_id,
157 "transport": transport_name(i.transport),
158 })).collect::<Vec<_>>(),
159 "capability_md": md,
160 });
161 println!("{}", serde_json::to_string_pretty(&value)?);
162 } else {
163 println!(
164 "{} {} {}",
165 "●".green(),
166 provider.id.bold(),
167 state_tag(provider.state)
168 );
169 for cap in &provider.capabilities {
170 println!(
171 " {} {} ({})",
172 "└─".dimmed(),
173 cap.contract_id,
174 transport_name(cap.transport)
175 );
176 }
177 if !md.is_empty() {
178 println!("\n{}", md);
179 }
180 }
181 }
182 Ok(())
183}
184
185pub async fn tools(endpoint: &str, json: bool) -> Result<()> {
186 let mut atlas = connect(endpoint).await?;
188 let providers = atlas
189 .query_capabilities("", "", atlas_pb::Transport::Mcp)
190 .await?;
191 let mut entries: Vec<(String, String, String, String)> = Vec::new();
192 for provider in &providers {
193 for cap in &provider.capabilities {
194 if cap.transport != atlas_pb::Transport::Mcp as i32 {
195 continue;
196 }
197 let schema = match cap.params.as_ref().and_then(|p| p.kind.as_ref()) {
198 Some(atlas_pb::transport_params::Kind::Mcp(m)) => m.input_schema_json.clone(),
199 _ => String::new(),
200 };
201 let description = cap.description.clone();
202 entries.push((
203 provider.id.clone(),
204 cap.contract_id.clone(),
205 description,
206 schema,
207 ));
208 }
209 }
210 if json {
211 let serialised: Vec<_> = entries
212 .iter()
213 .map(|(provider, c, d, s)| {
214 serde_json::json!({
215 "provider_id": provider,
216 "contract_id": c,
217 "description": d,
218 "input_schema_json": s,
219 })
220 })
221 .collect();
222 println!("{}", serde_json::to_string_pretty(&serialised)?);
223 return Ok(());
224 }
225 if entries.is_empty() {
226 println!("{} no MCP tools registered", "[tools]".yellow().bold());
227 return Ok(());
228 }
229 for (provider, c, d, _s) in &entries {
230 let leaf = c.rsplit_once('/').map(|(_, leaf)| leaf).unwrap_or(c);
231 println!(
232 "{} {} {}",
233 "●".green(),
234 leaf.bold(),
235 format!("[{}]", c).dimmed()
236 );
237 println!(" provider : {}", provider.dimmed());
238 if !d.is_empty() {
239 println!(" desc : {}", d);
240 }
241 }
242 Ok(())
243}
244
245pub async fn channels(endpoint: &str) -> Result<()> {
246 let atlas = connect(endpoint).await?;
247 let raw = atlas
248 .inner()
249 .inspect_atlas(atlas_pb::InspectAtlasRequest {})
250 .await
251 .context("InspectAtlas RPC")?
252 .into_inner()
253 .json;
254 let v: Value = serde_json::from_str(&raw).context("parse inspect json")?;
255 let channels = v
256 .get("channels")
257 .and_then(|c| c.as_object())
258 .cloned()
259 .unwrap_or_default();
260 if channels.is_empty() {
261 println!("{} no active channels", "[channels]".yellow().bold());
262 return Ok(());
263 }
264 for (id, ch) in channels.iter() {
265 let consumer = ch
266 .get("consumer_id")
267 .and_then(|x| x.as_str())
268 .unwrap_or("?");
269 let provider = ch
270 .get("provider_id")
271 .and_then(|x| x.as_str())
272 .unwrap_or("?");
273 let contract = ch
274 .get("contract_id")
275 .and_then(|x| x.as_str())
276 .unwrap_or("?");
277 let transport = ch.get("transport").and_then(|x| x.as_str()).unwrap_or("?");
278 let endpoint = ch.get("endpoint").and_then(|x| x.as_str()).unwrap_or("?");
279 println!("{} {}", "●".green(), id.bold());
280 println!(" consumer : {}", consumer);
281 println!(
282 " provider : {} ({} via {})",
283 provider, contract, transport
284 );
285 println!(" endpoint : {}", endpoint.dimmed());
286 }
287 Ok(())
288}
289
290pub async fn contracts(
291 endpoint: &str,
292 prefix: Option<&str>,
293 json: bool,
294 verbose: bool,
295) -> Result<()> {
296 let atlas = connect(endpoint).await?;
297 let resp = atlas
298 .inner()
299 .list_contracts(atlas_pb::ListContractsRequest {
300 namespace_prefix: prefix.unwrap_or("").to_string(),
301 })
302 .await
303 .context("ListContracts RPC")?
304 .into_inner();
305 if json {
306 let arr: Vec<Value> = resp
307 .contracts
308 .iter()
309 .map(|c| {
310 serde_json::json!({
311 "id": c.id,
312 "version": c.version,
313 "kind": c.kind,
314 "mode": c.mode,
315 "io_msg_type": c.io_msg_type,
316 "io_srv_type": c.io_srv_type,
317 "source_toml_path": c.source_toml_path,
318 "msg_fields": c.msg_fields.iter().map(|f| serde_json::json!({
319 "name": f.name, "type_name": f.type_name,
320 "is_primitive": f.is_primitive, "is_array": f.is_array,
321 "array_size": f.array_size,
322 })).collect::<Vec<_>>(),
323 "srv_request_fields": c.srv_request_fields.iter().map(|f| serde_json::json!({
324 "name": f.name, "type_name": f.type_name,
325 "is_primitive": f.is_primitive, "is_array": f.is_array,
326 "array_size": f.array_size,
327 })).collect::<Vec<_>>(),
328 "srv_response_fields": c.srv_response_fields.iter().map(|f| serde_json::json!({
329 "name": f.name, "type_name": f.type_name,
330 "is_primitive": f.is_primitive, "is_array": f.is_array,
331 "array_size": f.array_size,
332 })).collect::<Vec<_>>(),
333 })
334 })
335 .collect();
336 println!("{}", serde_json::to_string_pretty(&arr)?);
337 return Ok(());
338 }
339 if resp.contracts.is_empty() {
340 let label = match prefix {
341 Some(p) if !p.is_empty() => format!(" with prefix '{p}'"),
342 _ => String::new(),
343 };
344 println!(
345 "{} no contracts loaded{label}",
346 "[contracts]".yellow().bold()
347 );
348 return Ok(());
349 }
350 for c in &resp.contracts {
351 let io = if !c.io_msg_type.is_empty() {
352 c.io_msg_type.clone()
353 } else if !c.io_srv_type.is_empty() {
354 c.io_srv_type.clone()
355 } else {
356 "(none)".dimmed().to_string()
357 };
358 println!(
359 "● {} {} {} {}",
360 c.id.bold(),
361 format!("[{}]", c.kind).dimmed(),
362 format!("mode={}", c.mode).cyan(),
363 format!("idl={io}").dimmed(),
364 );
365 if verbose {
366 if !c.msg_fields.is_empty() {
367 for f in &c.msg_fields {
368 let arr = if f.is_array {
369 if f.array_size == 0 {
370 "[]".to_string()
371 } else {
372 format!("[{}]", f.array_size)
373 }
374 } else {
375 String::new()
376 };
377 println!(" {} : {}{arr}", f.name, f.type_name);
378 }
379 }
380 if !c.srv_request_fields.is_empty() || !c.srv_response_fields.is_empty() {
381 println!(" {}", "request:".dimmed());
382 for f in &c.srv_request_fields {
383 println!(" {} : {}", f.name, f.type_name);
384 }
385 println!(" {}", "response:".dimmed());
386 for f in &c.srv_response_fields {
387 println!(" {} : {}", f.name, f.type_name);
388 }
389 }
390 if !c.source_toml_path.is_empty() {
391 println!(" {} {}", "src:".dimmed(), c.source_toml_path.dimmed());
392 }
393 }
394 }
395 if !verbose {
396 println!(
397 "\n{} {} contract(s); pass -v for field schemas + source paths",
398 "[contracts]".green().bold(),
399 resp.contracts.len()
400 );
401 }
402 Ok(())
403}
404
405pub async fn inspect(endpoint: &str) -> Result<()> {
406 let atlas = connect(endpoint).await?;
407 let raw = atlas
408 .inner()
409 .inspect_atlas(atlas_pb::InspectAtlasRequest {})
410 .await
411 .context("InspectAtlas RPC")?
412 .into_inner()
413 .json;
414 println!("{raw}");
415 Ok(())
416}