use std::sync::Arc;
use rmcp::handler::server::ServerHandler;
use crate::Ctx;
use crate::errors;
use crate::mcp::{DEFAULT_VERSIONS, Options, handler, transport};
use crate::registry::Registry;
use crate::spec::command::{Command, MCPFieldHint, MCPHints};
use xyz_rust::XyzArgs;
#[derive(XyzArgs)]
struct SumArgs {
#[xyz(desc = "左操作数", required)]
a: i64,
#[xyz(desc = "右操作数", required)]
b: i64,
#[xyz(desc = "全局 10", default = "10")]
k: i32,
}
#[derive(serde::Serialize, xyz_rust::XyzOutput)]
struct SumResp {
#[serde(rename = "sum")]
sum: i64,
}
fn sum(_: &Ctx, in_: &SumArgs) -> errors::Result<SumResp> {
Ok(SumResp { sum: in_.a + in_.b })
}
fn reg() -> Registry {
let reg = Registry::new();
Command::new("math.sum", sum)
.summary("求和")
.description("两个整数相加")
.mcp(MCPHints {
name: String::new(),
description: String::new(),
title: "求和工具".into(),
meta: serde_json::Map::new(),
skip: false,
annotations: vec!["read".into(), "title:求和工具".into(), "destructive".into()],
fields: std::collections::HashMap::from([(
"k".to_string(),
MCPFieldHint {
default: Some(15.into()),
description: None,
},
)]),
})
.register(®)
.unwrap();
reg
}
#[test]
fn versions_validation() {
assert!(transport::validate_versions(&[]).is_ok());
assert!(transport::validate_versions(&["2025-11-25".into()]).is_ok());
assert!(transport::validate_versions(&["2026-07-28".into()]).is_ok());
let err = transport::validate_versions(&["1999-01-01".into()]).unwrap_err();
assert!(
err.to_string().contains("unknown protocol version"),
"{err}"
);
let err = transport::validate_versions(&["".into()]).unwrap_err();
assert!(err.to_string().contains("empty protocol version"), "{err}");
}
#[test]
fn transport_version_constraints() {
let mut opts = Options {
versions: vec![crate::mcp::PROTOCOL_V2026_07_28.into()],
..Default::default()
};
assert!(transport::validate_transport_versions("http", &opts).is_err());
opts.stateless = true;
assert!(transport::validate_transport_versions("http", &opts).is_ok());
let mut s_opts = Options {
versions: vec![crate::mcp::PROTOCOL_V2025_03_26.into()],
..Default::default()
};
assert!(transport::validate_transport_versions("stdio", &s_opts).is_ok());
s_opts.versions = vec![];
assert!(transport::validate_transport_versions("http", &s_opts).is_ok());
}
#[test]
fn default_versions_newest_first() {
assert_eq!(DEFAULT_VERSIONS[0], "2026-07-28");
assert_eq!(DEFAULT_VERSIONS.len(), 5);
}
#[test]
fn build_creates_tool_metadata() {
let mut opts = Options::default();
let srv = handler::build(®(), &opts, Arc::new(Ctx::new())).unwrap();
let tool = srv.get_tool("math.sum").unwrap();
assert_eq!(tool.name, "math.sum");
assert_eq!(tool.description.as_deref(), Some("求和\n\n两个整数相加"));
let inschema = tool.input_schema.clone();
let v: serde_json::Value = serde_json::json!(serde_json::to_value(&*inschema).unwrap());
assert_eq!(v["properties"]["k"]["default"], 15);
assert_eq!(v["properties"]["a"]["type"], "integer");
assert_eq!(v["required"], serde_json::json!(["a", "b"]));
let out = tool.output_schema.as_ref().unwrap();
let ov: serde_json::Value = serde_json::to_value(&**out).unwrap();
assert_eq!(ov["properties"]["sum"]["type"], "integer");
let ann = tool.annotations.as_ref().unwrap();
assert_eq!(ann.read_only_hint, Some(true));
assert_eq!(ann.destructive_hint, Some(true));
assert_eq!(ann.title.as_deref(), Some("求和工具"));
opts.versions = vec!["2025-11-25".into()];
let pinned = handler::build(®(), &opts, Arc::new(Ctx::new())).unwrap();
let supported = pinned.supported_protocol_versions().into_owned();
assert_eq!(supported.len(), 1);
assert_eq!(supported[0].as_str(), "2025-11-25");
}
#[test]
fn impl_defaults() {
let srv = handler::build(®(), &Options::default(), Arc::new(Ctx::new())).unwrap();
let info = srv.get_info();
assert_eq!(info.server_info.name, crate::cli::app::bin_name());
assert_eq!(info.server_info.version, "0.0.0");
}
#[test]
fn parse_args_forms() {
let (t, o) = crate::mcp::args::parse_args(&s2(&[
"http",
"--addr",
":9000",
"--versions=a,b",
"--json-response",
"--stateless",
"--bearer=x",
"--session-timeout",
"90s",
"--name",
"N",
"--server-version",
"V",
]))
.unwrap();
assert_eq!(t, "http");
assert_eq!(o.addr, ":9000");
assert_eq!(o.versions, vec!["a", "b"]);
assert!(o.json_response);
assert!(o.stateless);
assert_eq!(o.bearer_tokens, vec!["x"]);
assert_eq!(o.session_timeout, std::time::Duration::from_secs(90));
assert_eq!(o.name, "N");
assert_eq!(o.version, "V");
let err = crate::mcp::args::parse_args(&s2(&[])).unwrap_err();
assert!(err.to_string().contains("missing transport"), "{err}");
let err = crate::mcp::args::parse_args(&s2(&["x", "y"])).unwrap_err();
assert!(err.to_string().contains("unexpected argument"), "{err}");
let (_, o) = crate::mcp::args::parse_args(&s2(&["http", "--index", "./wiki"])).unwrap();
assert_eq!(o.defaults["index"], "./wiki");
let err = crate::mcp::args::parse_args(&s2(&["--dangling"])).unwrap_err();
assert!(err.to_string().contains("needs an argument"), "{err}");
}
#[test]
fn options_merge_defaults() {
let base = Options {
addr: ":9000".into(),
bearer_tokens: vec!["b".into()],
stateless: true,
..Default::default()
};
let mut from_flags = Options::default();
from_flags.merge_defaults(&base);
assert_eq!(from_flags.addr, ":9000");
assert_eq!(from_flags.bearer_tokens, vec!["b"]);
assert!(from_flags.stateless);
let mut from_flags2 = Options {
addr: ":7777".into(),
..Default::default()
};
from_flags2.merge_defaults(&base);
assert_eq!(from_flags2.addr, ":7777");
}
fn s2(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn block_envelope_passes_through() {
use rmcp::model::CallToolResponse;
let env = serde_json::json!({"content":[
{"type":"text","text":"hello"},
{"type":"image","mimeType":"image/png","data":"aGVsbG8="}
]});
let resp = handler::success_response(env.clone(), None);
let CallToolResponse::Complete(res) = resp else {
panic!("want complete response");
};
assert!(res.is_error.is_none());
match &res.content[0] {
rmcp::model::ContentBlock::Text(t) => assert_eq!(t.text, "hello"),
other => panic!("want text block, got {other:?}"),
}
match &res.content[1] {
rmcp::model::ContentBlock::Image(img) => {
assert_eq!(img.data, "aGVsbG8=");
assert_eq!(img.mime_type, "image/png");
}
other => panic!("want image block, got {other:?}"),
}
assert_eq!(res.structured_content, Some(env));
let resp = handler::success_response(serde_json::json!({"msg": "hi"}), None);
let CallToolResponse::Complete(plain) = resp else {
panic!("want complete response");
};
assert_eq!(plain.content.len(), 1);
assert!(matches!(
plain.content[0],
rmcp::model::ContentBlock::Text(_)
));
assert_eq!(
plain.structured_content,
Some(serde_json::json!({"msg": "hi"}))
);
}
#[test]
fn mcp_name_override() {
use crate::spec::command::Command;
fn hello(_: &Ctx, _: &SumArgs) -> errors::Result<String> {
Ok("hi".to_string())
}
let reg = Registry::new();
Command::new("svc.echo", hello)
.mcp(MCPHints {
name: "sys.echo".into(),
..Default::default()
})
.register(®)
.unwrap();
let srv = handler::build(®, &Options::default(), Arc::new(Ctx::new())).unwrap();
assert!(
srv.get_tool("sys.echo").is_some(),
"override name must be listed"
);
assert!(
srv.get_tool("svc.echo").is_none(),
"original name must not be callable once overridden"
);
let reg2 = Registry::new();
let err = Command::new("svc.echo", hello)
.mcp(MCPHints {
name: "bad name!".into(),
..Default::default()
})
.register(®2)
.unwrap_err();
assert!(
err.to_string().contains("mcp name override"),
"want grammar rejection, got: {err}"
);
}
#[test]
fn result_meta_shape() {
use crate::errors;
use handler::{result_meta, success_response};
use rmcp::model::CallToolResponse;
let meta = result_meta(
"app",
"1.2",
&[("X-Env".to_string(), "test".to_string())],
"math.sum",
7,
None,
);
let v = serde_json::to_value(&meta).unwrap();
let xyz = &v["xyz"];
assert_eq!(xyz["app_name"], "app");
assert_eq!(xyz["app_version"], "1.2");
assert_eq!(xyz["sdk_version"], crate::version::SDK_VERSION);
assert_eq!(xyz["command"], "math.sum");
assert_eq!(xyz["duration_ms"], 7);
assert_eq!(xyz["headers"]["X-Env"], "test");
assert!(xyz.get("error").is_none());
let err = errors::Error::new(errors::Kind::NotFound, "user 7")
.with_code("USER_NOT_FOUND")
.with_detail("user_id", serde_json::json!(7));
let body = errors::error_body(&err);
let meta = result_meta("app", "dev", &[], "svc.get", 3, Some(&body));
let v = serde_json::to_value(&meta).unwrap();
assert_eq!(v["xyz"]["error"]["kind"], "not_found");
assert_eq!(v["xyz"]["error"]["code"], "USER_NOT_FOUND");
assert_eq!(v["xyz"]["error"]["detail"]["user_id"], 7);
let resp = success_response(serde_json::json!("ok"), Some(meta));
let CallToolResponse::Complete(res) = resp else {
panic!("complete");
};
assert!(res.meta.is_some());
}
#[test]
fn rich_error_http_status_override() {
use crate::errors;
let e = errors::Error::new(errors::Kind::Conflict, "duplicate").with_status(410);
assert_eq!(errors::error_status(&e), 410);
let plain = errors::Error::new(errors::Kind::NotFound, "no");
assert_eq!(errors::error_status(&plain), 404);
let wrapped = errors::Error::wrap(errors::Kind::Unavailable, e);
let body = errors::error_body(&wrapped);
assert_eq!(body.code, None); assert_eq!(body.kind, errors::Kind::Unavailable);
}
#[test]
fn tool_metadata_rich_overrides() {
use crate::spec::command::{Command, MCPFieldHint, MCPHints};
#[derive(XyzArgs)]
struct RichArgs {
#[xyz(desc = "原描述")]
k: i32,
}
fn rich(_: &Ctx, _: &RichArgs) -> errors::Result<String> {
Ok("ok".into())
}
let reg = Registry::new();
let mut meta = serde_json::Map::new();
meta.insert("team".into(), serde_json::json!("core"));
Command::new("rich.tool", rich)
.summary("摘要")
.description("长描述")
.mcp(MCPHints {
name: String::new(),
description: "覆写描述".into(),
title: "人类标题".into(),
meta,
skip: false,
annotations: vec!["read".into()],
fields: std::collections::HashMap::from([(
"k".to_string(),
MCPFieldHint {
default: None,
description: Some("字段描述覆写".into()),
},
)]),
})
.register(®)
.unwrap();
let srv = handler::build(®, &Options::default(), Arc::new(Ctx::new())).unwrap();
let tool = srv.get_tool("rich.tool").unwrap();
assert_eq!(tool.description.as_deref(), Some("覆写描述"));
let ann = tool.annotations.as_ref().expect("annotations");
assert_eq!(ann.title.as_deref(), Some("人类标题"));
let meta = tool.meta.as_ref().expect("tool _meta");
assert_eq!(meta.0["team"], "core");
let v: serde_json::Value = serde_json::to_value(&*tool.input_schema).unwrap();
assert_eq!(v["properties"]["k"]["description"], "字段描述覆写");
}