Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions crates/rmcp/src/model/elicitation_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<StringFormat>,

/// Default value
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Cow<'static, str>>,
}

impl Default for StringSchema {
Expand All @@ -119,6 +123,7 @@ impl Default for StringSchema {
min_length: None,
max_length: None,
format: None,
default: None,
}
}
}
Expand Down Expand Up @@ -208,6 +213,12 @@ impl StringSchema {
self.format = Some(format);
self
}

/// Set default value
pub fn with_default(mut self, default: impl Into<Cow<'static, str>>) -> Self {
self.default = Some(default.into());
self
}
}

// =============================================================================
Expand Down Expand Up @@ -241,6 +252,10 @@ pub struct NumberSchema {
/// Maximum value (inclusive)
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,

/// Default value
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
}

impl Default for NumberSchema {
Expand All @@ -251,6 +266,7 @@ impl Default for NumberSchema {
description: None,
minimum: None,
maximum: None,
default: None,
}
}
}
Expand Down Expand Up @@ -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
}
}

// =============================================================================
Expand Down Expand Up @@ -491,6 +513,10 @@ pub struct EnumSchema {
/// Human-readable description
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<Cow<'static, str>>,

/// Default value
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}

impl EnumSchema {
Expand All @@ -502,6 +528,7 @@ impl EnumSchema {
enum_names: None,
title: None,
description: None,
default: None,
}
}

Expand All @@ -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
}
}

// =============================================================================
Expand Down Expand Up @@ -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")));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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": [
Expand Down
84 changes: 83 additions & 1 deletion examples/servers/src/elicitation_stdio.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -106,6 +106,88 @@ 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<RoleServer>,
) -> Result<CallToolResult, McpError> {
// 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]
Expand Down