feat: relax outputSchema to accept non-object JSON Schema types (SEP-2106)#895
feat: relax outputSchema to accept non-object JSON Schema types (SEP-2106)#895branben wants to merge 3 commits into
Conversation
|
thanks @branben if you can run a fmt over this to get CI happy - I think this is then good to merge, thanks! |
just checked on this @michaelneale happy I could contribute! |
| assert!(tool.output_schema.is_some()); | ||
|
|
||
| let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap(); | ||
| assert!(schema_str.contains("\"type\":\"integer\"")); |
There was a problem hiding this comment.
To prevent false positives
| assert!(schema_str.contains("\"type\":\"integer\"")); | |
| assert_eq!(schema.get("type"), Some(&serde_json::json!("integer"))); |
There was a problem hiding this comment.
@branben You marked this as resolved but it still doesn't seem to be fixed!
|
Thanks for the review @DaleSeo! All 4 comments addressed in the latest push:
The Please let me know where else I can make meaningful contributions if there's a specific pain point that's bugging the team! |
|
@branben You might've forgotten to push those fixes? I'm still seeing the original code. 😅 |
47e5d23 to
6588978
Compare
…urns Result - Add strip_output() that strips title/description without validating type (Dale modelcontextprotocol#1) - Change schema_for_output to return Arc<JsonObject> instead of Result (Dale modelcontextprotocol#2) - Cache only Arc<JsonObject> success values, not Result (Dale modelcontextprotocol#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 modelcontextprotocol#4) - Update test_schema_for_output_rejects_primitive to accept_primitive (SEP-2106) Co-authored-by: Orca <help@stably.ai>
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
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 ()
6588978 to
28c2bcc
Compare
| assert!(tool.output_schema.is_some()); | ||
|
|
||
| let schema_str = serde_json::to_string(tool.output_schema.as_ref().unwrap()).unwrap(); | ||
| assert!(schema_str.contains("\"type\":\"integer\"")); |
There was a problem hiding this comment.
@branben You marked this as resolved but it still doesn't seem to be fixed!
| let schema_str = serde_json::to_string(schema).unwrap(); | ||
| assert!(schema_str.contains("integer")); |
There was a problem hiding this comment.
| let schema_str = serde_json::to_string(schema).unwrap(); | |
| assert!(schema_str.contains("integer")); | |
| assert_eq!(schema.get("type"), Some(&serde_json::json!("integer"))); |
| let schema_value: serde_json::Value = serde_json::from_str( | ||
| &serde_json::to_string(&*schema).expect("failed to serialize schema"), | ||
| ) | ||
| .expect("failed to parse schema JSON"); | ||
| assert_eq!(schema_value["type"], "array"); |
There was a problem hiding this comment.
schema is already an Arc<JsonObject> so we can read the key directly.
| let schema_value: serde_json::Value = serde_json::from_str( | |
| &serde_json::to_string(&*schema).expect("failed to serialize schema"), | |
| ) | |
| .expect("failed to parse schema JSON"); | |
| assert_eq!(schema_value["type"], "array"); | |
| assert_eq!(schema.get("type"), Some(&serde_json::json!("array"))); |
| impl SyncTool<TraitBasedToolServer> for ArrayTool { | ||
| fn invoke( | ||
| _service: &TraitBasedToolServer, | ||
| _param: Self::Parameter, | ||
| ) -> Result<Self::Output, Self::Error> { | ||
| Ok(vec![]) | ||
| } | ||
| } | ||
| impl AsyncTool<TraitBasedToolServer> for ArrayTool { | ||
| async fn invoke( | ||
| _service: &TraitBasedToolServer, | ||
| _param: Self::Parameter, | ||
| ) -> Result<Self::Output, Self::Error> { | ||
| Ok(vec![]) | ||
| } | ||
| } |
|
|
||
| /// Generate and strip a JSON schema for outputSchema (top-level "title" and | ||
| /// "description" are removed; output schemas are not restricted to root type "object"). | ||
| pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> { |
There was a problem hiding this comment.
Public API Check is red in CI because the schema_for_output return-type change is a breaking change:
-pub fn ...::schema_for_output<...>() -> Result<Arc<JsonObject>, String>
+pub fn ...::schema_for_output<...>() -> Arc<JsonObject>
Error: The API diff is not allowed as per --deny: Changed items not allowed
That's expected and fine, and the plan is to batch all 2026-07-28 spec breaking changes into v3.0. So this SEP-2106 implementation belongs in that major.
We need to mark the introducing commit breaking though replacing feat: with feat!: so the CI gate computes major and Public API Check passes.
Summary
Relax
schema_for_outputto accept any JSON Schema 2020-12 root type (arrays, primitives, compositions) foroutputSchema, while keepingschema_for_inputenforcingtype: "object". This implements SEP-2106, which was accepted May 18 2026.PR #860 (merged Jun 2 2026) added title/desc stripping and input validation. This PR covers the remaining work: decoupling the type gate so output schemas are no longer rejected for non-object types.
Changes
crates/rmcp/src/handler/server/common.rs: Splitvalidate_and_stripinto:validate_and_strip_input— keepstype: "object"check for inputSchemavalidate_and_strip_output— strips title/desc, no type check (accepts any JSON Schema root type)crates/rmcp/src/model/tool.rs: Updatedwith_output_schemadoc comment to remove incorrect "root type object" panic referenceOption<T>), unit type,Json<T>macro path,ToolBasetrait path, cache correctness, and negative tests for input rejectionBackward Compatibility
schema_for_outputreturn type unchanged (Result<Arc<JsonObject>, String>)Verification
cargo test -p rmcp --all-features: 424 passed, 0 failedcargo clippy --all-targets --all-features -- -D warnings: No issues found