use wordval::prelude::*;
use wordval::serde_json::{json, Value as JsonValue};
use wordval::axum;
#[derive(Debug, serde::Deserialize)]
struct CreateUserReq {
name: String,
email: Option<String>,
}
impl wordval::wordval_agent::tools::schema::JsonSchema for CreateUserReq {
fn json_schema() -> JsonValue {
json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
},
"required": ["name"],
})
}
}
#[wordval::post("/api/user/create")]
#[wordval::tools]
async fn create_user(Json(req): Json<CreateUserReq>) -> ResResult<JsonValue> {
Ok(Res::ok(json!({"id": 1, "name": req.name})))
}
#[wordval::tools(description = "按 ID 查询用户")]
async fn get_user(id: u64, verbose: Option<bool>) -> Res<JsonValue> {
if verbose.unwrap_or(false) {
Res::ok(json!({"id": id, "detail": "full"}))
} else {
Res::ok(json!({"id": id}))
}
}
fn find_tool(name: &str) -> Box<dyn wordval::wordval_agent::tools::Tool> {
wordval::wordval_agent::tools::registry::registered_tools()
.into_iter()
.find(|t| t.name() == name)
.unwrap_or_else(|| panic!("工具 {name} 未注册"))
}
fn ctx() -> wordval::wordval_agent::tools::ToolCtx {
wordval::wordval_agent::tools::ToolCtx::new(std::env::temp_dir(), 0)
}
#[test]
fn macro_keeps_original_fn() {
let _ = create_user;
let _ = get_user;
}
#[test]
fn schema_generated() {
let s = find_tool("create_user").parameters_schema();
assert_eq!(s["properties"]["name"]["type"], "string");
assert_eq!(s["properties"]["email"]["type"], "string");
assert!(s["required"].as_array().unwrap().contains(&json!("name")));
let g = find_tool("get_user");
assert_eq!(g.description(), "按 ID 查询用户");
let gs = g.parameters_schema();
assert_eq!(gs["properties"]["id"]["type"], "integer");
assert!(gs["required"].as_array().unwrap().contains(&json!("id")));
assert!(!gs["required"]
.as_array()
.unwrap()
.contains(&json!("verbose")));
}
#[tokio::test]
async fn execute_ok() {
let r = find_tool("create_user")
.execute(&json!({ "name": "张三" }), &ctx())
.await;
assert!(!r.is_error, "{}", r.content);
assert_eq!(r.content, r#"{"id":1,"name":"张三"}"#);
let g = find_tool("get_user");
let r = g.execute(&json!({ "id": 7 }), &ctx()).await;
assert!(!r.is_error, "{}", r.content);
assert_eq!(r.content, r#"{"id":7}"#);
let r = g
.execute(&json!({ "id": 7, "verbose": true }), &ctx())
.await;
assert!(!r.is_error, "{}", r.content);
assert_eq!(r.content, r#"{"detail":"full","id":7}"#);
}
#[tokio::test]
async fn execute_errors() {
let g = find_tool("get_user");
let r = g.execute(&json!({}), &ctx()).await;
assert!(
r.is_error && r.content.contains("缺少必需参数: id"),
"{}",
r.content
);
let r = g.execute(&json!({ "id": "x" }), &ctx()).await;
assert!(
r.is_error && r.content.contains("解析失败"),
"{}",
r.content
);
}
async fn post_json(app: &axum::Router, payload: JsonValue) -> (axum::http::StatusCode, JsonValue) {
use axum::body::Body;
use http_body_util::BodyExt;
use tower::ServiceExt;
let resp = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/")
.header("content-type", "application/json")
.body(Body::from(payload.to_string()))
.unwrap(),
)
.await
.unwrap();
let status = resp.status();
let bytes = resp.into_body().collect().await.unwrap().to_bytes();
let v = if bytes.is_empty() {
json!({})
} else {
wordval::serde_json::from_slice(&bytes).unwrap()
};
(status, v)
}
#[tokio::test]
async fn mcp_initialize() {
let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
let (status, v) = post_json(&app, json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": { "name": "test" } }
}))
.await;
assert_eq!(status, 200);
assert_eq!(v["id"], 1);
assert_eq!(v["result"]["protocolVersion"], "2025-03-26");
assert_eq!(v["result"]["serverInfo"]["name"], "wordval");
}
#[tokio::test]
async fn mcp_tools_list_includes_macro_tools() {
let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
let (_, v) = post_json(
&app,
json!({
"jsonrpc": "2.0", "id": 2, "method": "tools/list"
}),
)
.await;
let tools = v["result"]["tools"].as_array().unwrap();
let create = tools
.iter()
.find(|t| t["name"] == "create_user")
.expect("MCP tools/list 应包含 create_user");
assert_eq!(
create["inputSchema"]["properties"]["name"]["type"],
"string"
);
let get = tools
.iter()
.find(|t| t["name"] == "get_user")
.expect("MCP tools/list 应包含 get_user");
assert_eq!(get["description"], "按 ID 查询用户");
}
#[tokio::test]
async fn mcp_tools_call() {
let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
let (_, v) = post_json(
&app,
json!({
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": { "name": "get_user", "arguments": { "id": 9 } }
}),
)
.await;
assert_eq!(v["result"]["content"][0]["type"], "text");
assert_eq!(v["result"]["content"][0]["text"], r#"{"id":9}"#);
assert_eq!(v["result"]["isError"], false);
let (_, v) = post_json(
&app,
json!({
"jsonrpc": "2.0", "id": 4, "method": "tools/call",
"params": { "name": "no_such_tool", "arguments": {} }
}),
)
.await;
assert_eq!(v["error"]["code"], -32602);
}
#[tokio::test]
async fn mcp_unknown_method_and_notification() {
let app = wordval::wordval_agent::mcp_server::McpServer::default().router();
let (_, v) = post_json(
&app,
json!({
"jsonrpc": "2.0", "id": 5, "method": "bogus/method"
}),
)
.await;
assert_eq!(v["error"]["code"], -32601);
let (status, v) = post_json(
&app,
json!({
"jsonrpc": "2.0", "method": "notifications/initialized"
}),
)
.await;
assert_eq!(status, 202);
assert_eq!(v, json!({}));
}