use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use subako_function_sdk::{Answer, MANIFEST_FLAG, ToolError, function, package};
#[derive(Deserialize, JsonSchema)]
struct GreetInput {
name: String,
#[serde(default)]
shout: bool,
}
#[derive(Serialize, JsonSchema)]
struct Greeting {
text: String,
}
#[function]
fn greet(input: GreetInput) -> Result<Greeting, ToolError> {
let text = format!("hello {}", input.name);
Ok(Greeting {
text: match input.shout {
true => text.to_uppercase(),
false => text,
},
})
}
mod tools {
use super::*;
#[derive(Deserialize, JsonSchema)]
pub struct Nothing {}
#[function]
pub fn fail(_: Nothing) -> Result<Greeting, String> {
Err("no".to_string())
}
#[function]
pub fn show(_: Nothing) -> Result<Answer<Greeting>, ToolError> {
Ok(Answer::new(Greeting {
text: "look".to_string(),
})
.with_image_file("/volume/tmp/picture.png"))
}
}
package! {
name: "hello",
description: concat!("Says ", "hello."),
functions: [greet, tools::fail, tools::show],
main: false,
}
fn answer(value: Value) -> Value {
json!({"content": [{"type": "text", "text": value.to_string()}], "structuredContent": value})
}
fn failure(message: &str) -> Value {
json!({"content": [{"type": "text", "text": message}], "isError": true})
}
fn answered(method: &str, mut params: Value) -> Value {
params["_meta"] = json!({
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
});
let request = json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params});
let (out, code) = package().execute(&["hello".to_string()], format!("{request}\n").as_bytes());
assert_eq!(code, 0);
let response: Value = serde_json::from_slice(&out).unwrap();
assert_eq!(response["id"], 1);
let mut result = response["result"].clone();
let result = result.as_object_mut().unwrap();
assert_eq!(result.remove("resultType"), Some(json!("complete")));
assert_eq!(
result.remove("_meta"),
Some(json!({"io.modelcontextprotocol/serverInfo": {
"name": "hello",
"version": env!("CARGO_PKG_VERSION"),
}}))
);
Value::Object(result.clone())
}
fn call(tool: &str, arguments: Value) -> Value {
answered("tools/call", json!({"name": tool, "arguments": arguments}))
}
#[test]
fn tools_list_comes_from_the_functions() {
let text = json!({
"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"],
});
assert_eq!(
answered("tools/list", json!({}))["tools"],
json!([
{
"name": "greet",
"description": "Greet someone by name.",
"inputSchema": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Who to greet."},
"shout": {"type": "boolean", "default": false},
},
"required": ["name"],
},
"outputSchema": text,
},
{
"name": "fail",
"description": "Always fails.",
"inputSchema": {"type": "object"},
"outputSchema": text,
},
{
"name": "show",
"description": "Shows a picture.",
"inputSchema": {"type": "object"},
"outputSchema": text,
},
])
);
}
#[test]
fn the_manifest_names_the_abi_and_the_package() {
let args = ["hello".to_string(), MANIFEST_FLAG.to_string()];
let (manifest, code) = package().execute(&args, b"");
assert_eq!(code, 0);
let manifest: Value = serde_json::from_slice(&manifest).unwrap();
assert_eq!(
manifest,
json!({"abi": 1, "name": "hello", "description": "Says hello."})
);
}
#[test]
fn a_call_runs_the_named_function() {
assert_eq!(
call("greet", json!({"name": "ada", "shout": true})),
answer(json!({"text": "HELLO ADA"}))
);
}
#[test]
fn an_answer_shows_its_json_then_its_blocks() {
assert_eq!(
call("show", json!({})),
json!({
"content": [
{"type": "text", "text": r#"{"text":"look"}"#},
{"type": "resource_link", "uri": "volume:///tmp/picture.png", "name": "picture.png"},
],
"structuredContent": {"text": "look"},
})
);
}
#[test]
fn a_function_error_reaches_the_model() {
assert_eq!(call("fail", json!({})), failure("no"));
}
#[test]
fn bad_input_and_unknown_tools_are_tool_errors() {
let out = call("greet", json!({"shout": true}));
assert_eq!(out["isError"], true);
assert!(
out["content"][0]["text"]
.as_str()
.unwrap()
.starts_with("invalid input")
);
assert_eq!(call("nope", json!({})), failure("unknown tool: nope"));
}
#[test]
fn the_function_stays_callable() {
let greeting = greet(GreetInput {
name: "ada".to_string(),
shout: false,
})
.unwrap();
assert_eq!(greeting.text, "hello ada");
assert_eq!(
package()
.functions()
.iter()
.map(|f| f.name())
.collect::<Vec<_>>(),
["greet", "fail", "show"]
);
}