Skip to content

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

Open
branben wants to merge 4 commits into
modelcontextprotocol:mainfrom
branben:feat/sep-2106-output-schema-non-object
Open

feat: relax outputSchema to accept non-object JSON Schema types (SEP-2106)#895
branben wants to merge 4 commits into
modelcontextprotocol:mainfrom
branben:feat/sep-2106-output-schema-non-object

Conversation

@branben

@branben branben commented Jun 6, 2026

Copy link
Copy Markdown

Summary

Relax schema_for_output to accept any JSON Schema 2020-12 root type (arrays, primitives, compositions) for outputSchema, while keeping schema_for_input enforcing type: "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: Split validate_and_strip into:
    • validate_and_strip_input — keeps type: "object" check for inputSchema
    • validate_and_strip_output — strips title/desc, no type check (accepts any JSON Schema root type)
  • crates/rmcp/src/model/tool.rs: Updated with_output_schema doc comment to remove incorrect "root type object" panic reference
  • Tests: 17 new tests across 4 test files covering primitives, arrays, compositions (Option<T>), unit type, Json<T> macro path, ToolBase trait path, cache correctness, and negative tests for input rejection

Backward Compatibility

  • schema_for_output return type unchanged (Result<Arc<JsonObject>, String>)
  • All existing tests for object output types still pass
  • Change is strictly relaxing: types that were previously rejected are now accepted

Verification

  • cargo test -p rmcp --all-features: 424 passed, 0 failed
  • cargo clippy --all-targets --all-features -- -D warnings: No issues found

Brandon Bennett added 3 commits June 6, 2026 13:36
… (SEP-2106)

Split validate_and_strip into validate_and_strip_input (keeps type:"object"
check for inputSchema) and validate_and_strip_output (accepts any JSON Schema
root type per SEP-2106). Update schema_for_output to use the new output variant.
Flip test_schema_for_output_rejects_primitive to assert Ok and rename to
test_schema_for_output_accepts_primitive. Add test_schema_for_output_accepts_array
and test_schema_for_output_strips_title_for_primitive. Update with_output_schema
doc comment to remove incorrect "root type object" panic reference.
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 ()
@branben branben requested a review from a team as a code owner June 6, 2026 18:39
@github-actions github-actions Bot added T-test Testing related changes T-core Core library changes T-handler Handler implementation changes T-model Model/data structure changes labels Jun 6, 2026
@michaelneale

Copy link
Copy Markdown
Contributor

thanks @branben if you can run a fmt over this to get CI happy - I think this is then good to merge, thanks!

@branben

branben commented Jun 10, 2026

Copy link
Copy Markdown
Author

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!

@DaleSeo DaleSeo linked an issue Jun 23, 2026 that may be closed by this pull request
4 tasks

@DaleSeo DaleSeo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing the sep, @branben! This looks good overall. I have some minor comments.

/// Strip top-level `title`/`description` from a JSON schema for outputSchema.
/// Unlike inputSchema, outputSchema may have any JSON Schema 2020-12 root type
/// (objects, arrays, primitives, compositions) per SEP-2106.
fn validate_and_strip_output(raw: &Arc<JsonObject>) -> Arc<JsonObject> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This performs no validation.

Suggested change
fn validate_and_strip_output(raw: &Arc<JsonObject>) -> Arc<JsonObject> {
fn strip_output(raw: &Arc<JsonObject>) -> Arc<JsonObject> {

/// # Panics
///
/// Panics if the generated schema does not have root type "object" as required by MCP specification.
/// Panics if output schema generation fails.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't likely panic because schema_for_output no longer return Err.

/// Top-level "title" and "description" are always removed.
pub fn schema_for_output<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObject>, String> {
thread_local! {
static CACHE_FOR_OUTPUT: std::sync::RwLock<HashMap<TypeId, Result<Arc<JsonObject>, String>>> = Default::default();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can cache the success value only.

Suggested change
static CACHE_FOR_OUTPUT: std::sync::RwLock<HashMap<TypeId, Result<Arc<JsonObject>, String>>> = Default::default();
static CACHE_FOR_OUTPUT: std::sync::RwLock<HashMap<TypeId, Arc<JsonObject>>> = Default::default();

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\""));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent false positives

Suggested change
assert!(schema_str.contains("\"type\":\"integer\""));
assert_eq!(schema.get("type"), Some(&serde_json::json!("integer")));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2026-07-28 T-core Core library changes T-handler Handler implementation changes T-model Model/data structure changes T-test Testing related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SEP-2106: Tool schemas conform to JSON Schema 2020-12

3 participants