zagens_runtime_adapters/mcp/format.rs
1use serde_json;
2
3/// Whether an MCP `tools/call` result signals a **tool-level** failure via
4/// the spec's `isError` flag. This is distinct from a JSON-RPC protocol
5/// error (handled in `connection.rs`): a tool can return a successful RPC
6/// response whose payload still represents a failed tool invocation.
7pub fn is_tool_error(result: &serde_json::Value) -> bool {
8 result
9 .get("isError")
10 .and_then(serde_json::Value::as_bool)
11 .unwrap_or(false)
12}
13
14/// Extract the model-facing content from an MCP `tools/call` result.
15///
16/// Concatenates the text of every `text` content block; non-text blocks
17/// (image, resource, …) are rendered as `[<type> content]` placeholders so
18/// the model knows something was returned without flooding the context with
19/// base64. Falls back to pretty-printed JSON when there's no `content` array.
20pub fn extract_tool_content(result: &serde_json::Value) -> String {
21 result
22 .get("content")
23 .and_then(|v| v.as_array())
24 .map_or_else(
25 || serde_json::to_string_pretty(result).unwrap_or_default(),
26 |arr| {
27 arr.iter()
28 .filter_map(|item| match item.get("type")?.as_str()? {
29 "text" => item.get("text")?.as_str().map(String::from),
30 other => Some(format!("[{other} content]")),
31 })
32 .collect::<Vec<_>>()
33 .join("\n")
34 },
35 )
36}
37
38/// Human-readable rendering of a tool result, prefixing `Error:` on failure.
39/// Prefer [`is_tool_error`] + [`extract_tool_content`] on execution paths that
40/// need to map failures onto a structured error type.
41pub fn format_tool_result(result: &serde_json::Value) -> String {
42 let content = extract_tool_content(result);
43 if is_tool_error(result) {
44 format!("Error: {content}")
45 } else {
46 content
47 }
48}