Skip to content

Commit 24dde29

Browse files
Brandon BennettBrandon Bennett
authored andcommitted
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
1 parent 5b57d8a commit 24dde29

4 files changed

Lines changed: 211 additions & 0 deletions

File tree

crates/rmcp/src/handler/server/router/tool/tool_traits.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,43 @@ mod tests {
340340
assert_eq!(result, ErrorData::invalid_params("invalid params", None));
341341
}
342342
}
343+
344+
struct ArrayTool;
345+
impl ToolBase for ArrayTool {
346+
type Parameter = AddParameter;
347+
type Output = Vec<AddOutput>;
348+
type Error = ErrorData;
349+
350+
fn name() -> Cow<'static, str> {
351+
"array-tool".into()
352+
}
353+
}
354+
impl SyncTool<TraitBasedToolServer> for ArrayTool {
355+
fn invoke(
356+
_service: &TraitBasedToolServer,
357+
_param: Self::Parameter,
358+
) -> Result<Self::Output, Self::Error> {
359+
Ok(vec![])
360+
}
361+
}
362+
impl AsyncTool<TraitBasedToolServer> for ArrayTool {
363+
async fn invoke(
364+
_service: &TraitBasedToolServer,
365+
_param: Self::Parameter,
366+
) -> Result<Self::Output, Self::Error> {
367+
Ok(vec![])
368+
}
369+
}
370+
371+
#[test]
372+
fn test_toolbase_output_schema_with_array_output() {
373+
let schema = ArrayTool::output_schema();
374+
assert!(schema.is_some());
375+
let schema = schema.unwrap();
376+
let schema_value: serde_json::Value = serde_json::from_str(
377+
&serde_json::to_string(&*schema).expect("failed to serialize schema"),
378+
)
379+
.expect("failed to parse schema JSON");
380+
assert_eq!(schema_value["type"], "array");
381+
}
343382
}

crates/rmcp/tests/test_json_schema_detection.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ impl TestServer {
6060
pub async fn explicit_schema(&self) -> Result<String, String> {
6161
Ok("test".to_string())
6262
}
63+
64+
/// Tool that returns Json<Vec<T>> - array output schema
65+
#[tool(name = "with-json-array")]
66+
pub async fn with_json_array(&self) -> Result<Json<Vec<TestData>>, String> {
67+
Ok(Json(vec![TestData {
68+
value: "test".to_string(),
69+
}]))
70+
}
71+
72+
/// Tool that returns Result<Json<Vec<T>>, ErrorData> - array output schema
73+
#[tool(name = "result-with-json-array")]
74+
pub async fn result_with_json_array(&self) -> Result<Json<Vec<TestData>>, rmcp::ErrorData> {
75+
Ok(Json(vec![TestData {
76+
value: "test".to_string(),
77+
}]))
78+
}
79+
80+
/// Tool that returns Json<String> - string output schema
81+
#[tool(name = "with-json-string")]
82+
pub async fn with_json_string(&self) -> Result<Json<String>, String> {
83+
Ok(Json("test".to_string()))
84+
}
6385
}
6486

6587
#[tokio::test]
@@ -113,3 +135,57 @@ async fn test_explicit_schema_override() {
113135
"Explicit output_schema attribute should work"
114136
);
115137
}
138+
139+
#[tokio::test]
140+
async fn test_json_array_type_generates_schema() {
141+
let server = TestServer::new();
142+
let tools = server.tool_router.list_all();
143+
144+
let array_tool = tools.iter().find(|t| t.name == "with-json-array").unwrap();
145+
assert!(
146+
array_tool.output_schema.is_some(),
147+
"Json<Vec<T>> return type should generate output schema"
148+
);
149+
let schema = array_tool.output_schema.as_ref().unwrap();
150+
assert_eq!(
151+
schema.get("type").and_then(|v| v.as_str()),
152+
Some("array"),
153+
"Json<Vec<T>> should produce an array schema"
154+
);
155+
}
156+
157+
#[tokio::test]
158+
async fn test_result_with_json_array_generates_schema() {
159+
let server = TestServer::new();
160+
let tools = server.tool_router.list_all();
161+
162+
let result_array_tool = tools
163+
.iter()
164+
.find(|t| t.name == "result-with-json-array")
165+
.unwrap();
166+
assert!(
167+
result_array_tool.output_schema.is_some(),
168+
"Result<Json<Vec<T>>, ErrorData> return type should generate output schema"
169+
);
170+
}
171+
172+
#[tokio::test]
173+
async fn test_json_string_type_generates_schema() {
174+
let server = TestServer::new();
175+
let tools = server.tool_router.list_all();
176+
177+
let string_tool = tools
178+
.iter()
179+
.find(|t| t.name == "with-json-string")
180+
.unwrap();
181+
assert!(
182+
string_tool.output_schema.is_some(),
183+
"Json<String> return type should generate output schema"
184+
);
185+
let schema = string_tool.output_schema.as_ref().unwrap();
186+
assert_eq!(
187+
schema.get("type").and_then(|v| v.as_str()),
188+
Some("string"),
189+
"Json<String> should produce a string schema"
190+
);
191+
}

crates/rmcp/tests/test_structured_output.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ impl TestServer {
9393
Err("User not found".to_string())
9494
}
9595
}
96+
97+
/// Tool that returns a list of calculation results
98+
#[tool(name = "calculate-list", description = "Return a list of calculation results")]
99+
pub async fn calculate_list(
100+
&self,
101+
params: Parameters<CalculationRequest>,
102+
) -> Result<Json<Vec<CalculationResult>>, String> {
103+
Ok(Json(vec![CalculationResult {
104+
sum: params.0.a + params.0.b,
105+
product: params.0.a * params.0.b,
106+
}]))
107+
}
108+
109+
/// Tool that returns a count
110+
#[tool(name = "get-count", description = "Return a count")]
111+
pub async fn get_count(&self) -> Result<Json<i32>, String> {
112+
Ok(Json(42))
113+
}
96114
}
97115

98116
#[tokio::test]
@@ -360,3 +378,39 @@ fn test_call_tool_result_deserialize_without_content() {
360378
assert!(result.content.is_empty());
361379
assert!(result.structured_content.is_some());
362380
}
381+
382+
#[tokio::test]
383+
async fn test_tool_with_array_output_schema() {
384+
let server = TestServer::new();
385+
let tools = server.tool_router.list_all();
386+
387+
// Find the calculate-list tool
388+
let calculate_list_tool = tools.iter().find(|t| t.name == "calculate-list").unwrap();
389+
390+
// Verify it has an output schema
391+
assert!(calculate_list_tool.output_schema.is_some());
392+
393+
let schema = calculate_list_tool.output_schema.as_ref().unwrap();
394+
395+
// Check that the schema contains array type
396+
let schema_str = serde_json::to_string(schema).unwrap();
397+
assert!(schema_str.contains("array"));
398+
}
399+
400+
#[tokio::test]
401+
async fn test_tool_with_primitive_output_schema() {
402+
let server = TestServer::new();
403+
let tools = server.tool_router.list_all();
404+
405+
// Find the get-count tool
406+
let get_count_tool = tools.iter().find(|t| t.name == "get-count").unwrap();
407+
408+
// Verify it has an output schema
409+
assert!(get_count_tool.output_schema.is_some());
410+
411+
let schema = get_count_tool.output_schema.as_ref().unwrap();
412+
413+
// Check that the schema contains integer type
414+
let schema_str = serde_json::to_string(schema).unwrap();
415+
assert!(schema_str.contains("integer"));
416+
}

crates/rmcp/tests/test_tool_builder_methods.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,45 @@ fn test_chained_builder_methods() {
6161
Some(&serde_json::json!("object"))
6262
);
6363
}
64+
65+
#[test]
66+
fn test_with_output_schema_primitive() {
67+
let tool = Tool::new("test", "Test tool", JsonObject::new()).with_output_schema::<i32>();
68+
69+
assert!(tool.output_schema.is_some());
70+
71+
let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap();
72+
assert!(schema_str.contains("\"type\":\"integer\""));
73+
// title should be stripped from output schema
74+
assert!(!schema_str.contains("title"));
75+
}
76+
77+
#[test]
78+
fn test_with_output_schema_array() {
79+
let tool = Tool::new("test", "Test tool", JsonObject::new())
80+
.with_output_schema::<Vec<String>>();
81+
82+
assert!(tool.output_schema.is_some());
83+
84+
let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap();
85+
assert!(schema_str.contains("\"type\":\"array\""));
86+
assert!(schema_str.contains("items"));
87+
// title should be stripped from output schema
88+
assert!(!schema_str.contains("title"));
89+
}
90+
91+
#[test]
92+
fn test_with_output_schema_option() {
93+
let tool = Tool::new("test", "Test tool", JsonObject::new())
94+
.with_output_schema::<Option<String>>();
95+
96+
assert!(tool.output_schema.is_some());
97+
98+
let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap();
99+
// Option<String> generates a composition schema (anyOf/oneOf/type array with null)
100+
assert!(
101+
schema_str.contains("anyOf") || schema_str.contains("oneOf") || schema_str.contains("null"),
102+
"Expected composition schema for Option<String>, got: {schema_str}"
103+
);
104+
assert!(!schema_str.contains("title"));
105+
}

0 commit comments

Comments
 (0)