use serde_json::{json, Map, Value};
pub fn envelope(kind: &str, version: u32, properties: Value, required: &[&str]) -> Value {
let mut props = Map::new();
props.insert(
"v".into(),
json!({ "const": version, "description": "Payload schema version." }),
);
props.insert(
"kind".into(),
json!({ "const": kind, "description": "Payload discriminator — the tool name." }),
);
props.insert(
"tool_error".into(),
json!({
"type": "object",
"description": "Business failure — the tool ran and said \"no\". Absent on \
success. (At the MCP boundary a business failure surfaces as an \
isError:true result instead of riding inside a success payload.)",
"properties": {
"code": { "type": "string", "description": "Stable machine code, e.g. no_live_app." },
"message": { "type": "string" }
},
"required": ["code", "message"]
}),
);
if let Value::Object(payload) = properties {
for (key, prop) in payload {
props.insert(key, prop);
}
}
let mut req: Vec<Value> = vec![json!("v"), json!("kind")];
req.extend(required.iter().map(|k| json!(k)));
json!({ "type": "object", "properties": props, "required": req })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_adds_v_kind_and_optional_tool_error() {
let s = envelope(
"demo",
3,
json!({ "answer": { "type": "integer" } }),
&["answer"],
);
assert_eq!(s["type"], "object");
assert_eq!(s["properties"]["v"]["const"], 3);
assert_eq!(s["properties"]["kind"]["const"], "demo");
assert_eq!(s["properties"]["answer"]["type"], "integer");
assert_eq!(s["properties"]["tool_error"]["type"], "object");
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(required, ["v", "kind", "answer"]);
}
#[test]
fn envelope_with_no_payload_requires_only_v_and_kind() {
let s = envelope("bare", 1, json!({}), &[]);
let required: Vec<&str> = s["required"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(required, ["v", "kind"]);
}
}