Skip to content

Commit 8fa6ad7

Browse files
committed
feat: add conversion methods to ElicitationSchema
Add from_json_schema() and from_type() methods to ElicitationSchema for easier type-to-schema conversion. This addresses feedback about improving ergonomics when working with generated schemas. Also make all struct fields public for better flexibility.
1 parent 4424191 commit 8fa6ad7

File tree

2 files changed

+97
-33
lines changed

2 files changed

+97
-33
lines changed

crates/rmcp/src/model/elicitation_schema.rs

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -72,27 +72,27 @@ pub enum PrimitiveSchema {
7272
pub struct StringSchema {
7373
/// Type discriminator
7474
#[serde(rename = "type")]
75-
type_: StringTypeConst,
75+
pub type_: StringTypeConst,
7676

7777
/// Optional title for the schema
7878
#[serde(skip_serializing_if = "Option::is_none")]
79-
title: Option<Cow<'static, str>>,
79+
pub title: Option<Cow<'static, str>>,
8080

8181
/// Human-readable description
8282
#[serde(skip_serializing_if = "Option::is_none")]
83-
description: Option<Cow<'static, str>>,
83+
pub description: Option<Cow<'static, str>>,
8484

8585
/// Minimum string length
8686
#[serde(skip_serializing_if = "Option::is_none")]
87-
min_length: Option<u32>,
87+
pub min_length: Option<u32>,
8888

8989
/// Maximum string length
9090
#[serde(skip_serializing_if = "Option::is_none")]
91-
max_length: Option<u32>,
91+
pub max_length: Option<u32>,
9292

9393
/// String format - limited to: "email", "uri", "date", "date-time"
9494
#[serde(skip_serializing_if = "Option::is_none")]
95-
format: Option<Cow<'static, str>>,
95+
pub format: Option<Cow<'static, str>>,
9696
}
9797

9898
impl Default for StringSchema {
@@ -209,7 +209,7 @@ impl StringSchema {
209209
pub struct NumberSchema {
210210
/// Type discriminator
211211
#[serde(rename = "type")]
212-
type_: NumberTypeConst,
212+
pub type_: NumberTypeConst,
213213

214214
/// Optional title for the schema
215215
#[serde(skip_serializing_if = "Option::is_none")]
@@ -303,7 +303,7 @@ impl NumberSchema {
303303
pub struct IntegerSchema {
304304
/// Type discriminator
305305
#[serde(rename = "type")]
306-
type_: IntegerTypeConst,
306+
pub type_: IntegerTypeConst,
307307

308308
/// Optional title for the schema
309309
#[serde(skip_serializing_if = "Option::is_none")]
@@ -394,7 +394,7 @@ impl IntegerSchema {
394394
pub struct BooleanSchema {
395395
/// Type discriminator
396396
#[serde(rename = "type")]
397-
type_: BooleanTypeConst,
397+
pub type_: BooleanTypeConst,
398398

399399
/// Optional title for the schema
400400
#[serde(skip_serializing_if = "Option::is_none")]
@@ -459,23 +459,23 @@ impl BooleanSchema {
459459
pub struct EnumSchema {
460460
/// Type discriminator (always "string" for enums)
461461
#[serde(rename = "type")]
462-
type_: StringTypeConst,
462+
pub type_: StringTypeConst,
463463

464464
/// Allowed enum values (string values only per MCP spec)
465465
#[serde(rename = "enum")]
466-
enum_values: Vec<String>,
466+
pub enum_values: Vec<String>,
467467

468468
/// Optional human-readable names for each enum value
469469
#[serde(skip_serializing_if = "Option::is_none")]
470-
enum_names: Option<Vec<String>>,
470+
pub enum_names: Option<Vec<String>>,
471471

472472
/// Optional title for the schema
473473
#[serde(skip_serializing_if = "Option::is_none")]
474-
title: Option<Cow<'static, str>>,
474+
pub title: Option<Cow<'static, str>>,
475475

476476
/// Human-readable description
477477
#[serde(skip_serializing_if = "Option::is_none")]
478-
description: Option<Cow<'static, str>>,
478+
pub description: Option<Cow<'static, str>>,
479479
}
480480

481481
impl EnumSchema {
@@ -535,7 +535,7 @@ impl EnumSchema {
535535
pub struct ElicitationSchema {
536536
/// Always "object" for elicitation schemas
537537
#[serde(rename = "type")]
538-
type_: ObjectTypeConst,
538+
pub type_: ObjectTypeConst,
539539

540540
/// Optional title for the schema
541541
#[serde(skip_serializing_if = "Option::is_none")]
@@ -565,6 +565,74 @@ impl ElicitationSchema {
565565
}
566566
}
567567

568+
/// Convert from a JSON Schema object (typically generated by schemars)
569+
///
570+
/// This allows converting from JsonObject to ElicitationSchema, which is useful
571+
/// when working with automatically generated schemas from types.
572+
///
573+
/// # Example
574+
///
575+
/// ```rust,ignore
576+
/// use rmcp::model::*;
577+
///
578+
/// let json_schema = schema_for_type::<MyType>();
579+
/// let elicitation_schema = ElicitationSchema::from_json_schema(json_schema)?;
580+
/// ```
581+
///
582+
/// # Errors
583+
///
584+
/// Returns a [`serde_json::Error`] if the JSON object cannot be deserialized
585+
/// into a valid ElicitationSchema.
586+
pub fn from_json_schema(schema: crate::model::JsonObject) -> Result<Self, serde_json::Error> {
587+
serde_json::from_value(serde_json::Value::Object(schema))
588+
}
589+
590+
/// Generate an ElicitationSchema from a Rust type that implements JsonSchema
591+
///
592+
/// This is a convenience method that combines schema generation and conversion.
593+
/// It uses the same schema generation settings as the rest of the MCP SDK.
594+
///
595+
/// # Example
596+
///
597+
/// ```rust,ignore
598+
/// use rmcp::model::*;
599+
/// use schemars::JsonSchema;
600+
/// use serde::{Deserialize, Serialize};
601+
///
602+
/// #[derive(JsonSchema, Serialize, Deserialize)]
603+
/// struct UserInput {
604+
/// name: String,
605+
/// age: u32,
606+
/// }
607+
///
608+
/// let schema = ElicitationSchema::from_type::<UserInput>()?;
609+
/// ```
610+
///
611+
/// # Errors
612+
///
613+
/// Returns a [`serde_json::Error`] if the generated schema cannot be converted
614+
/// to a valid ElicitationSchema.
615+
#[cfg(feature = "schemars")]
616+
pub fn from_type<T>() -> Result<Self, serde_json::Error>
617+
where
618+
T: schemars::JsonSchema,
619+
{
620+
use crate::schemars::generate::SchemaSettings;
621+
622+
let mut settings = SchemaSettings::draft07();
623+
settings.transforms = vec![Box::new(schemars::transform::AddNullable::default())];
624+
let generator = settings.into_generator();
625+
let schema = generator.into_root_schema_for::<T>();
626+
let object = serde_json::to_value(schema).expect("failed to serialize schema");
627+
match object {
628+
serde_json::Value::Object(object) => Self::from_json_schema(object),
629+
_ => panic!(
630+
"Schema serialization produced non-object value: expected JSON object but got {:?}",
631+
object
632+
),
633+
}
634+
}
635+
568636
/// Set the required fields
569637
pub fn with_required(mut self, required: Vec<String>) -> Self {
570638
self.required = Some(required);
@@ -609,10 +677,10 @@ impl ElicitationSchema {
609677
/// ```
610678
#[derive(Debug, Default)]
611679
pub struct ElicitationSchemaBuilder {
612-
properties: BTreeMap<String, PrimitiveSchema>,
613-
required: Vec<String>,
614-
title: Option<Cow<'static, str>>,
615-
description: Option<Cow<'static, str>>,
680+
pub properties: BTreeMap<String, PrimitiveSchema>,
681+
pub required: Vec<String>,
682+
pub title: Option<Cow<'static, str>>,
683+
pub description: Option<Cow<'static, str>>,
616684
}
617685

618686
impl ElicitationSchemaBuilder {

crates/rmcp/src/service/server.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -677,20 +677,16 @@ impl Peer<RoleServer> {
677677
}
678678

679679
// Generate schema automatically from type
680-
let schema_json = crate::handler::server::tool::schema_for_type::<T>();
681-
682-
// Convert JsonObject to ElicitationSchema
683-
let schema: crate::model::ElicitationSchema =
684-
serde_json::from_value(serde_json::Value::Object(schema_json)).map_err(|e| {
685-
ElicitationError::Service(ServiceError::McpError(crate::ErrorData::invalid_params(
686-
format!(
687-
"Invalid schema for type {}: {}",
688-
std::any::type_name::<T>(),
689-
e
690-
),
691-
None,
692-
)))
693-
})?;
680+
let schema = crate::model::ElicitationSchema::from_type::<T>().map_err(|e| {
681+
ElicitationError::Service(ServiceError::McpError(crate::ErrorData::invalid_params(
682+
format!(
683+
"Invalid schema for type {}: {}",
684+
std::any::type_name::<T>(),
685+
e
686+
),
687+
None,
688+
)))
689+
})?;
694690

695691
let response = self
696692
.create_elicitation_with_timeout(

0 commit comments

Comments
 (0)