feat: relax outputSchema to accept non-object JSON Schema types (SEP-2106) (#895)

* 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>
This commit is contained in:
Brandon Bennett 2026-07-08 18:19:28 +00:00 • committed by GitHub
commit 9e3de344f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -60,6 +60,28 @@ impl TestServer {
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]
@ -113,3 +135,54 @@ async fn test_explicit_schema_override() {
"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"
);
}