From 60b1e21f11923f6b169d4a05363d9c3685fc9cdb Mon Sep 17 00:00:00 2001 From: Pavel Bezglasnyy Date: Thu, 13 Nov 2025 23:07:49 +0100 Subject: [PATCH 1/7] feat(elicitation) implement SEP-1330 Elicitation Enum Schema Improvements and Standards Compliance --- crates/rmcp/src/model/elicitation_schema.rs | 632 ++++++++++++++++-- crates/rmcp/tests/test_elicitation.rs | 2 +- .../server_json_rpc_message_schema.json | 364 ++++++++-- ...erver_json_rpc_message_schema_current.json | 364 ++++++++-- examples/servers/Cargo.toml | 4 + .../servers/src/elicitation_enum_inference.rs | 184 +++++ 6 files changed, 1404 insertions(+), 146 deletions(-) create mode 100644 examples/servers/src/elicitation_enum_inference.rs diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 095e28e9..953b5a77 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -32,6 +32,7 @@ const_string!(NumberTypeConst = "number"); const_string!(IntegerTypeConst = "integer"); const_string!(BooleanTypeConst = "boolean"); const_string!(EnumTypeConst = "string"); +const_string!(ArrayTypeConst = "array"); // ============================================================================= // PRIMITIVE SCHEMA DEFINITIONS @@ -45,6 +46,9 @@ const_string!(EnumTypeConst = "string"); #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum PrimitiveSchema { + // Note: Enum must be first to avoid ambiguity during deserialization + /// Enum property (explicit enum schema) + Enum(EnumSchema), /// String property (with optional enum constraint) String(StringSchema), /// Number property (with optional enum constraint) @@ -53,8 +57,6 @@ pub enum PrimitiveSchema { Integer(IntegerSchema), /// Boolean property Boolean(BooleanSchema), - /// Enum property (explicit enum schema) - Enum(EnumSchema), } // ============================================================================= @@ -466,62 +468,411 @@ impl BooleanSchema { /// Schema definition for enum properties. /// -/// Compliant with MCP 2025-06-18 specification for elicitation schemas. -/// Enums must have string type and can optionally include human-readable names. +/// Represent single entry for titled item #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ConstTitle { + #[serde(rename = "const")] + pub const_: String, + pub title: String, +} + +/// Legacy enum schema, keep for backward compatibility +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct EnumSchema { - /// Type discriminator (always "string" for enums) +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct LegacyEnumSchema { #[serde(rename = "type")] pub type_: StringTypeConst, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, + #[serde(rename = "enum")] + pub enum_: Vec, + pub enum_names: Option>, +} - /// Allowed enum values (string values only per MCP spec) +/// Untitled single-select +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct UntitledSingleSelectEnumSchema { + #[serde(rename = "type")] + pub type_: StringTypeConst, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, #[serde(rename = "enum")] - pub enum_values: Vec, + pub enum_: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, +} - /// Optional human-readable names for each enum value +/// Titled single-select +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct TitledSingleSelectEnumSchema { + #[serde(rename = "type")] + pub type_: StringTypeConst, #[serde(skip_serializing_if = "Option::is_none")] - pub enum_names: Option>, + pub title: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, + #[serde(rename = "oneOf")] + pub one_of: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, +} - /// Optional title for the schema +/// Combined single-select +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(untagged)] +pub enum SingleSelectEnumSchema { + Untitled(UntitledSingleSelectEnumSchema), + Titled(TitledSingleSelectEnumSchema), +} + +/// Items for untitled multi-select options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct UntitledItems { + #[serde(rename = "type")] + pub type_: StringTypeConst, + #[serde(rename = "enum")] + pub enum_: Vec, +} + +/// Items for titled multi-select options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct TitledItems { + // Note: Schemars produces "oneOf" here, but MCP spec uses "anyOf" + #[serde(rename = "anyOf", alias = "oneOf")] + pub any_of: Vec, +} + +/// Multi-select untitled options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct UntitledMultiSelectEnumSchema { + #[serde(rename = "type")] + pub type_: ArrayTypeConst, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_items: Option, + pub items: UntitledItems, + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option>, +} - /// Human-readable description +/// Multi-select titled options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase")] +pub struct TitledMultiSelectEnumSchema { + #[serde(rename = "type")] + pub type_: ArrayTypeConst, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_items: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_items: Option, + pub items: TitledItems, + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option>, } -impl EnumSchema { - /// Create a new enum schema with string values - pub fn new(values: Vec) -> Self { +/// Multi-select enum options +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(untagged)] +pub enum MultiSelectEnumSchema { + Untitled(UntitledMultiSelectEnumSchema), + Titled(TitledMultiSelectEnumSchema), +} + +/// Compliant with MCP 2025-11-25 specification for elicitation schemas. +/// Enums must have string type for values and can optionally include human-readable names. +/// +/// # Example +/// +/// ```rust +/// use rmcp::model::*; +/// +/// let enum_schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) +/// .multiselect() +/// .min_items(1u64).expect("Min items should be correct value") +/// .max_items(4u64).expect("Max items should be correct value") +/// .description("Country code") +/// .build(); +/// ``` +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(untagged)] +pub enum EnumSchema { + Single(SingleSelectEnumSchema), + Multi(MultiSelectEnumSchema), + Legacy(LegacyEnumSchema), +} + +/// Builder for EnumSchema +#[derive(Debug)] +pub struct EnumSchemaBuilder { + /// Enum values + enum_values: Vec, + /// If true generate SingleSelect EnumSchema, MultiSelect otherwise + single_select: bool, + /// If true generate Titled EnumSchema, UnTitled otherwise + titled: bool, + /// Title of EnumSchema + schema_title: Option>, + /// Description of EnumSchema + description: Option>, + /// Titles of given enum values + enum_titles: Vec, + /// Minimum number of items to choose for MultiSelect + min_items: Option, + /// Maximum number of items to choose for MultiSelect + max_items: Option, + /// Default values for enum + default: Vec, +} + +impl Default for EnumSchemaBuilder { + fn default() -> Self { Self { - type_: StringTypeConst, - enum_values: values, - enum_names: None, - title: None, + schema_title: None, description: None, + single_select: true, + titled: false, + enum_titles: Vec::new(), + enum_values: Vec::new(), + min_items: None, + max_items: None, + default: Vec::new(), } } +} - /// Set enum names (human-readable names for each enum value) - pub fn enum_names(mut self, names: Vec) -> Self { - self.enum_names = Some(names); +macro_rules! enum_schema_builder { + ($field:ident: $type:ty) => { + pub fn $field(mut self, value: $type) -> Self { + self.$field = Some(value.into()); + self + } + }; +} + +/// Enum selection builder +impl EnumSchemaBuilder { + pub fn new(values: Vec) -> EnumSchemaBuilder { + EnumSchemaBuilder { + enum_values: values, + single_select: true, + titled: false, + ..Default::default() + } + } + + /// Set titles to enum values. Also, implicitly set this enum schema as titled + pub fn enum_titles(mut self, titles: Vec) -> Result { + if titles.len() != self.enum_values.len() { + return Err(format!( + "Provided number of titles do not matched to number of values: expected {}, but got {}", + self.enum_values.len(), + titles.len() + )); + } + self.titled = true; + self.enum_titles = titles; + Ok(self) + } + + /// Set enum as single-select + /// If it was multi-select, clear default values + pub fn single_select(mut self) -> EnumSchemaBuilder { + if !self.single_select { + self.default = Vec::new(); + } + self.single_select = true; self } - /// Set title - pub fn title(mut self, title: impl Into>) -> Self { - self.title = Some(title.into()); + /// Set enum as multi-select + /// If it was single-select, clear default value + pub fn multiselect(mut self) -> EnumSchemaBuilder { + if self.single_select { + self.default = Vec::new(); + } + self.single_select = false; self } - /// Set description - pub fn description(mut self, description: impl Into>) -> Self { - self.description = Some(description.into()); + /// Set enum as untitled + pub fn untitled(mut self) -> EnumSchemaBuilder { + self.titled = false; self } + + /// Set default value for single-select enum + pub fn single_select_default( + mut self, + default_value: String, + ) -> Result { + if self.single_select { + return Err( + "Set single default value available only when the builder is set to single-select. \ + Use multi_select_default method for multi-select options", + ); + } + self.default = vec![default_value]; + Ok(self) + } + + /// Set default value for multi-select enum + pub fn multi_select_default( + mut self, + default_values: Vec, + ) -> Result { + if self.single_select { + return Err( + "Set multiple default values available only when the builder is set to multi-select. \ + Use single_select_default method for single-select options", + ); + } + self.default = default_values; + Ok(self) + } + + /// Set minimal number of items for multi-select enum options + pub fn min_items(mut self, value: u64) -> Result { + if let Some(max) = self.max_items + && value > max + { + return Err("Provided value is greater than max_items"); + } + self.min_items = Some(value); + Ok(self) + } + + /// Set maximal number of items for multi-select enum options + pub fn max_items(mut self, value: u64) -> Result { + if let Some(min) = self.min_items + && value < min + { + return Err("Provided value is less than min_items"); + } + self.max_items = Some(value); + Ok(self) + } + + enum_schema_builder!(schema_title: impl Into>); + enum_schema_builder!(description: impl Into>); + + /// Build enum schema + pub fn build(mut self) -> EnumSchema { + match (self.single_select, self.titled) { + (true, false) => EnumSchema::Single(SingleSelectEnumSchema::Untitled( + UntitledSingleSelectEnumSchema { + type_: StringTypeConst, + title: self.schema_title, + description: self.description, + enum_: self.enum_values, + default: self.default.pop(), + }, + )), + (true, true) => EnumSchema::Single(SingleSelectEnumSchema::Titled( + TitledSingleSelectEnumSchema { + type_: StringTypeConst, + title: self.schema_title, + description: self.description, + one_of: self + .enum_titles + .into_iter() + .zip(self.enum_values) + .map(|(title, const_)| ConstTitle { const_, title }) + .collect(), + default: self.default.pop(), + }, + )), + (false, false) => EnumSchema::Multi(MultiSelectEnumSchema::Untitled( + UntitledMultiSelectEnumSchema { + type_: ArrayTypeConst, + title: self.schema_title, + description: self.description, + min_items: self.min_items, + max_items: self.max_items, + items: UntitledItems { + type_: StringTypeConst, + enum_: self.enum_values, + }, + default: if self.default.is_empty() { + None + } else { + Some(self.default) + }, + }, + )), + (false, true) => { + EnumSchema::Multi(MultiSelectEnumSchema::Titled(TitledMultiSelectEnumSchema { + type_: ArrayTypeConst, + title: self.schema_title, + description: self.description, + min_items: self.min_items, + max_items: self.max_items, + items: TitledItems { + any_of: self + .enum_titles + .into_iter() + .zip(self.enum_values) + .map(|(title, const_)| ConstTitle { const_, title }) + .collect(), + }, + default: if self.default.is_empty() { + None + } else { + Some(self.default) + }, + })) + } + } + } +} + +impl EnumSchema { + /// Creates a new `EnumSchemaBuilder` with the given enum values. + /// + /// This convenience method allows you to construct an enum schema by specifying + /// the possible string values for the enum. Use the returned builder to further + /// configure the schema before building it. + /// + /// # Arguments + /// + /// * `values` - A vector of strings representing the allowed enum values. + /// + /// # Example + /// + /// ``` + /// use rmcp::model::*; + /// + /// let builder = EnumSchema::builder(vec!["A".to_string(), "B".to_string()]); + /// ``` + pub fn builder(values: Vec) -> EnumSchemaBuilder { + EnumSchemaBuilder::new(values) + } } // ============================================================================= @@ -599,6 +950,7 @@ impl ElicitationSchema { /// Returns a [`serde_json::Error`] if the JSON object cannot be deserialized /// into a valid ElicitationSchema. pub fn from_json_schema(schema: crate::model::JsonObject) -> Result { + println!("{}", serde_json::to_string_pretty(&schema)?); serde_json::from_value(serde_json::Value::Object(schema)) } @@ -972,13 +1324,13 @@ impl ElicitationSchemaBuilder { // Enum convenience methods /// Add a required enum property - pub fn required_enum(self, name: impl Into, values: Vec) -> Self { - self.required_property(name, PrimitiveSchema::Enum(EnumSchema::new(values))) + pub fn required_enum(self, name: impl Into, enum_schema: EnumSchema) -> Self { + self.required_property(name, PrimitiveSchema::Enum(enum_schema)) } /// Add an optional enum property - pub fn optional_enum(self, name: impl Into, values: Vec) -> Self { - self.property(name, PrimitiveSchema::Enum(EnumSchema::new(values))) + pub fn optional_enum(self, name: impl Into, enum_schema: EnumSchema) -> Self { + self.property(name, PrimitiveSchema::Enum(enum_schema)) } /// Mark an existing property as required @@ -1041,6 +1393,8 @@ impl ElicitationSchemaBuilder { #[cfg(test)] mod tests { + use anyhow::{Result, anyhow}; + use serde_json::json; use super::*; @@ -1087,22 +1441,90 @@ mod tests { } #[test] - fn test_enum_schema_serialization() { - let schema = EnumSchema::new(vec!["US".to_string(), "UK".to_string()]) - .enum_names(vec![ + fn test_enum_schema_untitled_single_select_serialization() { + let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) + .description("Country code") + .build(); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "string"); + assert_eq!(json["enum"], json!(["US", "UK"])); + assert_eq!(json["description"], "Country code"); + } + + #[test] + fn test_enum_schema_untitled_multi_select_serialization() -> Result<()> { + let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) + .multiselect() + .min_items(1u64) + .map_err(|e| anyhow!("{e}"))? + .max_items(4u64) + .map_err(|e| anyhow!("{e}"))? + .description("Country code") + .build(); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "array"); + assert_eq!(json["minItems"], 1u64); + assert_eq!(json["maxItems"], 4u64); + assert_eq!(json["items"], json!({"type":"string", "enum":["US", "UK"]})); + assert_eq!(json["description"], "Country code"); + Ok(()) + } + + #[test] + fn test_enum_schema_titled_single_select_serialization() -> Result<()> { + let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) + .enum_titles(vec![ "United States".to_string(), "United Kingdom".to_string(), ]) - .description("Country code"); + .map_err(|e| anyhow!("{e}"))? + .description("Country code") + .build(); let json = serde_json::to_value(&schema).unwrap(); assert_eq!(json["type"], "string"); - assert_eq!(json["enum"], json!(["US", "UK"])); assert_eq!( - json["enumNames"], - json!(["United States", "United Kingdom"]) + json["oneOf"], + json!([ + {"const": "US", "title":"United States"}, + {"const": "UK", "title":"United Kingdom"} + ]) + ); + assert_eq!(json["description"], "Country code"); + Ok(()) + } + + #[test] + fn test_enum_schema_titled_multi_select_serialization() -> Result<()> { + let schema = EnumSchema::builder(vec!["US".to_string(), "UK".to_string()]) + .enum_titles(vec![ + "United States".to_string(), + "United Kingdom".to_string(), + ]) + .map_err(|e| anyhow!("{e}"))? + .multiselect() + .min_items(1u64) + .map_err(|e| anyhow!("{e}"))? + .max_items(4u64) + .map_err(|e| anyhow!("{e}"))? + .description("Country code") + .build(); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "array"); + assert_eq!(json["minItems"], 1u64); + assert_eq!(json["maxItems"], 4u64); + assert_eq!( + json["items"], + json!({"anyOf":[ + {"const":"US","title":"United States"}, + {"const":"UK","title":"United Kingdom"} + ]}) ); assert_eq!(json["description"], "Country code"); + Ok(()) } #[test] @@ -1121,14 +1543,13 @@ mod tests { #[test] fn test_elicitation_schema_builder_complex() { + let enum_schema = + EnumSchema::builder(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]).build(); let schema = ElicitationSchema::builder() .required_string_with("name", |s| s.length(1, 100)) .required_integer("age", 0, 150) .optional_bool("newsletter", false) - .required_enum( - "country", - vec!["US".to_string(), "UK".to_string(), "CA".to_string()], - ) + .required_enum("country", enum_schema) .description("User registration") .build() .unwrap(); @@ -1177,4 +1598,129 @@ mod tests { assert!(result.is_err()); assert_eq!(result.unwrap_err(), "minimum must be <= maximum"); } + + #[cfg(feature = "schemars")] + mod schemars_tests { + use anyhow::Result; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use serde_json::json; + + use crate::model::ElicitationSchema; + + #[derive(Debug, Serialize, Deserialize, JsonSchema, Default)] + #[schemars(inline)] + #[schemars(extend("type" = "string"))] + enum TitledEnum { + #[schemars(title = "Title for the first value")] + #[default] + FirstValue, + #[schemars(title = "Title for the second value")] + SecondValue, + } + + #[derive(Debug, Serialize, Deserialize, JsonSchema)] + #[schemars(inline)] + enum UntitledEnum { + First, + Second, + Third, + } + + fn default_untitled_multi_select() -> Vec { + vec![UntitledEnum::Second, UntitledEnum::Third] + } + + #[derive(Debug, Serialize, Deserialize, JsonSchema)] + #[schemars(description = "User information")] + struct UserInfo { + #[schemars(description = "User's name")] + pub name: String, + pub single_select_untitled: UntitledEnum, + #[schemars( + title = "Single Select Titled", + description = "Description for single select enum", + default + )] + pub single_select_titled: TitledEnum, + #[serde(default = "default_untitled_multi_select")] + pub multi_select_untitled: Vec, + #[schemars( + title = "Multi Select Titled", + description = "Multi Select Description" + )] + pub multi_select_titled: Vec, + } + + #[test] + fn test_schema_inference_for_enum_fields() -> Result<()> { + let schema = ElicitationSchema::from_type::()?; + + let json = serde_json::to_value(&schema)?; + assert_eq!(json["type"], "object"); + assert_eq!(json["description"], "User information"); + assert_eq!( + json["required"], + json!(["name", "single_select_untitled", "multi_select_titled"]) + ); + let properties = match json.get("properties") { + Some(serde_json::Value::Object(map)) => map, + _ => panic!("Schema does not have 'properties' field or it is not object type"), + }; + + assert_eq!(properties.len(), 5); + assert_eq!( + properties["name"], + json!({"description":"User's name", "type":"string"}) + ); + + assert_eq!( + properties["single_select_untitled"], + serde_json::json!({ + "type":"string", + "enum":["First", "Second", "Third"] + }) + ); + + assert_eq!( + properties["single_select_titled"], + json!({ + "type":"string", + "title":"Single Select Titled", + "description":"Description for single select enum", + "oneOf":[ + {"const":"FirstValue", "title":"Title for the first value"}, + {"const":"SecondValue", "title":"Title for the second value"} + ], + "default":"FirstValue" + }) + ); + assert_eq!( + properties["multi_select_untitled"], + serde_json::json!({ + "type":"array", + "items" : { + "type":"string", + "enum":["First", "Second", "Third"] + }, + "default":["Second", "Third"] + }) + ); + assert_eq!( + properties["multi_select_titled"], + serde_json::json!({ + "type":"array", + "title":"Multi Select Titled", + "description":"Multi Select Description", + "items":{ + "anyOf":[ + {"const":"FirstValue", "title":"Title for the first value"}, + {"const":"SecondValue", "title":"Title for the second value"} + ] + } + }) + ); + Ok(()) + } + } } diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 8dc6f160..f03edcf2 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -406,7 +406,7 @@ async fn test_elicitation_structured_schemas() { .optional_bool("newsletter", false) .required_enum( "country", - vec!["US".to_string(), "UK".to_string(), "CA".to_string()], + EnumSchema::builder(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]).build(), ) .description("User registration information") .build() diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index 223989cb..8aa0cbed 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -307,6 +307,11 @@ } } }, + "ArrayTypeConst": { + "type": "string", + "format": "const", + "const": "array" + }, "BooleanSchema": { "description": "Schema definition for boolean properties.", "type": "object", @@ -445,6 +450,22 @@ "values" ] }, + "ConstTitle": { + "description": "Schema definition for enum properties.\n\nRepresent single entry for titled item", + "type": "object", + "properties": { + "const": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "const", + "title" + ] + }, "ContextInclusion": { "description": "Specifies how much context should be included in sampling requests.\n\nThis allows clients to control what additional context information\nshould be provided to the LLM when processing sampling requests.", "oneOf": [ @@ -664,52 +685,17 @@ "type": "object" }, "EnumSchema": { - "description": "Schema definition for enum properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nEnums must have string type and can optionally include human-readable names.", - "type": "object", - "properties": { - "description": { - "description": "Human-readable description", - "type": [ - "string", - "null" - ] - }, - "enum": { - "description": "Allowed enum values (string values only per MCP spec)", - "type": "array", - "items": { - "type": "string" - } - }, - "enumNames": { - "description": "Optional human-readable names for each enum value", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + "description": "Compliant with MCP 2025-11-25 specification for elicitation schemas.\nEnums must have string type for values and can optionally include human-readable names.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet enum_schema = EnumSchema::builder(vec![\"US\".to_string(), \"UK\".to_string()])\n .multiselect()\n .min_items(1u64).expect(\"Min items should be correct value\")\n .max_items(4u64).expect(\"Max items should be correct value\")\n .description(\"Country code\")\n .build();\n```", + "anyOf": [ + { + "$ref": "#/definitions/SingleSelectEnumSchema" }, - "title": { - "description": "Optional title for the schema", - "type": [ - "string", - "null" - ] + { + "$ref": "#/definitions/MultiSelectEnumSchema" }, - "type": { - "description": "Type discriminator (always \"string\" for enums)", - "allOf": [ - { - "$ref": "#/definitions/StringTypeConst" - } - ] + { + "$ref": "#/definitions/LegacyEnumSchema" } - }, - "required": [ - "type", - "enum" ] }, "ErrorCode": { @@ -1028,6 +1014,46 @@ "format": "const", "const": "2.0" }, + "LegacyEnumSchema": { + "description": "Legacy enum schema, keep for backward compatibility", + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "enumNames": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "enum" + ] + }, "ListPromptsResult": { "type": "object", "properties": { @@ -1213,6 +1239,17 @@ } } }, + "MultiSelectEnumSchema": { + "description": "Multi-select enum options", + "anyOf": [ + { + "$ref": "#/definitions/UntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/definitions/TitledMultiSelectEnumSchema" + } + ] + }, "Notification": { "type": "object", "properties": { @@ -1381,6 +1418,14 @@ "PrimitiveSchema": { "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).", "anyOf": [ + { + "description": "Enum property (explicit enum schema)", + "allOf": [ + { + "$ref": "#/definitions/EnumSchema" + } + ] + }, { "description": "String property (with optional enum constraint)", "allOf": [ @@ -1412,14 +1457,6 @@ "$ref": "#/definitions/BooleanSchema" } ] - }, - { - "description": "Enum property (explicit enum schema)", - "allOf": [ - { - "$ref": "#/definitions/EnumSchema" - } - ] } ] }, @@ -2198,6 +2235,17 @@ } ] }, + "SingleSelectEnumSchema": { + "description": "Combined single-select", + "anyOf": [ + { + "$ref": "#/definitions/UntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/definitions/TitledSingleSelectEnumSchema" + } + ] + }, "StringFormat": { "description": "String format types allowed by the MCP specification.", "oneOf": [ @@ -2288,6 +2336,111 @@ "format": "const", "const": "string" }, + "TitledItems": { + "description": "Items for titled multi-select options", + "type": "object", + "properties": { + "anyOf": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstTitle" + } + } + }, + "required": [ + "anyOf" + ] + }, + "TitledMultiSelectEnumSchema": { + "description": "Multi-select titled options", + "type": "object", + "properties": { + "default": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "items": { + "$ref": "#/definitions/TitledItems" + }, + "maxItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "minItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/ArrayTypeConst" + } + }, + "required": [ + "type", + "items" + ] + }, + "TitledSingleSelectEnumSchema": { + "description": "Titled single-select", + "type": "object", + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstTitle" + } + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "oneOf" + ] + }, "Tool": { "description": "A tool that can be used by a model.", "type": "object", @@ -2414,6 +2567,115 @@ ] } } + }, + "UntitledItems": { + "description": "Items for untitled multi-select options", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "enum" + ] + }, + "UntitledMultiSelectEnumSchema": { + "description": "Multi-select untitled options", + "type": "object", + "properties": { + "default": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "items": { + "$ref": "#/definitions/UntitledItems" + }, + "maxItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "minItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/ArrayTypeConst" + } + }, + "required": [ + "type", + "items" + ] + }, + "UntitledSingleSelectEnumSchema": { + "description": "Untitled single-select", + "type": "object", + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "enum" + ] } } } \ No newline at end of file diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index 223989cb..8aa0cbed 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -307,6 +307,11 @@ } } }, + "ArrayTypeConst": { + "type": "string", + "format": "const", + "const": "array" + }, "BooleanSchema": { "description": "Schema definition for boolean properties.", "type": "object", @@ -445,6 +450,22 @@ "values" ] }, + "ConstTitle": { + "description": "Schema definition for enum properties.\n\nRepresent single entry for titled item", + "type": "object", + "properties": { + "const": { + "type": "string" + }, + "title": { + "type": "string" + } + }, + "required": [ + "const", + "title" + ] + }, "ContextInclusion": { "description": "Specifies how much context should be included in sampling requests.\n\nThis allows clients to control what additional context information\nshould be provided to the LLM when processing sampling requests.", "oneOf": [ @@ -664,52 +685,17 @@ "type": "object" }, "EnumSchema": { - "description": "Schema definition for enum properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nEnums must have string type and can optionally include human-readable names.", - "type": "object", - "properties": { - "description": { - "description": "Human-readable description", - "type": [ - "string", - "null" - ] - }, - "enum": { - "description": "Allowed enum values (string values only per MCP spec)", - "type": "array", - "items": { - "type": "string" - } - }, - "enumNames": { - "description": "Optional human-readable names for each enum value", - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - } + "description": "Compliant with MCP 2025-11-25 specification for elicitation schemas.\nEnums must have string type for values and can optionally include human-readable names.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet enum_schema = EnumSchema::builder(vec![\"US\".to_string(), \"UK\".to_string()])\n .multiselect()\n .min_items(1u64).expect(\"Min items should be correct value\")\n .max_items(4u64).expect(\"Max items should be correct value\")\n .description(\"Country code\")\n .build();\n```", + "anyOf": [ + { + "$ref": "#/definitions/SingleSelectEnumSchema" }, - "title": { - "description": "Optional title for the schema", - "type": [ - "string", - "null" - ] + { + "$ref": "#/definitions/MultiSelectEnumSchema" }, - "type": { - "description": "Type discriminator (always \"string\" for enums)", - "allOf": [ - { - "$ref": "#/definitions/StringTypeConst" - } - ] + { + "$ref": "#/definitions/LegacyEnumSchema" } - }, - "required": [ - "type", - "enum" ] }, "ErrorCode": { @@ -1028,6 +1014,46 @@ "format": "const", "const": "2.0" }, + "LegacyEnumSchema": { + "description": "Legacy enum schema, keep for backward compatibility", + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "enumNames": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "enum" + ] + }, "ListPromptsResult": { "type": "object", "properties": { @@ -1213,6 +1239,17 @@ } } }, + "MultiSelectEnumSchema": { + "description": "Multi-select enum options", + "anyOf": [ + { + "$ref": "#/definitions/UntitledMultiSelectEnumSchema" + }, + { + "$ref": "#/definitions/TitledMultiSelectEnumSchema" + } + ] + }, "Notification": { "type": "object", "properties": { @@ -1381,6 +1418,14 @@ "PrimitiveSchema": { "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).", "anyOf": [ + { + "description": "Enum property (explicit enum schema)", + "allOf": [ + { + "$ref": "#/definitions/EnumSchema" + } + ] + }, { "description": "String property (with optional enum constraint)", "allOf": [ @@ -1412,14 +1457,6 @@ "$ref": "#/definitions/BooleanSchema" } ] - }, - { - "description": "Enum property (explicit enum schema)", - "allOf": [ - { - "$ref": "#/definitions/EnumSchema" - } - ] } ] }, @@ -2198,6 +2235,17 @@ } ] }, + "SingleSelectEnumSchema": { + "description": "Combined single-select", + "anyOf": [ + { + "$ref": "#/definitions/UntitledSingleSelectEnumSchema" + }, + { + "$ref": "#/definitions/TitledSingleSelectEnumSchema" + } + ] + }, "StringFormat": { "description": "String format types allowed by the MCP specification.", "oneOf": [ @@ -2288,6 +2336,111 @@ "format": "const", "const": "string" }, + "TitledItems": { + "description": "Items for titled multi-select options", + "type": "object", + "properties": { + "anyOf": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstTitle" + } + } + }, + "required": [ + "anyOf" + ] + }, + "TitledMultiSelectEnumSchema": { + "description": "Multi-select titled options", + "type": "object", + "properties": { + "default": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "items": { + "$ref": "#/definitions/TitledItems" + }, + "maxItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "minItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/ArrayTypeConst" + } + }, + "required": [ + "type", + "items" + ] + }, + "TitledSingleSelectEnumSchema": { + "description": "Titled single-select", + "type": "object", + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/ConstTitle" + } + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "oneOf" + ] + }, "Tool": { "description": "A tool that can be used by a model.", "type": "object", @@ -2414,6 +2567,115 @@ ] } } + }, + "UntitledItems": { + "description": "Items for untitled multi-select options", + "type": "object", + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "enum" + ] + }, + "UntitledMultiSelectEnumSchema": { + "description": "Multi-select untitled options", + "type": "object", + "properties": { + "default": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "items": { + "$ref": "#/definitions/UntitledItems" + }, + "maxItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "minItems": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0 + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/ArrayTypeConst" + } + }, + "required": [ + "type", + "items" + ] + }, + "UntitledSingleSelectEnumSchema": { + "description": "Untitled single-select", + "type": "object", + "properties": { + "default": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "enum": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/definitions/StringTypeConst" + } + }, + "required": [ + "type", + "enum" + ] } } } \ No newline at end of file diff --git a/examples/servers/Cargo.toml b/examples/servers/Cargo.toml index 8bf97f2a..57e2ccb4 100644 --- a/examples/servers/Cargo.toml +++ b/examples/servers/Cargo.toml @@ -105,3 +105,7 @@ path = "src/completion_stdio.rs" [[example]] name = "servers_progress_demo" path = "src/progress_demo.rs" + +[[example]] +name = "elicitation_enum_select" +path = "src/elicitation_enum_inference.rs" diff --git a/examples/servers/src/elicitation_enum_inference.rs b/examples/servers/src/elicitation_enum_inference.rs new file mode 100644 index 00000000..a1c24d0a --- /dev/null +++ b/examples/servers/src/elicitation_enum_inference.rs @@ -0,0 +1,184 @@ +//! Demonstration how to use enum selection in elicitation forms. +//! +//! This example server allows users to select enum values via elicitation forms. +//! To work with enum inference it is required to use `schemars` attributes and use some workarounds +use rmcp::transport::StreamableHttpService; +use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; + +use rmcp::{ + ErrorData as McpError, ServerHandler, elicit_safe, + handler::server::router::tool::ToolRouter, + model::*, + service::{RequestContext, RoleServer}, + tool, tool_handler, tool_router, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +const BIND_ADDRESS: &str = "127.0.0.1:8000"; + +#[derive(Debug, Serialize, Deserialize, JsonSchema, Default)] +// inline attribute required to work for schema inference in elicitation forms +#[schemars(inline)] +// schemars does not provide required type field for enums, so we extend it here +#[schemars(extend("type" = "string"))] +enum TitledEnum { + #[schemars(title = "Title for the first value")] + #[default] + FirstValue, + #[schemars(title = "Title for the second value")] + SecondValue, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +// inline attribute required to work for schema inference in elicitation forms +#[schemars(inline)] +enum UntitledEnum { + First, + Second, + Third, +} + +fn default_untitled_multi_select() -> Vec { + vec![UntitledEnum::Second, UntitledEnum::Third] +} +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[schemars(description = "User information")] +struct SelectEnumForm { + pub single_select_untitled: UntitledEnum, + #[schemars( + title = "Single Select Titled", + description = "Description for single select enum", + default + )] + pub single_select_titled: TitledEnum, + #[serde(default = "default_untitled_multi_select")] + pub multi_select_untitled: Vec, + #[schemars( + title = "Multi Select Titled", + description = "Multi Select Description" + )] + pub multi_select_titled: Vec, +} + +impl Display for SelectEnumForm { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = format!( + "Current Selections:\n\ + Single Select Untitled: {:?}\n\ + Single Select Titled: {:?}\ + Multi Select Untitled: {:?}\n\ + Multi Select Titled: {:?}\n", + self.single_select_untitled, + self.single_select_titled, + self.multi_select_untitled, + self.multi_select_titled, + ); + write!(f, "{s}") + } +} + +elicit_safe!(SelectEnumForm); + +#[derive(Clone)] +struct ElicitationEnumFormServer { + selection: Arc>, + tool_router: ToolRouter, +} + +#[tool_router] +impl ElicitationEnumFormServer { + pub fn new() -> Self { + Self { + selection: Arc::new(Mutex::new(SelectEnumForm { + single_select_untitled: UntitledEnum::First, + single_select_titled: TitledEnum::FirstValue, + multi_select_untitled: vec![UntitledEnum::Second], + multi_select_titled: vec![TitledEnum::SecondValue], + })), + tool_router: Self::tool_router(), + } + } + + #[tool(description = "Get current enum selection form")] + async fn get_enum_form(&self) -> Result { + let guard = self.selection.lock().await; + Ok(CallToolResult::success(vec![Content::text(format!( + "{}", + *guard + ))])) + } + + #[tool(description = "Set enum selection via elicitation form")] + async fn set_enum_form( + &self, + context: RequestContext, + ) -> Result { + match context + .peer + .elicit::("Please provide your selection".to_string()) + .await + { + Ok(Some(form)) => { + let mut guard = self.selection.lock().await; + *guard = form; + Ok(CallToolResult::success(vec![Content::text(format!( + "Updated Selection:\n{}", + *guard + ))])) + } + Ok(None) => { + return Ok(CallToolResult::success(vec![Content::text( + "Elicitation cancelled by user.", + )])); + } + Err(err) => { + return Err(McpError::internal_error( + format!("Elicitation failed: {err}"), + None, + )); + } + } + } +} + +#[tool_handler] +impl ServerHandler for ElicitationEnumFormServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation::from_build_env(), + instructions: Some( + "Simple server demonstrating elicitation for enum selection".to_string(), + ), + ..Default::default() + } + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::registry() + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "debug".to_string().into())) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let service = StreamableHttpService::new( + || Ok(ElicitationEnumFormServer::new()), + LocalSessionManager::default().into(), + Default::default(), + ); + + let router = axum::Router::new().nest_service("/mcp", service); + let tcp_listener = tokio::net::TcpListener::bind(BIND_ADDRESS).await?; + let _ = axum::serve(tcp_listener, router) + .with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() }) + .await; + Ok(()) +} From 8951dfe736ebcc01dcaa13af956e1af6b61d05da Mon Sep 17 00:00:00 2001 From: Pavel Bezglasnyy Date: Fri, 14 Nov 2025 01:31:52 +0100 Subject: [PATCH 2/7] fix remove debug print statement --- crates/rmcp/src/model/elicitation_schema.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 953b5a77..357cd3d9 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -950,7 +950,6 @@ impl ElicitationSchema { /// Returns a [`serde_json::Error`] if the JSON object cannot be deserialized /// into a valid ElicitationSchema. pub fn from_json_schema(schema: crate::model::JsonObject) -> Result { - println!("{}", serde_json::to_string_pretty(&schema)?); serde_json::from_value(serde_json::Value::Object(schema)) } From bce5ea0d1420e2d980e0587e07c3b10879e206a7 Mon Sep 17 00:00:00 2001 From: Pavel Bezglasny Date: Fri, 14 Nov 2025 01:40:46 +0100 Subject: [PATCH 3/7] Update crates/rmcp/src/model/elicitation_schema.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/rmcp/src/model/elicitation_schema.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 357cd3d9..75bee58e 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -691,7 +691,7 @@ impl EnumSchemaBuilder { pub fn enum_titles(mut self, titles: Vec) -> Result { if titles.len() != self.enum_values.len() { return Err(format!( - "Provided number of titles do not matched to number of values: expected {}, but got {}", + "Provided number of titles do not match number of values: expected {}, but got {}", self.enum_values.len(), titles.len() )); From 22df2de3a4f8b3ba654ce9ac7849a6c4ff9c1917 Mon Sep 17 00:00:00 2001 From: Pavel Bezglasny Date: Fri, 14 Nov 2025 01:40:54 +0100 Subject: [PATCH 4/7] Update examples/servers/src/elicitation_enum_inference.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/servers/src/elicitation_enum_inference.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/servers/src/elicitation_enum_inference.rs b/examples/servers/src/elicitation_enum_inference.rs index a1c24d0a..252e4004 100644 --- a/examples/servers/src/elicitation_enum_inference.rs +++ b/examples/servers/src/elicitation_enum_inference.rs @@ -72,7 +72,7 @@ impl Display for SelectEnumForm { let s = format!( "Current Selections:\n\ Single Select Untitled: {:?}\n\ - Single Select Titled: {:?}\ + Single Select Titled: {:?}\n\ Multi Select Untitled: {:?}\n\ Multi Select Titled: {:?}\n", self.single_select_untitled, From 6d11e7291630373ea3661750a0a6ab06a3aa5ad1 Mon Sep 17 00:00:00 2001 From: Pavel Bezglasny Date: Fri, 14 Nov 2025 01:42:11 +0100 Subject: [PATCH 5/7] Update examples/servers/src/elicitation_enum_inference.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/servers/src/elicitation_enum_inference.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/servers/src/elicitation_enum_inference.rs b/examples/servers/src/elicitation_enum_inference.rs index 252e4004..6449bfe6 100644 --- a/examples/servers/src/elicitation_enum_inference.rs +++ b/examples/servers/src/elicitation_enum_inference.rs @@ -1,7 +1,11 @@ //! Demonstration how to use enum selection in elicitation forms. //! //! This example server allows users to select enum values via elicitation forms. -//! To work with enum inference it is required to use `schemars` attributes and use some workarounds +//! To work with enum inference, it is required to use specific `schemars` attributes and apply some workarounds: +//! - Use `#[schemars(inline)]` to ensure the enum is inlined in the schema. +//! - Use `#[schemars(extend("type" = "string"))]` to manually add the required type field, since `schemars` does not provide it for enums. +//! - Optionally, use `#[schemars(title = "...")]` to provide titles for enum variants. +//! For more details, see: https://docs.rs/schemars/latest/schemars/ use rmcp::transport::StreamableHttpService; use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; From a27acee733cadb8cef7b7336f0acf329f4d43d1e Mon Sep 17 00:00:00 2001 From: Pavel Bezglasnyy Date: Fri, 14 Nov 2025 01:49:48 +0100 Subject: [PATCH 6/7] fix PR review issues --- crates/rmcp/src/model/elicitation_schema.rs | 14 +++++++++----- .../servers/src/elicitation_enum_inference.rs | 15 ++++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 75bee58e..c9b5f4cd 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -42,11 +42,14 @@ const_string!(ArrayTypeConst = "array"); /// /// According to MCP 2025-06-18 specification, elicitation schemas must have /// properties of primitive types only (string, number, integer, boolean, enum). +/// +/// Note: Put Enum as the first variant to avoid ambiguity during deserialization. +/// This is due to the fact that EnumSchema can contain StringSchema internally and serde +/// uses first match wins strategy when deserializing untagged enums. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum PrimitiveSchema { - // Note: Enum must be first to avoid ambiguity during deserialization /// Enum property (explicit enum schema) Enum(EnumSchema), /// String property (with optional enum constraint) @@ -468,9 +471,9 @@ impl BooleanSchema { /// Schema definition for enum properties. /// -/// Represent single entry for titled item #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +/// Represent single entry for titled item pub struct ConstTitle { #[serde(rename = "const")] pub const_: String, @@ -604,7 +607,7 @@ pub enum MultiSelectEnumSchema { Titled(TitledMultiSelectEnumSchema), } -/// Compliant with MCP 2025-11-25 specification for elicitation schemas. +/// Compliant with MCP 2025-06-18 specification for elicitation schemas. /// Enums must have string type for values and can optionally include human-readable names. /// /// # Example @@ -722,7 +725,9 @@ impl EnumSchemaBuilder { } /// Set enum as untitled + /// Clears any previously set titles pub fn untitled(mut self) -> EnumSchemaBuilder { + self.enum_titles = Vec::new(); self.titled = false; self } @@ -732,7 +737,7 @@ impl EnumSchemaBuilder { mut self, default_value: String, ) -> Result { - if self.single_select { + if !self.single_select { return Err( "Set single default value available only when the builder is set to single-select. \ Use multi_select_default method for multi-select options", @@ -1393,7 +1398,6 @@ impl ElicitationSchemaBuilder { #[cfg(test)] mod tests { use anyhow::{Result, anyhow}; - use serde_json::json; use super::*; diff --git a/examples/servers/src/elicitation_enum_inference.rs b/examples/servers/src/elicitation_enum_inference.rs index 6449bfe6..2ecec311 100644 --- a/examples/servers/src/elicitation_enum_inference.rs +++ b/examples/servers/src/elicitation_enum_inference.rs @@ -6,8 +6,10 @@ //! - Use `#[schemars(extend("type" = "string"))]` to manually add the required type field, since `schemars` does not provide it for enums. //! - Optionally, use `#[schemars(title = "...")]` to provide titles for enum variants. //! For more details, see: https://docs.rs/schemars/latest/schemars/ -use rmcp::transport::StreamableHttpService; -use rmcp::transport::streamable_http_server::session::local::LocalSessionManager; +use std::{ + fmt::{Display, Formatter}, + sync::Arc, +}; use rmcp::{ ErrorData as McpError, ServerHandler, elicit_safe, @@ -15,15 +17,14 @@ use rmcp::{ model::*, service::{RequestContext, RoleServer}, tool, tool_handler, tool_router, + transport::{ + StreamableHttpService, streamable_http_server::session::local::LocalSessionManager, + }, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; -use std::sync::Arc; use tokio::sync::Mutex; -use tracing_subscriber::EnvFilter; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; const BIND_ADDRESS: &str = "127.0.0.1:8000"; From 52e38891cfbc57ea95b52e43c0863a8442968625 Mon Sep 17 00:00:00 2001 From: Pavel Bezglasnyy Date: Fri, 14 Nov 2025 01:51:41 +0100 Subject: [PATCH 7/7] fix unit test --- .../test_message_schema/server_json_rpc_message_schema.json | 4 ++-- .../server_json_rpc_message_schema_current.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index 8aa0cbed..2929d92c 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -685,7 +685,7 @@ "type": "object" }, "EnumSchema": { - "description": "Compliant with MCP 2025-11-25 specification for elicitation schemas.\nEnums must have string type for values and can optionally include human-readable names.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet enum_schema = EnumSchema::builder(vec![\"US\".to_string(), \"UK\".to_string()])\n .multiselect()\n .min_items(1u64).expect(\"Min items should be correct value\")\n .max_items(4u64).expect(\"Max items should be correct value\")\n .description(\"Country code\")\n .build();\n```", + "description": "Compliant with MCP 2025-06-18 specification for elicitation schemas.\nEnums must have string type for values and can optionally include human-readable names.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet enum_schema = EnumSchema::builder(vec![\"US\".to_string(), \"UK\".to_string()])\n .multiselect()\n .min_items(1u64).expect(\"Min items should be correct value\")\n .max_items(4u64).expect(\"Max items should be correct value\")\n .description(\"Country code\")\n .build();\n```", "anyOf": [ { "$ref": "#/definitions/SingleSelectEnumSchema" @@ -1416,7 +1416,7 @@ "const": "ping" }, "PrimitiveSchema": { - "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).", + "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).\n\nNote: Put Enum as the first variant to avoid ambiguity during deserialization.\nThis is due to the fact that EnumSchema can contain StringSchema internally and serde\nuses first match wins strategy when deserializing untagged enums.", "anyOf": [ { "description": "Enum property (explicit enum schema)", diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index 8aa0cbed..2929d92c 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -685,7 +685,7 @@ "type": "object" }, "EnumSchema": { - "description": "Compliant with MCP 2025-11-25 specification for elicitation schemas.\nEnums must have string type for values and can optionally include human-readable names.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet enum_schema = EnumSchema::builder(vec![\"US\".to_string(), \"UK\".to_string()])\n .multiselect()\n .min_items(1u64).expect(\"Min items should be correct value\")\n .max_items(4u64).expect(\"Max items should be correct value\")\n .description(\"Country code\")\n .build();\n```", + "description": "Compliant with MCP 2025-06-18 specification for elicitation schemas.\nEnums must have string type for values and can optionally include human-readable names.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet enum_schema = EnumSchema::builder(vec![\"US\".to_string(), \"UK\".to_string()])\n .multiselect()\n .min_items(1u64).expect(\"Min items should be correct value\")\n .max_items(4u64).expect(\"Max items should be correct value\")\n .description(\"Country code\")\n .build();\n```", "anyOf": [ { "$ref": "#/definitions/SingleSelectEnumSchema" @@ -1416,7 +1416,7 @@ "const": "ping" }, "PrimitiveSchema": { - "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).", + "description": "Primitive schema definition for elicitation properties.\n\nAccording to MCP 2025-06-18 specification, elicitation schemas must have\nproperties of primitive types only (string, number, integer, boolean, enum).\n\nNote: Put Enum as the first variant to avoid ambiguity during deserialization.\nThis is due to the fact that EnumSchema can contain StringSchema internally and serde\nuses first match wins strategy when deserializing untagged enums.", "anyOf": [ { "description": "Enum property (explicit enum schema)",