diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 78f109365..54de05f37 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -1116,6 +1116,14 @@ impl EnumSchema { #[serde(rename_all = "camelCase", into = "ElicitationSchemaWire")] #[non_exhaustive] pub struct ElicitationSchema { + /// Optional JSON Schema dialect identifier (the `$schema` keyword). + /// + /// The 2025-11-25 protocol revision allows a `requestedSchema` to declare its + /// dialect. It is preserved verbatim so a declared dialect survives a + /// decode/re-encode round-trip instead of being silently dropped. + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + pub schema: Option>, + /// Always "object" for elicitation schemas #[serde(rename = "type")] pub type_: ObjectTypeConst, @@ -1144,6 +1152,8 @@ pub struct ElicitationSchema { #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct ElicitationSchemaWire { + #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")] + schema: Option>, #[serde(rename = "type")] type_: ObjectTypeConst, #[serde(skip_serializing_if = "Option::is_none")] @@ -1158,6 +1168,7 @@ struct ElicitationSchemaWire { impl From for ElicitationSchema { fn from(schema: ElicitationSchemaWire) -> Self { Self { + schema: schema.schema, type_: schema.type_, title: schema.title, property_order: Some(schema.properties.keys().cloned().collect()), @@ -1183,6 +1194,7 @@ impl From for ElicitationSchemaWire { properties.extend(remaining); Self { + schema: schema.schema, type_: schema.type_, title: schema.title, properties, @@ -1206,6 +1218,7 @@ impl ElicitationSchema { pub fn new(properties: BTreeMap) -> Self { let property_order = Some(properties.keys().cloned().collect()); Self { + schema: None, type_: ObjectTypeConst, title: None, properties, @@ -1301,6 +1314,12 @@ impl ElicitationSchema { self } + /// Set the JSON Schema dialect identifier (the `$schema` keyword). + pub fn with_schema(mut self, schema: impl Into>) -> Self { + self.schema = Some(schema.into()); + self + } + /// Create a builder for constructing elicitation schemas fluently pub fn builder() -> ElicitationSchemaBuilder { ElicitationSchemaBuilder::new() @@ -1703,6 +1722,7 @@ impl ElicitationSchemaBuilder { let property_order = Some(self.properties.keys().cloned().collect()); Ok(ElicitationSchema { + schema: None, type_: ObjectTypeConst, title: self.title, properties: self.properties, @@ -1902,6 +1922,54 @@ mod tests { Ok(()) } + #[test] + fn test_elicitation_schema_preserves_schema_dialect_roundtrip() -> anyhow::Result<()> { + // Regression test for #1168: a top-level `$schema` dialect declaration on a + // `requestedSchema` was silently dropped, because the wire bridge struct had + // no field to hold it. It must survive a decode/re-encode round-trip. + let input = serde_json::json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + } + }); + let schema: ElicitationSchema = serde_json::from_value(input.clone())?; + assert_eq!( + schema.schema.as_deref(), + Some("https://json-schema.org/draft/2020-12/schema"), + ); + let output = serde_json::to_value(&schema)?; + assert_eq!(output, input); + Ok(()) + } + + #[test] + fn test_elicitation_schema_omits_schema_dialect_when_absent() -> anyhow::Result<()> { + // A schema with no dialect must not emit a `$schema` key (no `"$schema": null`). + let input = serde_json::json!({ + "type": "object", + "properties": { "name": { "type": "string" } } + }); + let schema: ElicitationSchema = serde_json::from_value(input)?; + assert!(schema.schema.is_none()); + let json = serde_json::to_value(&schema)?; + assert!(json.get("$schema").is_none()); + Ok(()) + } + + #[test] + fn test_elicitation_schema_with_schema_setter_serializes_dialect() -> anyhow::Result<()> { + let schema = ElicitationSchema::new(BTreeMap::new()) + .with_schema("https://json-schema.org/draft/2020-12/schema"); + let json = serde_json::to_value(&schema)?; + assert_eq!( + json["$schema"], + "https://json-schema.org/draft/2020-12/schema" + ); + Ok(()) + } + #[test] fn test_legacy_enum_schema_no_enum_names_omits_field() -> anyhow::Result<()> { // `LegacyEnumSchema` with `enum_names: None` must not serialize `"enumNames": null`.