use smol_str::SmolStr;
use std::collections::HashMap;
use std::path::Path;
use super::deserializer;
pub use super::err::DeserializationError;
use super::parser;
#[derive(Debug, Clone)]
pub enum PropertyType {
Unknown,
Bool,
Integer,
Float,
Number,
String,
Decimal,
Datetime,
Duration,
IpAddr,
Null,
Enum {
variants: Vec<SmolStr>,
},
Array {
element_ty: Box<PropertyType>,
},
Tuple {
types: Vec<PropertyType>,
},
Union {
types: Vec<PropertyType>,
},
Object {
properties: Vec<Property>,
additional_properties: Option<Box<PropertyType>>,
},
Ref {
name: SmolStr,
},
}
#[derive(Debug, Clone)]
pub struct Property {
pub(crate) name: SmolStr,
pub(crate) description: Option<String>,
pub(crate) required: bool,
pub(crate) prop_type: PropertyType,
}
impl Property {
pub fn new(
name: SmolStr,
required: bool,
prop_type: PropertyType,
description: Option<String>,
) -> Self {
Self {
name,
description,
required,
prop_type,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_required(&self) -> bool {
self.required
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn property_type(&self) -> &PropertyType {
&self.prop_type
}
}
#[derive(Debug, Clone)]
pub struct PropertyTypeDef {
pub(crate) name: SmolStr,
pub(crate) prop_type: PropertyType,
pub(crate) description: Option<String>,
}
impl PropertyTypeDef {
pub fn new(name: SmolStr, prop_type: PropertyType, description: Option<String>) -> Self {
Self {
name,
prop_type,
description,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn property_type(&self) -> &PropertyType {
&self.prop_type
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
#[derive(Debug, Clone)]
pub(crate) struct PropertyTypeDefs {
type_defs: HashMap<SmolStr, PropertyTypeDef>,
}
impl PropertyTypeDefs {
pub(crate) fn new(type_defs: HashMap<SmolStr, PropertyTypeDef>) -> Self {
Self { type_defs }
}
pub(crate) fn values(&self) -> impl Iterator<Item = &PropertyTypeDef> {
self.type_defs.values()
}
}
#[derive(Debug, Clone)]
pub struct Parameters {
pub(crate) properties: Vec<Property>,
pub(crate) type_defs: PropertyTypeDefs,
}
impl Parameters {
pub fn new(properties: Vec<Property>, type_defs: HashMap<SmolStr, PropertyTypeDef>) -> Self {
Self {
properties,
type_defs: PropertyTypeDefs::new(type_defs),
}
}
pub fn properties(&self) -> impl Iterator<Item = &Property> {
self.properties.iter()
}
pub fn type_definitions(&self) -> impl Iterator<Item = &PropertyTypeDef> {
self.type_defs.values()
}
}
#[derive(Debug, Clone)]
pub struct ToolDescription {
pub(crate) name: SmolStr,
pub(crate) description: Option<String>,
pub(crate) inputs: Parameters,
pub(crate) outputs: Parameters,
pub(crate) type_defs: PropertyTypeDefs,
}
impl ToolDescription {
pub fn new(
name: SmolStr,
inputs: Parameters,
outputs: Parameters,
type_defs: HashMap<SmolStr, PropertyTypeDef>,
description: Option<String>,
) -> Self {
Self {
name,
description,
inputs,
outputs,
type_defs: PropertyTypeDefs::new(type_defs),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn inputs(&self) -> &Parameters {
&self.inputs
}
pub fn outputs(&self) -> &Parameters {
&self.outputs
}
pub fn type_definitions(&self) -> impl Iterator<Item = &PropertyTypeDef> {
self.type_defs.values()
}
pub fn from_json_str(json_str: &str) -> Result<Self, DeserializationError> {
let mut parser = parser::json_parser::JsonParser::new(json_str);
deserializer::tool_description_from_json_value(parser.get_value()?)
}
pub fn from_json_file<P: AsRef<Path>>(json_file: P) -> Result<Self, DeserializationError> {
let contents = std::fs::read_to_string(json_file.as_ref()).map_err(|e| {
DeserializationError::read_error(json_file.as_ref().into(), format!("{e}"))
})?;
Self::from_json_str(&contents)
}
}
#[derive(Debug, Clone)]
pub struct ServerDescription {
pub(crate) tools: Vec<ToolDescription>,
pub(crate) type_defs: PropertyTypeDefs,
}
impl ServerDescription {
pub fn new(tools: Vec<ToolDescription>, type_defs: HashMap<SmolStr, PropertyTypeDef>) -> Self {
Self {
tools,
type_defs: PropertyTypeDefs::new(type_defs),
}
}
pub fn tool_descriptions(&self) -> impl Iterator<Item = &ToolDescription> {
self.tools.iter()
}
pub fn type_definitions(&self) -> impl Iterator<Item = &PropertyTypeDef> {
self.type_defs.values()
}
pub fn from_json_str(json_str: &str) -> Result<Self, DeserializationError> {
let mut parser = parser::json_parser::JsonParser::new(json_str);
deserializer::server_description_from_json_value(parser.get_value()?)
}
pub fn from_json_file<P: AsRef<Path>>(json_file: P) -> Result<Self, DeserializationError> {
let contents = std::fs::read_to_string(json_file.as_ref()).map_err(|e| {
DeserializationError::read_error(json_file.as_ref().into(), format!("{e}"))
})?;
Self::from_json_str(&contents)
}
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use smol_str::ToSmolStr;
#[test]
fn test_property() {
let property = Property::new(
"Prop".into(),
true,
PropertyType::Bool,
Some("Banana".to_string()),
);
assert!(property.name() == "Prop");
assert!(property.is_required());
assert_matches!(property.property_type(), PropertyType::Bool);
assert_matches!(property.description(), Some("Banana"));
}
#[test]
fn test_type_def() {
let type_def = PropertyTypeDef::new(
"my_type".into(),
PropertyType::Datetime,
Some("My Type".to_string()),
);
assert!(type_def.name() == "my_type");
assert_matches!(type_def.property_type(), PropertyType::Datetime);
assert_matches!(type_def.description(), Some("My Type"));
}
#[test]
fn test_parameters() {
let props = vec![
Property::new("first".into(), true, PropertyType::Bool, None),
Property::new("second".into(), false, PropertyType::Float, None),
];
let type_defs = vec![
(
"my_bool".to_smolstr(),
PropertyTypeDef::new("my_bool".into(), PropertyType::Bool, None),
),
(
"my_int".to_smolstr(),
PropertyTypeDef::new("my_int".into(), PropertyType::Integer, None),
),
]
.into_iter()
.collect();
let params = Parameters::new(props, type_defs);
assert_matches!(
params.properties().map(Property::name).collect::<Vec<_>>(),
["first", "second"]
);
assert_matches!(
params
.properties()
.map(Property::is_required)
.collect::<Vec<_>>(),
[true, false]
);
assert_matches!(
params
.properties()
.map(Property::property_type)
.collect::<Vec<_>>(),
[PropertyType::Bool, PropertyType::Float]
);
assert_matches!(
params
.properties()
.map(Property::description)
.collect::<Vec<_>>(),
[None, None]
);
let type_defs = params.type_definitions().cloned().collect::<Vec<_>>();
assert!(type_defs.len() == 2);
if type_defs.get(0).map(PropertyTypeDef::name) == Some("my_bool") {
assert_matches!(
type_defs
.iter()
.map(PropertyTypeDef::name)
.collect::<Vec<_>>(),
["my_bool", "my_int"]
);
assert_matches!(
type_defs
.iter()
.map(PropertyTypeDef::property_type)
.collect::<Vec<_>>(),
[PropertyType::Bool, PropertyType::Integer]
);
assert_matches!(
type_defs
.iter()
.map(PropertyTypeDef::description)
.collect::<Vec<_>>(),
[None, None]
);
} else {
assert_matches!(
type_defs
.iter()
.map(PropertyTypeDef::name)
.collect::<Vec<_>>(),
["my_int", "my_bool"]
);
assert_matches!(
type_defs
.iter()
.map(PropertyTypeDef::property_type)
.collect::<Vec<_>>(),
[PropertyType::Integer, PropertyType::Bool]
);
assert_matches!(
type_defs
.iter()
.map(PropertyTypeDef::description)
.collect::<Vec<_>>(),
[None, None]
);
}
}
#[test]
fn test_tool_from_json_str_simple() {
let tool_description = r#"{
"name": "check_task_status",
"description": "Check if a task is ready for work",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": "string"}
},
"required": ["task_id"]
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Failed to parse MCP Description");
assert!(tool.name() == "check_task_status");
assert_matches!(
tool.description(),
Some("Check if a task is ready for work")
);
assert!(tool.type_definitions().count() == 0);
assert!(tool.inputs().type_definitions().count() == 0);
assert!(tool.outputs().properties().count() == 0);
assert!(tool.outputs().type_definitions().count() == 0);
let inputs = tool.inputs().properties().cloned().collect::<Vec<_>>();
assert!(inputs.len() == 1);
assert_matches!(inputs.get(0).map(Property::name), Some("task_id"));
assert_matches!(inputs.get(0).map(Property::is_required), Some(true));
assert_matches!(
inputs.get(0).map(Property::property_type),
Some(PropertyType::String)
);
assert_matches!(inputs.get(0).and_then(Property::description), None);
}
#[test]
fn test_server_from_json_str_simple() {
let server_description = r#"{
"result": {
"tools": [{
"name": "check_task_status",
"description": "Check if a task is ready for work",
"inputSchema": {
"type": "object",
"properties": {
"task_id": {"type": "string"}
},
"required": ["task_id"]
}
}]
}
}"#;
let tools = ServerDescription::from_json_str(server_description)
.expect("Failed to parse server description");
assert!(tools.type_definitions().count() == 0);
assert!(tools.tool_descriptions().count() == 1);
let tool = tools.tool_descriptions().next().unwrap();
assert!(tool.name() == "check_task_status");
assert_matches!(
tool.description(),
Some("Check if a task is ready for work")
);
assert!(tool.type_definitions().count() == 0);
assert!(tool.inputs().type_definitions().count() == 0);
assert!(tool.outputs().properties().count() == 0);
assert!(tool.outputs().type_definitions().count() == 0);
let inputs = tool.inputs().properties().cloned().collect::<Vec<_>>();
assert!(inputs.len() == 1);
assert_matches!(inputs.get(0).map(Property::name), Some("task_id"));
assert_matches!(inputs.get(0).map(Property::is_required), Some(true));
assert_matches!(
inputs.get(0).map(Property::property_type),
Some(PropertyType::String)
);
assert_matches!(inputs.get(0).and_then(Property::description), None);
}
#[test]
fn test_result_file_but_result_not_object_error() {
let server_description = r#"{
"result": false
}"#;
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::MissingExpectedAttribute(..))
);
}
#[test]
fn test_result_file_without_tools_list_error() {
let server_description = r#"{
"result": {}
}"#;
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::MissingExpectedAttribute(..))
);
}
#[test]
fn test_result_file_tool_not_array_error() {
let server_description = r#"{
"result": {
"tools": {}
}
}"#;
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_empty_array_of_tools() {
let server_description = "[]";
let tools = ServerDescription::from_json_str(server_description).unwrap();
assert!(tools.tool_descriptions().count() == 0);
assert!(tools.type_definitions().count() == 0);
}
#[test]
fn test_deserialize_wrong_type() {
let server_description = "true";
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_tool_name_wrong_type() {
let server_description = r#"[
{ "name": false }
]"#;
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_tool_name_not_found_error() {
let server_description = r#"[{}]"#;
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::MissingExpectedAttribute(..))
);
}
#[test]
fn test_tool_with_no_inputs_error() {
let server_description = r#"{
"result": {
"tools": [
{
"name": "test_tool",
"description": "a tool for testing",
"properties": {
"comment": "properties should be \"parameters\" or \"inputSchema\""
}
}
]
}
}"#;
assert_matches!(
ServerDescription::from_json_str(server_description),
Err(DeserializationError::MissingExpectedAttribute(..))
);
}
#[test]
fn test_tool_description_not_string_error() {
let tool_description = r#"{
"name": "test_tool",
"description": false,
"inputSchema": {}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_tool_no_description() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {}
}"#;
let tool = ToolDescription::from_json_str(tool_description).unwrap();
assert!(tool.description().is_none());
}
#[test]
fn test_tool_not_object_error() {
let tool_description = "[]";
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_inputs_not_object_error() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": []
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_typedefs_not_object_error() {
let tool_description = r#"{
"name": "test_tool",
"$defs": [],
"inputSchema": {}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_required_contains_non_string_error() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {},
"required": [true]
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_required_is_false() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": { "type": "string" }
},
"required": false
}
}"#;
let tool = ToolDescription::from_json_str(tool_description).unwrap();
assert!(tool.inputs().properties().count() == 1);
let property = tool.inputs().properties().next().unwrap();
assert!(!property.is_required());
assert!(property.name() == "test_attr");
}
#[test]
fn test_required_is_true_error() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": { "type": "string" }
},
"required": true
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_properties_is_false() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": false,
"required": false
}
}"#;
let tool = ToolDescription::from_json_str(tool_description).unwrap();
assert!(tool.inputs().properties().count() == 0);
}
#[test]
fn test_properties_is_true_error() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": true,
"required": false
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_property_with_non_string_description_error() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "string",
"description": false
}
},
"required": false
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_empty_enum_errors() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "string",
"enum": []
}
}
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedValue(..))
);
}
#[test]
fn test_enum_non_string_errors() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "string",
"enum": [true]
}
}
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
);
}
#[test]
fn test_array_items_missing_is_unknown() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "array"
}
}
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Array with unknown items should parse");
assert_eq!(tool.inputs().type_definitions().count(), 0);
assert_eq!(tool.inputs().properties().count(), 1);
let input = tool.inputs().properties().next().unwrap();
assert_eq!(input.name(), "test_attr");
assert_matches!(input.property_type(), PropertyType::Array { element_ty } if matches!(element_ty.as_ref(), PropertyType::Unknown) )
}
#[test]
fn test_array_items_is_null_is_unknown() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "array",
"items": null
}
}
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Array with unknown items should parse");
assert_eq!(tool.inputs().type_definitions().count(), 0);
assert_eq!(tool.inputs().properties().count(), 1);
let input = tool.inputs().properties().next().unwrap();
assert_eq!(input.name(), "test_attr");
assert_matches!(input.property_type(), PropertyType::Array { element_ty } if matches!(element_ty.as_ref(), PropertyType::Unknown) )
}
#[test]
fn test_array_items_is_bool_is_unknown() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "array",
"items": true
}
}
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Array with unknown items should parse");
assert_eq!(tool.inputs().type_definitions().count(), 0);
assert_eq!(tool.inputs().properties().count(), 1);
let input = tool.inputs().properties().next().unwrap();
assert_eq!(input.name(), "test_attr");
assert_matches!(input.property_type(), PropertyType::Array { element_ty } if matches!(element_ty.as_ref(), PropertyType::Unknown) )
}
#[test]
fn test_array_items_is_number_errors() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": "array",
"items": 0
}
}
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
)
}
#[test]
fn test_attr_type_missing_is_unknown() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {}
}
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Array with unknown items should parse");
assert_eq!(tool.inputs().type_definitions().count(), 0);
assert_eq!(tool.inputs().properties().count(), 1);
let input = tool.inputs().properties().next().unwrap();
assert_eq!(input.name(), "test_attr");
assert_matches!(input.property_type(), PropertyType::Unknown)
}
#[test]
fn test_attr_type_is_null_is_unknown() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": null
}
}
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Array with unknown items should parse");
assert_eq!(tool.inputs().type_definitions().count(), 0);
assert_eq!(tool.inputs().properties().count(), 1);
let input = tool.inputs().properties().next().unwrap();
assert_eq!(input.name(), "test_attr");
assert_matches!(input.property_type(), PropertyType::Unknown)
}
#[test]
fn test_attr_type_is_bool_is_unknown() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": true
}
}
}
}"#;
let tool = ToolDescription::from_json_str(tool_description)
.expect("Array with unknown items should parse");
assert_eq!(tool.inputs().type_definitions().count(), 0);
assert_eq!(tool.inputs().properties().count(), 1);
let input = tool.inputs().properties().next().unwrap();
assert_eq!(input.name(), "test_attr");
assert_matches!(input.property_type(), PropertyType::Unknown)
}
#[test]
fn test_property_type_is_number_errors() {
let tool_description = r#"{
"name": "test_tool",
"inputSchema": {
"type": "object",
"properties": {
"test_attr": {
"type": 3
}
}
}
}"#;
assert_matches!(
ToolDescription::from_json_str(tool_description),
Err(DeserializationError::UnexpectedType(..))
)
}
}