From e151793e05065a490f0bee1bf3336737e653ad6f Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Sun, 9 Nov 2025 17:02:22 +0100 Subject: [PATCH 1/4] feat: add default value support to elicitation schemas (SEP-1034) Implement optional default values for StringSchema, NumberSchema, and EnumSchema following the BooleanSchema pattern. This allows forms to be pre-populated with sensible defaults, improving user experience in elicitation workflows. Changes: - Add optional `default` field to StringSchema (Cow<'static, str>) - Add optional `default` field to NumberSchema (f64) - Add optional `default` field to EnumSchema (String) - Add `with_default()` builder methods to all three schemas - Add comprehensive tests for default value serialization and deserialization - Update elicitation example to demonstrate default values usage All defaults are optional (Option) for backward compatibility and use skip_serializing_if to ensure old clients ignore the new field. --- crates/rmcp/src/model/elicitation_schema.rs | 143 ++++++++++++++++++++ examples/servers/src/elicitation_stdio.rs | 86 +++++++++++- 2 files changed, 228 insertions(+), 1 deletion(-) diff --git a/crates/rmcp/src/model/elicitation_schema.rs b/crates/rmcp/src/model/elicitation_schema.rs index 095e28e9..dcdeb6ec 100644 --- a/crates/rmcp/src/model/elicitation_schema.rs +++ b/crates/rmcp/src/model/elicitation_schema.rs @@ -108,6 +108,10 @@ pub struct StringSchema { /// String format - limited to: "email", "uri", "date", "date-time" #[serde(skip_serializing_if = "Option::is_none")] pub format: Option, + + /// Default value + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option>, } impl Default for StringSchema { @@ -119,6 +123,7 @@ impl Default for StringSchema { min_length: None, max_length: None, format: None, + default: None, } } } @@ -208,6 +213,12 @@ impl StringSchema { self.format = Some(format); self } + + /// Set default value + pub fn with_default(mut self, default: impl Into>) -> Self { + self.default = Some(default.into()); + self + } } // ============================================================================= @@ -241,6 +252,10 @@ pub struct NumberSchema { /// Maximum value (inclusive) #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, + + /// Default value + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, } impl Default for NumberSchema { @@ -251,6 +266,7 @@ impl Default for NumberSchema { description: None, minimum: None, maximum: None, + default: None, } } } @@ -302,6 +318,12 @@ impl NumberSchema { self.description = Some(description.into()); self } + + /// Set default value + pub fn with_default(mut self, default: f64) -> Self { + self.default = Some(default); + self + } } // ============================================================================= @@ -491,6 +513,10 @@ pub struct EnumSchema { /// Human-readable description #[serde(skip_serializing_if = "Option::is_none")] pub description: Option>, + + /// Default value + #[serde(skip_serializing_if = "Option::is_none")] + pub default: Option, } impl EnumSchema { @@ -502,6 +528,7 @@ impl EnumSchema { enum_names: None, title: None, description: None, + default: None, } } @@ -522,6 +549,12 @@ impl EnumSchema { self.description = Some(description.into()); self } + + /// Set default value + pub fn with_default(mut self, default: String) -> Self { + self.default = Some(default); + self + } } // ============================================================================= @@ -1177,4 +1210,114 @@ mod tests { assert!(result.is_err()); assert_eq!(result.unwrap_err(), "minimum must be <= maximum"); } + + #[test] + fn test_string_schema_with_default() { + let schema = StringSchema::new() + .with_default("example@test.com") + .description("Email with default"); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "string"); + assert_eq!(json["default"], "example@test.com"); + assert_eq!(json["description"], "Email with default"); + } + + #[test] + fn test_string_schema_without_default() { + let schema = StringSchema::new().description("Email without default"); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "string"); + assert!(json.get("default").is_none()); + } + + #[test] + fn test_number_schema_with_default() { + let schema = NumberSchema::new() + .range(0.0, 100.0) + .with_default(50.0) + .description("Percentage with default"); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "number"); + assert_eq!(json["default"], 50.0); + assert_eq!(json["minimum"], 0.0); + assert_eq!(json["maximum"], 100.0); + } + + #[test] + fn test_number_schema_without_default() { + let schema = NumberSchema::new().range(0.0, 100.0); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "number"); + assert!(json.get("default").is_none()); + } + + #[test] + fn test_enum_schema_with_default() { + let schema = EnumSchema::new(vec!["US".to_string(), "UK".to_string(), "CA".to_string()]) + .with_default("US".to_string()) + .description("Country with default"); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "string"); + assert_eq!(json["enum"], json!(["US", "UK", "CA"])); + assert_eq!(json["default"], "US"); + } + + #[test] + fn test_enum_schema_without_default() { + let schema = EnumSchema::new(vec!["US".to_string(), "UK".to_string()]); + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "string"); + assert!(json.get("default").is_none()); + } + + #[test] + fn test_elicitation_schema_with_defaults() { + let schema = ElicitationSchema::builder() + .required_string_with("name", |s| s.length(1, 100)) + .property( + "email", + PrimitiveSchema::String(StringSchema::email().with_default("user@example.com")), + ) + .property( + "age", + PrimitiveSchema::Number(NumberSchema::new().range(0.0, 150.0).with_default(25.0)), + ) + .property( + "country", + PrimitiveSchema::Enum( + EnumSchema::new(vec!["US".to_string(), "UK".to_string()]) + .with_default("US".to_string()), + ), + ) + .description("User registration with defaults") + .build() + .unwrap(); + + let json = serde_json::to_value(&schema).unwrap(); + + assert_eq!(json["type"], "object"); + assert_eq!(json["properties"]["email"]["default"], "user@example.com"); + assert_eq!(json["properties"]["age"]["default"], 25.0); + assert_eq!(json["properties"]["country"]["default"], "US"); + assert!(json["properties"]["name"].get("default").is_none()); + } + + #[test] + fn test_default_serialization_roundtrip() { + let original = StringSchema::new() + .with_default("test") + .description("Test schema"); + + let json = serde_json::to_value(&original).unwrap(); + let deserialized: StringSchema = serde_json::from_value(json).unwrap(); + + assert_eq!(original, deserialized); + assert_eq!(deserialized.default, Some(Cow::Borrowed("test"))); + } } diff --git a/examples/servers/src/elicitation_stdio.rs b/examples/servers/src/elicitation_stdio.rs index 10ee6611..16f09f61 100644 --- a/examples/servers/src/elicitation_stdio.rs +++ b/examples/servers/src/elicitation_stdio.rs @@ -1,6 +1,6 @@ //! Simple MCP Server with Elicitation //! -//! Demonstrates user name collection via elicitation +//! Demonstrates user name collection via elicitation and default values use std::sync::Arc; @@ -106,6 +106,90 @@ impl ElicitationServer { "User name reset. Next greeting will ask for name again.".to_string(), )])) } + + #[tool(description = "Reply to email with default values demonstration")] + async fn reply_email( + &self, + context: RequestContext, + ) -> Result { + // Build schema with default values for email reply + let schema = ElicitationSchema::builder() + .string_property("recipient", |s| { + s.format(StringFormat::Email) + .with_default("sender@example.com") + .description("Email recipient") + }) + .string_property("cc", |s| { + s.format(StringFormat::Email) + .with_default("team@example.com") + .description("CC recipients") + }) + .property( + "priority", + PrimitiveSchema::Enum( + EnumSchema::new(vec![ + "low".to_string(), + "normal".to_string(), + "high".to_string(), + ]) + .with_default("normal".to_string()) + .description("Email priority"), + ), + ) + .number_property("confidence", |n| { + n.range(0.0, 1.0) + .with_default(0.8) + .description("Reply confidence score") + }) + .required_string("subject") + .required_string("body") + .description("Email reply configuration with defaults") + .build() + .map_err(|e| McpError::internal_error(format!("Schema build error: {}", e), None))?; + + // Request email details with pre-filled defaults + let response = context + .peer + .create_elicitation(CreateElicitationRequestParam { + message: "Configure email reply".to_string(), + requested_schema: schema, + }) + .await + .map_err(|e| McpError::internal_error(format!("Elicitation error: {}", e), None))?; + + match response.action { + ElicitationAction::Accept => { + if let Some(reply_data) = response.content { + let recipient = reply_data + .get("recipient") + .and_then(|v| v.as_str()) + .unwrap_or("unknown"); + let subject = reply_data + .get("subject") + .and_then(|v| v.as_str()) + .unwrap_or("No subject"); + let priority = reply_data + .get("priority") + .and_then(|v| v.as_str()) + .unwrap_or("normal"); + + Ok(CallToolResult::success(vec![Content::text(format!( + "Email reply configured:\nTo: {}\nSubject: {}\nPriority: {}\n\nDefaults were used for pre-filling the form!", + recipient, subject, priority + ))])) + } else { + Ok(CallToolResult::success(vec![Content::text( + "Email accepted but no content provided".to_string(), + )])) + } + } + ElicitationAction::Decline | ElicitationAction::Cancel => { + Ok(CallToolResult::success(vec![Content::text( + "Email reply cancelled".to_string(), + )])) + } + } + } } #[tool_handler] From f9d903923f1a7f9c380fd25bd443d97ad8aaa04b Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Sun, 9 Nov 2025 18:04:56 +0100 Subject: [PATCH 2/4] chore: update JSON schema and format code - Update server JSON RPC message schema with new default fields - Apply code formatting to elicitation example --- .../server_json_rpc_message_schema.json | 22 +++++++++++++++++++ ...erver_json_rpc_message_schema_current.json | 22 +++++++++++++++++++ examples/servers/src/elicitation_stdio.rs | 8 +++---- 3 files changed, 47 insertions(+), 5 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 663a6894..eef1b88f 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 @@ -667,6 +667,13 @@ "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": { + "default": { + "description": "Default value", + "type": [ + "string", + "null" + ] + }, "description": { "description": "Human-readable description", "type": [ @@ -1320,6 +1327,14 @@ "description": "Schema definition for number properties (floating-point).\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nSupports only the fields allowed by the MCP spec.", "type": "object", "properties": { + "default": { + "description": "Default value", + "type": [ + "number", + "null" + ], + "format": "double" + }, "description": { "description": "Human-readable description", "type": [ @@ -2227,6 +2242,13 @@ "description": "Schema definition for string properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nSupports only the fields allowed by the MCP spec:\n- format limited to: \"email\", \"uri\", \"date\", \"date-time\"", "type": "object", "properties": { + "default": { + "description": "Default value", + "type": [ + "string", + "null" + ] + }, "description": { "description": "Human-readable description", "type": [ 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 663a6894..eef1b88f 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 @@ -667,6 +667,13 @@ "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": { + "default": { + "description": "Default value", + "type": [ + "string", + "null" + ] + }, "description": { "description": "Human-readable description", "type": [ @@ -1320,6 +1327,14 @@ "description": "Schema definition for number properties (floating-point).\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nSupports only the fields allowed by the MCP spec.", "type": "object", "properties": { + "default": { + "description": "Default value", + "type": [ + "number", + "null" + ], + "format": "double" + }, "description": { "description": "Human-readable description", "type": [ @@ -2227,6 +2242,13 @@ "description": "Schema definition for string properties.\n\nCompliant with MCP 2025-06-18 specification for elicitation schemas.\nSupports only the fields allowed by the MCP spec:\n- format limited to: \"email\", \"uri\", \"date\", \"date-time\"", "type": "object", "properties": { + "default": { + "description": "Default value", + "type": [ + "string", + "null" + ] + }, "description": { "description": "Human-readable description", "type": [ diff --git a/examples/servers/src/elicitation_stdio.rs b/examples/servers/src/elicitation_stdio.rs index 16f09f61..d834f718 100644 --- a/examples/servers/src/elicitation_stdio.rs +++ b/examples/servers/src/elicitation_stdio.rs @@ -183,11 +183,9 @@ impl ElicitationServer { )])) } } - ElicitationAction::Decline | ElicitationAction::Cancel => { - Ok(CallToolResult::success(vec![Content::text( - "Email reply cancelled".to_string(), - )])) - } + ElicitationAction::Decline | ElicitationAction::Cancel => Ok(CallToolResult::success( + vec![Content::text("Email reply cancelled".to_string())], + )), } } } From eff2d451a7fccd4f87c7df656c150b15115f8ff6 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Fri, 14 Nov 2025 13:57:31 +0100 Subject: [PATCH 3/4] feat(examples): display all elicitation form fields in email reply tool Extract and display all fields from elicitation response including cc, body, and confidence in addition to previously shown recipient, subject, and priority fields. --- examples/servers/src/elicitation_stdio.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/servers/src/elicitation_stdio.rs b/examples/servers/src/elicitation_stdio.rs index d834f718..803ed49b 100644 --- a/examples/servers/src/elicitation_stdio.rs +++ b/examples/servers/src/elicitation_stdio.rs @@ -164,18 +164,30 @@ impl ElicitationServer { .get("recipient") .and_then(|v| v.as_str()) .unwrap_or("unknown"); + let cc = reply_data + .get("cc") + .and_then(|v| v.as_str()) + .unwrap_or("none"); let subject = reply_data .get("subject") .and_then(|v| v.as_str()) .unwrap_or("No subject"); + let body = reply_data + .get("body") + .and_then(|v| v.as_str()) + .unwrap_or("No body"); let priority = reply_data .get("priority") .and_then(|v| v.as_str()) .unwrap_or("normal"); + let confidence = reply_data + .get("confidence") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); Ok(CallToolResult::success(vec![Content::text(format!( - "Email reply configured:\nTo: {}\nSubject: {}\nPriority: {}\n\nDefaults were used for pre-filling the form!", - recipient, subject, priority + "Email reply configured:\nTo: {}\nCC: {}\nSubject: {}\nBody: {}\nPriority: {}\nConfidence: {:.2}\n\nDefaults were used for pre-filling the form!", + recipient, cc, subject, body, priority, confidence ))])) } else { Ok(CallToolResult::success(vec![Content::text( From 22da84c90fd306768680f9cf5f2e5a76127ce182 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Fri, 14 Nov 2025 14:21:00 +0100 Subject: [PATCH 4/4] fix(examples): use schema default values as fallback in elicitation Use default values from schema (sender@example.com, team@example.com, 0.8) instead of hardcoded fallback values when user doesn't provide input. --- examples/servers/src/elicitation_stdio.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/servers/src/elicitation_stdio.rs b/examples/servers/src/elicitation_stdio.rs index 803ed49b..cb9923fe 100644 --- a/examples/servers/src/elicitation_stdio.rs +++ b/examples/servers/src/elicitation_stdio.rs @@ -163,11 +163,11 @@ impl ElicitationServer { let recipient = reply_data .get("recipient") .and_then(|v| v.as_str()) - .unwrap_or("unknown"); + .unwrap_or("sender@example.com"); let cc = reply_data .get("cc") .and_then(|v| v.as_str()) - .unwrap_or("none"); + .unwrap_or("team@example.com"); let subject = reply_data .get("subject") .and_then(|v| v.as_str()) @@ -183,7 +183,7 @@ impl ElicitationServer { let confidence = reply_data .get("confidence") .and_then(|v| v.as_f64()) - .unwrap_or(0.0); + .unwrap_or(0.8); Ok(CallToolResult::success(vec![Content::text(format!( "Email reply configured:\nTo: {}\nCC: {}\nSubject: {}\nBody: {}\nPriority: {}\nConfidence: {:.2}\n\nDefaults were used for pre-filling the form!",