use serde_json::Value;
use crate::invocation::Capture;
const ERRORED: i32 = 1;
pub(super) fn capture(result: &Value) -> Result<Capture, String> {
let o = result
.as_object()
.ok_or("the MCP server's result is not a JSON object")?;
let parts: &[Value] = match o.get("content") {
Some(Value::Array(parts)) => parts,
Some(_) => return Err("the MCP server's \"content\" is not an array".to_owned()),
None => &[],
};
let mut stdout = String::new();
let text = parts.iter().any(is_text);
let errored = o.get("isError") == Some(&Value::Bool(true));
for part in parts {
stdout.push_str(&rendered(part));
stdout.push('\n');
}
if let (false, Some(structured)) = (text, o.get("structuredContent")) {
stdout.push_str(&structured.to_string());
stdout.push('\n');
}
if stdout.is_empty() {
return Err("the MCP server's result carried neither content nor \
structuredContent — it did not answer"
.to_owned());
}
Ok(Capture {
stdout,
stderr: String::new(),
exit_code: if errored { ERRORED } else { 0 },
})
}
fn is_text(part: &Value) -> bool {
part.get("type").and_then(Value::as_str) == Some("text")
}
fn rendered(part: &Value) -> String {
if is_text(part) {
return part
.get("text")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
}
let kind = part.get("type").and_then(Value::as_str).unwrap_or("part");
let mime = part
.get("mimeType")
.or(part.pointer("/resource/mimeType"))
.and_then(Value::as_str)
.unwrap_or("unknown");
format!("[{kind} {mime}, {} bytes dropped]", carried(part))
}
fn carried(part: &Value) -> usize {
let data = part
.get("data")
.or(part.pointer("/resource/blob"))
.and_then(Value::as_str);
match data {
Some(base64) => decoded(base64),
None => part
.pointer("/resource/text")
.and_then(Value::as_str)
.unwrap_or_default()
.len(),
}
}
fn decoded(base64: &str) -> usize {
let padding = base64.chars().rev().take_while(|c| *c == '=').count();
(base64.len() / 4 * 3).saturating_sub(padding)
}