#![cfg(all(
feature = "proto-2026-07-28-rc",
feature = "http-server-volga",
feature = "http-client"
))]
use neva::App;
use neva::types::Json;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, schemars::JsonSchema)]
#[allow(dead_code)]
struct Profile {
name: String,
age: u32,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct Opaque {
blob: String,
}
#[derive(Serialize, schemars::JsonSchema)]
struct Greeting {
message: String,
}
#[neva::tool]
async fn add(a: i32, b: i32) -> i32 {
a + b
}
#[neva::tool]
async fn save_profile(profile: Json<Profile>) -> String {
profile.0.name
}
#[neva::tool]
async fn store(payload: Json<Opaque>) -> String {
payload.0.blob
}
#[neva::tool(
input_schema = r#"{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}"#
)]
async fn search(q: String) -> String {
q
}
#[neva::tool]
async fn make_greeting(name: String) -> Json<Greeting> {
Json(Greeting {
message: format!("hi {name}"),
})
}
#[tokio::test(flavor = "multi_thread")]
async fn tool_macro_emits_json_schema_2020() {
let port = pick_free_port();
let addr = format!("127.0.0.1:{port}");
let app =
App::new().with_options(|opt| opt.with_http(|http| http.bind(&addr).with_endpoint("/mcp")));
let handle = tokio::spawn(async move { app.run().await });
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
let client = reqwest::Client::new();
let url = format!("http://{addr}/mcp");
let list_body = serde_json::json!({
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
});
let resp = client
.post(&url)
.header("MCP-Protocol-Version", "2026-07-28")
.json(&list_body)
.send()
.await
.expect("tools/list failed");
assert!(resp.status().is_success());
let body: serde_json::Value = resp.json().await.unwrap();
let tools = body
.pointer("/result/tools")
.and_then(|v| v.as_array())
.expect("missing tools array");
let by_name = |name: &str| -> serde_json::Value {
tools
.iter()
.find(|t| t["name"] == serde_json::json!(name))
.unwrap_or_else(|| panic!("tool {name} not listed"))
.clone()
};
let add = by_name("add");
assert_eq!(add["inputSchema"]["type"], serde_json::json!("object"));
assert_eq!(
add["inputSchema"]["properties"]["a"]["type"],
serde_json::json!("number")
);
assert_eq!(
add["inputSchema"]["properties"]["b"]["type"],
serde_json::json!("number")
);
let req: Vec<String> = serde_json::from_value(add["inputSchema"]["required"].clone()).unwrap();
assert!(req.contains(&"a".to_string()) && req.contains(&"b".to_string()));
let save = by_name("save_profile");
let profile_schema = &save["inputSchema"]["properties"]["profile"];
assert_eq!(profile_schema["type"], serde_json::json!("object"));
assert!(profile_schema["properties"]["name"].is_object());
assert!(profile_schema["properties"]["age"].is_object());
let save_str = serde_json::to_string(&save["inputSchema"]).unwrap();
assert!(!save_str.contains("$ref"), "must be inlined: {save_str}");
assert!(!save_str.contains("$defs"), "must be inlined: {save_str}");
let store = by_name("store");
assert_eq!(
store["inputSchema"]["properties"]["payload"],
serde_json::json!({ "type": "object" })
);
let search = by_name("search");
assert_eq!(
search["inputSchema"]["properties"]["q"]["type"],
serde_json::json!("string")
);
let req: Vec<String> =
serde_json::from_value(search["inputSchema"]["required"].clone()).unwrap();
assert_eq!(req, vec!["q".to_string()]);
assert!(
by_name("save_profile")["outputSchema"].is_null(),
"primitive return must not emit outputSchema"
);
let greet = by_name("make_greeting");
assert_eq!(greet["outputSchema"]["type"], serde_json::json!("object"));
assert!(greet["outputSchema"]["properties"]["message"].is_object());
handle.abort();
}
fn pick_free_port() -> u16 {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
port
}