feat!: align model types with MCP 2025-11-25 spec (#927)

* feat!: align model types with MCP 2025-11-25 spec

* fix: complete 2025-11-25 model spec conformance
This commit is contained in:
Dale Seo 2026-06-24 20:41:13 -04:00 • committed by GitHub
commit 77932141e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -49,8 +49,8 @@ const_string!(ArrayTypeConst = "array");
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
pub enum PrimitiveSchema {
#[non_exhaustive]
pub enum PrimitiveSchemaDefinition {
/// Enum property (explicit enum schema)
Enum(EnumSchema),
/// String property (with optional enum constraint)
@ -63,6 +63,9 @@ pub enum PrimitiveSchema {
Boolean(BooleanSchema),
}
#[deprecated(since = "2.0.0", note = "Renamed to PrimitiveSchemaDefinition")]
pub type PrimitiveSchema = PrimitiveSchemaDefinition;
// =============================================================================
// STRING SCHEMA
// =============================================================================
@ -71,7 +74,7 @@ pub enum PrimitiveSchema {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub enum StringFormat {
/// Email address format
Email,
@ -346,7 +349,7 @@ impl NumberSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct IntegerSchema {
/// Type discriminator
#[serde(rename = "type")]
@ -513,7 +516,7 @@ impl BooleanSchema {
/// Represent single entry for titled item
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ConstTitle {
#[serde(rename = "const")]
pub const_: String,
@ -534,7 +537,7 @@ impl ConstTitle {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct LegacyEnumSchema {
#[serde(rename = "type")]
pub type_: StringTypeConst,
@ -546,6 +549,21 @@ pub struct LegacyEnumSchema {
pub enum_: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
impl LegacyEnumSchema {
pub fn new(enum_values: Vec<String>) -> Self {
Self {
type_: StringTypeConst,
title: None,
description: None,
enum_: enum_values,
enum_names: None,
default: None,
}
}
}
/// Untitled single-select
@ -601,7 +619,7 @@ impl TitledSingleSelectEnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub enum SingleSelectEnumSchema {
Untitled(UntitledSingleSelectEnumSchema),
Titled(TitledSingleSelectEnumSchema),
@ -610,7 +628,7 @@ pub enum SingleSelectEnumSchema {
/// Items for untitled multi-select options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct UntitledItems {
#[serde(rename = "type")]
pub type_: StringTypeConst,
@ -618,10 +636,19 @@ pub struct UntitledItems {
pub enum_: Vec<String>,
}
impl UntitledItems {
pub fn new(enum_values: Vec<String>) -> Self {
Self {
type_: StringTypeConst,
enum_: enum_values,
}
}
}
/// Items for titled multi-select options
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct TitledItems {
// MCP spec requires "anyOf" for multi-select enums (allows any combination)
// Alias "oneOf" for compatibility with schemars
@ -727,7 +754,7 @@ impl TitledMultiSelectEnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub enum MultiSelectEnumSchema {
Untitled(UntitledMultiSelectEnumSchema),
Titled(TitledMultiSelectEnumSchema),
@ -751,7 +778,7 @@ pub enum MultiSelectEnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[expect(clippy::exhaustive_enums, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub enum EnumSchema {
Single(SingleSelectEnumSchema),
Multi(MultiSelectEnumSchema),
@ -1090,7 +1117,7 @@ impl EnumSchema {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
#[non_exhaustive]
pub struct ElicitationSchema {
/// Always "object" for elicitation schemas
#[serde(rename = "type")]
@ -1101,7 +1128,7 @@ pub struct ElicitationSchema {
pub title: Option<Cow<'static, str>>,
/// Property definitions (must be primitive types)
pub properties: BTreeMap<String, PrimitiveSchema>,
pub properties: BTreeMap<String, PrimitiveSchemaDefinition>,
/// List of required property names
#[serde(skip_serializing_if = "Option::is_none")]
@ -1114,7 +1141,7 @@ pub struct ElicitationSchema {
impl ElicitationSchema {
/// Create a new elicitation schema with the given properties
pub fn new(properties: BTreeMap<String, PrimitiveSchema>) -> Self {
pub fn new(properties: BTreeMap<String, PrimitiveSchemaDefinition>) -> Self {
Self {
type_: ObjectTypeConst,
title: None,
@ -1237,7 +1264,7 @@ impl ElicitationSchema {
#[derive(Debug, Default)]
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
pub struct ElicitationSchemaBuilder {
pub properties: BTreeMap<String, PrimitiveSchema>,
pub properties: BTreeMap<String, PrimitiveSchemaDefinition>,
pub required: Vec<String>,
pub title: Option<Cow<'static, str>>,
pub description: Option<Cow<'static, str>>,
@ -1250,13 +1277,17 @@ impl ElicitationSchemaBuilder {
}
/// Add a property to the schema
pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
pub fn property(mut self, name: impl Into<String>, schema: PrimitiveSchemaDefinition) -> Self {
self.properties.insert(name.into(), schema);
self
}
/// Add a required property to the schema
pub fn required_property(mut self, name: impl Into<String>, schema: PrimitiveSchema) -> Self {
pub fn required_property(
mut self,
name: impl Into<String>,
schema: PrimitiveSchemaDefinition,
) -> Self {
let name_str = name.into();
self.required.push(name_str.clone());
self.properties.insert(name_str, schema);
@ -1264,7 +1295,7 @@ impl ElicitationSchemaBuilder {
}
// ===========================================================================
// TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchema wrapper
// TYPED PROPERTY METHODS - Cleaner API without PrimitiveSchemaDefinition wrapper
// ===========================================================================
/// Add a string property with custom builder (required)
@ -1273,8 +1304,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(StringSchema) -> StringSchema,
) -> Self {
self.properties
.insert(name.into(), PrimitiveSchema::String(f(StringSchema::new())));
self.properties.insert(
name.into(),
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
);
self
}
@ -1286,8 +1319,10 @@ impl ElicitationSchemaBuilder {
) -> Self {
let name_str = name.into();
self.required.push(name_str.clone());
self.properties
.insert(name_str, PrimitiveSchema::String(f(StringSchema::new())));
self.properties.insert(
name_str,
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
);
self
}
@ -1297,8 +1332,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(NumberSchema) -> NumberSchema,
) -> Self {
self.properties
.insert(name.into(), PrimitiveSchema::Number(f(NumberSchema::new())));
self.properties.insert(
name.into(),
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
);
self
}
@ -1310,8 +1347,10 @@ impl ElicitationSchemaBuilder {
) -> Self {
let name_str = name.into();
self.required.push(name_str.clone());
self.properties
.insert(name_str, PrimitiveSchema::Number(f(NumberSchema::new())));
self.properties.insert(
name_str,
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
);
self
}
@ -1323,7 +1362,7 @@ impl ElicitationSchemaBuilder {
) -> Self {
self.properties.insert(
name.into(),
PrimitiveSchema::Integer(f(IntegerSchema::new())),
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
);
self
}
@ -1336,8 +1375,10 @@ impl ElicitationSchemaBuilder {
) -> Self {
let name_str = name.into();
self.required.push(name_str.clone());
self.properties
.insert(name_str, PrimitiveSchema::Integer(f(IntegerSchema::new())));
self.properties.insert(
name_str,
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
);
self
}
@ -1349,7 +1390,7 @@ impl ElicitationSchemaBuilder {
) -> Self {
self.properties.insert(
name.into(),
PrimitiveSchema::Boolean(f(BooleanSchema::new())),
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
);
self
}
@ -1362,8 +1403,10 @@ impl ElicitationSchemaBuilder {
) -> Self {
let name_str = name.into();
self.required.push(name_str.clone());
self.properties
.insert(name_str, PrimitiveSchema::Boolean(f(BooleanSchema::new())));
self.properties.insert(
name_str,
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
);
self
}
@ -1373,22 +1416,28 @@ impl ElicitationSchemaBuilder {
/// Add a required string property
pub fn required_string(self, name: impl Into<String>) -> Self {
self.required_property(name, PrimitiveSchema::String(StringSchema::new()))
self.required_property(name, PrimitiveSchemaDefinition::String(StringSchema::new()))
}
/// Add an optional string property
pub fn optional_string(self, name: impl Into<String>) -> Self {
self.property(name, PrimitiveSchema::String(StringSchema::new()))
self.property(name, PrimitiveSchemaDefinition::String(StringSchema::new()))
}
/// Add a required email property
pub fn required_email(self, name: impl Into<String>) -> Self {
self.required_property(name, PrimitiveSchema::String(StringSchema::email()))
self.required_property(
name,
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
}
/// Add an optional email property
pub fn optional_email(self, name: impl Into<String>) -> Self {
self.property(name, PrimitiveSchema::String(StringSchema::email()))
self.property(
name,
PrimitiveSchemaDefinition::String(StringSchema::email()),
)
}
/// Add a required string property with custom builder
@ -1397,7 +1446,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(StringSchema) -> StringSchema,
) -> Self {
self.required_property(name, PrimitiveSchema::String(f(StringSchema::new())))
self.required_property(
name,
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
)
}
/// Add an optional string property with custom builder
@ -1406,7 +1458,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(StringSchema) -> StringSchema,
) -> Self {
self.property(name, PrimitiveSchema::String(f(StringSchema::new())))
self.property(
name,
PrimitiveSchemaDefinition::String(f(StringSchema::new())),
)
}
// Convenience methods for numbers
@ -1415,7 +1470,7 @@ impl ElicitationSchemaBuilder {
pub fn required_number(self, name: impl Into<String>, min: f64, max: f64) -> Self {
self.required_property(
name,
PrimitiveSchema::Number(NumberSchema::new().range(min, max)),
PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)),
)
}
@ -1423,7 +1478,7 @@ impl ElicitationSchemaBuilder {
pub fn optional_number(self, name: impl Into<String>, min: f64, max: f64) -> Self {
self.property(
name,
PrimitiveSchema::Number(NumberSchema::new().range(min, max)),
PrimitiveSchemaDefinition::Number(NumberSchema::new().range(min, max)),
)
}
@ -1433,7 +1488,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(NumberSchema) -> NumberSchema,
) -> Self {
self.required_property(name, PrimitiveSchema::Number(f(NumberSchema::new())))
self.required_property(
name,
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
)
}
/// Add an optional number property with custom builder
@ -1442,7 +1500,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(NumberSchema) -> NumberSchema,
) -> Self {
self.property(name, PrimitiveSchema::Number(f(NumberSchema::new())))
self.property(
name,
PrimitiveSchemaDefinition::Number(f(NumberSchema::new())),
)
}
// Convenience methods for integers
@ -1451,7 +1512,7 @@ impl ElicitationSchemaBuilder {
pub fn required_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
self.required_property(
name,
PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)),
)
}
@ -1459,7 +1520,7 @@ impl ElicitationSchemaBuilder {
pub fn optional_integer(self, name: impl Into<String>, min: i64, max: i64) -> Self {
self.property(
name,
PrimitiveSchema::Integer(IntegerSchema::new().range(min, max)),
PrimitiveSchemaDefinition::Integer(IntegerSchema::new().range(min, max)),
)
}
@ -1469,7 +1530,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(IntegerSchema) -> IntegerSchema,
) -> Self {
self.required_property(name, PrimitiveSchema::Integer(f(IntegerSchema::new())))
self.required_property(
name,
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
)
}
/// Add an optional integer property with custom builder
@ -1478,21 +1542,27 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(IntegerSchema) -> IntegerSchema,
) -> Self {
self.property(name, PrimitiveSchema::Integer(f(IntegerSchema::new())))
self.property(
name,
PrimitiveSchemaDefinition::Integer(f(IntegerSchema::new())),
)
}
// Convenience methods for booleans
/// Add a required boolean property
pub fn required_bool(self, name: impl Into<String>) -> Self {
self.required_property(name, PrimitiveSchema::Boolean(BooleanSchema::new()))
self.required_property(
name,
PrimitiveSchemaDefinition::Boolean(BooleanSchema::new()),
)
}
/// Add an optional boolean property with default value
pub fn optional_bool(self, name: impl Into<String>, default: bool) -> Self {
self.property(
name,
PrimitiveSchema::Boolean(BooleanSchema::new().with_default(default)),
PrimitiveSchemaDefinition::Boolean(BooleanSchema::new().with_default(default)),
)
}
@ -1502,7 +1572,10 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(BooleanSchema) -> BooleanSchema,
) -> Self {
self.required_property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new())))
self.required_property(
name,
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
)
}
/// Add an optional boolean property with custom builder
@ -1511,19 +1584,22 @@ impl ElicitationSchemaBuilder {
name: impl Into<String>,
f: impl FnOnce(BooleanSchema) -> BooleanSchema,
) -> Self {
self.property(name, PrimitiveSchema::Boolean(f(BooleanSchema::new())))
self.property(
name,
PrimitiveSchemaDefinition::Boolean(f(BooleanSchema::new())),
)
}
// Enum convenience methods
/// Add a required enum property using EnumSchema
pub fn required_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
self.required_property(name, PrimitiveSchema::Enum(enum_schema))
self.required_property(name, PrimitiveSchemaDefinition::Enum(enum_schema))
}
/// Add an optional enum property using EnumSchema
pub fn optional_enum_schema(self, name: impl Into<String>, enum_schema: EnumSchema) -> Self {
self.property(name, PrimitiveSchema::Enum(enum_schema))
self.property(name, PrimitiveSchemaDefinition::Enum(enum_schema))
}
/// Add a required enum property using values. Creates an untitled single-select enum.
@ -1534,12 +1610,13 @@ impl ElicitationSchemaBuilder {
pub fn required_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
self.required_property(
name,
PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema {
PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema {
type_: StringTypeConst,
title: None,
description: None,
enum_: values,
enum_names: None,
default: None,
})),
)
}
@ -1552,12 +1629,13 @@ impl ElicitationSchemaBuilder {
pub fn optional_enum(self, name: impl Into<String>, values: Vec<String>) -> Self {
self.property(
name,
PrimitiveSchema::Enum(EnumSchema::Legacy(LegacyEnumSchema {
PrimitiveSchemaDefinition::Enum(EnumSchema::Legacy(LegacyEnumSchema {
type_: StringTypeConst,
title: None,
description: None,
enum_: values,
enum_names: None,
default: None,
})),
)
}
@ -1732,6 +1810,7 @@ mod tests {
description: Some("A legacy enum schema".into()),
enum_: vec!["A".to_string(), "B".to_string()],
enum_names: Some(vec!["Option A".to_string(), "Option B".to_string()]),
default: None,
});
let json = serde_json::to_value(&schema)?;
@ -1776,6 +1855,7 @@ mod tests {
description: None,
enum_: vec!["a".to_string(), "b".to_string()],
enum_names: None,
default: None,
});
let json = serde_json::to_value(&schema)?;
assert!(!json.as_object().unwrap().contains_key("enumNames"));
@ -1970,14 +2050,14 @@ mod tests {
"type": "string",
"enum": ["a", "b"]
});
let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
assert!(matches!(schema, PrimitiveSchema::Enum(_)));
let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap();
assert!(matches!(schema, PrimitiveSchemaDefinition::Enum(_)));
// Test that string schemas deserialize as String variant
let json = json!({
"type": "string"
});
let schema: PrimitiveSchema = serde_json::from_value(json).unwrap();
assert!(matches!(schema, PrimitiveSchema::String(_)));
let schema: PrimitiveSchemaDefinition = serde_json::from_value(json).unwrap();
assert!(matches!(schema, PrimitiveSchemaDefinition::String(_)));
}
#[test]