* fix: address PR review - schema_for_output no longer validates or returns Result - Add strip_output() that strips title/description without validating type (Dale #1) - Change schema_for_output to return Arc<JsonObject> instead of Result (Dale #2) - Cache only Arc<JsonObject> success values, not Result (Dale #3) - Remove dead unwrap_or_else panic paths in with_output_schema, ToolBase, and macros - Tighten test assertions from contains to assert_eq on type field (Dale #4) - Update test_schema_for_output_rejects_primitive to accept_primitive (SEP-2106) Co-authored-by: Orca <help@stably.ai> * test(rmcp): add non-object output schema tests for SEP-2106 Add tests verifying schema_for_output accepts non-object types: - test_tool_builder_methods: primitive (i32), array (Vec<String>), option - test_structured_output: tool returning Json<Vec<T>> and Json<i32> - test_json_schema_detection: Json<Vec<T>>, Result<Json<Vec<T>>,E>, Json<String> - tool_traits: ToolBase::output_schema with Vec<AddOutput> output type * test(rmcp): add missing edge case tests from code review Add tests identified during code review: - description stripping for primitive types - composition types (Option<String> with anyOf/oneOf/null) - cache correctness (Arc::ptr_eq for repeated calls) - schema_for_input rejecting array types (not just primitives) - schema_for_output accepting unit type () * feat!: mark schema_for_output return-type change as breaking This introduces SEP-2106: schema_for_output no longer validates or returns Result. The public signature changed, so bump major. * fix: address Dale's PR review - direct schema.get assertions, remove ArrayTool - Replace loose schema_str.contains(...) assertions with direct schema.get("type") equality checks in test_tool_builder_methods.rs and test_structured_output.rs - Remove redundant ArrayTool fixture and its round-trip serde_json::from_str test from tool_traits.rs since schema is already Arc<JsonObject> - Drop dead schema_str variable in test_structured_output.rs --------- Co-authored-by: Brandon Bennett <brandonbennett@macbookair.myfiosgateway.com> Co-authored-by: Orca <help@stably.ai> Co-authored-by: Brandon Bennett <brandonbennett@Pursuits-Air.lan>
188 lines
5.7 KiB
Rust
188 lines
5.7 KiB
Rust
#![allow(clippy::exhaustive_structs)]
|
|
//cargo test --test test_json_schema_detection --features "client server macros"
|
|
use rmcp::{
|
|
Json, ServerHandler, handler::server::router::tool::ToolRouter, tool, tool_handler, tool_router,
|
|
};
|
|
use schemars::JsonSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Serialize, Deserialize, JsonSchema)]
|
|
pub struct TestData {
|
|
pub value: String,
|
|
}
|
|
|
|
#[tool_handler(router = self.tool_router)]
|
|
impl ServerHandler for TestServer {}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TestServer {
|
|
tool_router: ToolRouter<Self>,
|
|
}
|
|
|
|
impl Default for TestServer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[tool_router(router = tool_router)]
|
|
impl TestServer {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
tool_router: Self::tool_router(),
|
|
}
|
|
}
|
|
|
|
/// Tool that returns Json<T> - should have output schema
|
|
#[tool(name = "with-json")]
|
|
pub async fn with_json(&self) -> Result<Json<TestData>, String> {
|
|
Ok(Json(TestData {
|
|
value: "test".to_string(),
|
|
}))
|
|
}
|
|
|
|
/// Tool that returns regular type - should NOT have output schema
|
|
#[tool(name = "without-json")]
|
|
pub async fn without_json(&self) -> Result<String, String> {
|
|
Ok("test".to_string())
|
|
}
|
|
|
|
/// Tool that returns Result with inner Json - should have output schema
|
|
#[tool(name = "result-with-json")]
|
|
pub async fn result_with_json(&self) -> Result<Json<TestData>, rmcp::ErrorData> {
|
|
Ok(Json(TestData {
|
|
value: "test".to_string(),
|
|
}))
|
|
}
|
|
|
|
/// Tool with explicit output_schema attribute - should have output schema
|
|
#[tool(name = "explicit-schema", output_schema = rmcp::handler::server::tool::schema_for_type::<TestData>())]
|
|
pub async fn explicit_schema(&self) -> Result<String, String> {
|
|
Ok("test".to_string())
|
|
}
|
|
|
|
/// Tool that returns Json<Vec<T>> - array output schema
|
|
#[tool(name = "with-json-array")]
|
|
pub async fn with_json_array(&self) -> Result<Json<Vec<TestData>>, String> {
|
|
Ok(Json(vec![TestData {
|
|
value: "test".to_string(),
|
|
}]))
|
|
}
|
|
|
|
/// Tool that returns Result<Json<Vec<T>>, ErrorData> - array output schema
|
|
#[tool(name = "result-with-json-array")]
|
|
pub async fn result_with_json_array(&self) -> Result<Json<Vec<TestData>>, rmcp::ErrorData> {
|
|
Ok(Json(vec![TestData {
|
|
value: "test".to_string(),
|
|
}]))
|
|
}
|
|
|
|
/// Tool that returns Json<String> - string output schema
|
|
#[tool(name = "with-json-string")]
|
|
pub async fn with_json_string(&self) -> Result<Json<String>, String> {
|
|
Ok(Json("test".to_string()))
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_json_type_generates_schema() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
// Find the with-json tool
|
|
let json_tool = tools.iter().find(|t| t.name == "with-json").unwrap();
|
|
assert!(
|
|
json_tool.output_schema.is_some(),
|
|
"Json<T> return type should generate output schema"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_non_json_type_no_schema() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
// Find the without-json tool
|
|
let non_json_tool = tools.iter().find(|t| t.name == "without-json").unwrap();
|
|
assert!(
|
|
non_json_tool.output_schema.is_none(),
|
|
"Regular return type should NOT generate output schema"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_result_with_json_generates_schema() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
// Find the result-with-json tool
|
|
let result_json_tool = tools.iter().find(|t| t.name == "result-with-json").unwrap();
|
|
assert!(
|
|
result_json_tool.output_schema.is_some(),
|
|
"Result<Json<T>, E> return type should generate output schema"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_explicit_schema_override() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
// Find the explicit-schema tool
|
|
let explicit_tool = tools.iter().find(|t| t.name == "explicit-schema").unwrap();
|
|
assert!(
|
|
explicit_tool.output_schema.is_some(),
|
|
"Explicit output_schema attribute should work"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_json_array_type_generates_schema() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
let array_tool = tools.iter().find(|t| t.name == "with-json-array").unwrap();
|
|
assert!(
|
|
array_tool.output_schema.is_some(),
|
|
"Json<Vec<T>> return type should generate output schema"
|
|
);
|
|
let schema = array_tool.output_schema.as_ref().unwrap();
|
|
assert_eq!(
|
|
schema.get("type").and_then(|v| v.as_str()),
|
|
Some("array"),
|
|
"Json<Vec<T>> should produce an array schema"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_result_with_json_array_generates_schema() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
let result_array_tool = tools
|
|
.iter()
|
|
.find(|t| t.name == "result-with-json-array")
|
|
.unwrap();
|
|
assert!(
|
|
result_array_tool.output_schema.is_some(),
|
|
"Result<Json<Vec<T>>, ErrorData> return type should generate output schema"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_json_string_type_generates_schema() {
|
|
let server = TestServer::new();
|
|
let tools = server.tool_router.list_all();
|
|
|
|
let string_tool = tools.iter().find(|t| t.name == "with-json-string").unwrap();
|
|
assert!(
|
|
string_tool.output_schema.is_some(),
|
|
"Json<String> return type should generate output schema"
|
|
);
|
|
let schema = string_tool.output_schema.as_ref().unwrap();
|
|
assert_eq!(
|
|
schema.get("type").and_then(|v| v.as_str()),
|
|
Some("string"),
|
|
"Json<String> should produce a string schema"
|
|
);
|
|
}
|