diff --git a/crates/rmcp/src/handler/server/common.rs b/crates/rmcp/src/handler/server/common.rs index 4f344a86..fc3219e7 100644 --- a/crates/rmcp/src/handler/server/common.rs +++ b/crates/rmcp/src/handler/server/common.rs @@ -23,8 +23,10 @@ pub fn schema_for_type() -> Arc { } else { // explicitly to align json schema version to official specifications. // refer to https://github.com/modelcontextprotocol/modelcontextprotocol/pull/655 for details. - let mut settings = SchemaSettings::draft2020_12(); - settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())]; + let settings = SchemaSettings::draft2020_12(); + // Note: AddNullable is intentionally NOT used here because the `nullable` keyword + // is an OpenAPI 3.0 extension, not part of JSON Schema 2020-12. Using it would + // cause validation failures with strict JSON Schema validators. let generator = settings.into_generator(); let schema = generator.into_root_schema_for::(); let object = serde_json::to_value(schema).expect("failed to serialize schema"); diff --git a/crates/rmcp/tests/test_complex_schema.rs b/crates/rmcp/tests/test_complex_schema.rs index 1bc9051a..a9c41a3c 100644 --- a/crates/rmcp/tests/test_complex_schema.rs +++ b/crates/rmcp/tests/test_complex_schema.rs @@ -80,8 +80,10 @@ fn expected_schema() -> serde_json::Value { "type": "array" }, "system": { - "nullable": true, - "type": "string" + "type": [ + "string", + "null" + ] } }, "required": [ diff --git a/crates/rmcp/tests/test_tool_macros.rs b/crates/rmcp/tests/test_tool_macros.rs index a7609eec..837198cb 100644 --- a/crates/rmcp/tests/test_tool_macros.rs +++ b/crates/rmcp/tests/test_tool_macros.rs @@ -238,7 +238,7 @@ fn test_optional_field_schema_generation_via_macro() { ); // Verify the schema generated for the aggregated OptionalFieldTestSchema - // by the macro infrastructure (which should now use OpenAPI 3 settings) + // by the macro infrastructure using JSON Schema 2020-12 settings. let input_schema_map = &*tool_attr.input_schema; // Dereference Arc // Check the schema for the 'description' property within the input schema @@ -253,18 +253,21 @@ fn test_optional_field_schema_generation_via_macro() { .as_object() .unwrap(); - // Assert that the format is now `type: "string", nullable: true` + // Assert nullable Option is represented in JSON Schema 2020-12 form. + let type_value = description_schema + .get("type") + .expect("Schema for Option should include a type field"); + let type_array = type_value + .as_array() + .expect("Schema for Option should use a type array [T, null]"); assert_eq!( - description_schema.get("type").map(|v| v.as_str().unwrap()), - Some("string"), - "Schema for Option generated by macro should be type: \"string\"" + type_array, + &vec![serde_json::json!("string"), serde_json::json!("null")], + "Schema for Option should be type: [\"string\", \"null\"]" ); - assert_eq!( - description_schema - .get("nullable") - .map(|v| v.as_bool().unwrap()), - Some(true), - "Schema for Option generated by macro should have nullable: true" + assert!( + description_schema.get("nullable").is_none(), + "Schema for Option should not use OpenAPI nullable in JSON Schema 2020-12" ); // We still check the description is correct assert_eq!( @@ -274,12 +277,8 @@ fn test_optional_field_schema_generation_via_macro() { Some("An optional description field") ); - // Ensure the old 'type: [T, null]' format is NOT used - let type_value = description_schema.get("type").unwrap(); - assert!( - !type_value.is_array(), - "Schema type should not be an array [T, null]" - ); + // Ensure no OpenAPI-only nullable extension was emitted. + assert!(description_schema.get("nullable").is_none()); } // Define a dummy client handler