#![cfg(feature = "server")]
use anyhow::Result;
use mcplease::{
structured_output,
traits::{AsToolSchema, Tool, ToolMeta, ToolOutput},
types::{CallToolResult, ContentBlock, Example, RequestContext, ToolAnnotations},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetWeather {
location: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Weather {
temperature: f64,
conditions: String,
}
structured_output!(Weather);
impl ToolMeta for GetWeather {
fn examples() -> Vec<Example<Self>> {
vec![Example {
description: "by city",
item: Self {
location: "Portland".into(),
},
}]
}
fn annotations() -> Option<ToolAnnotations> {
Some(ToolAnnotations {
read_only_hint: Some(true),
open_world_hint: Some(true),
..ToolAnnotations::default()
})
}
fn title() -> Option<&'static str> {
Some("Weather Lookup")
}
}
impl Tool<()> for GetWeather {
type Output = Weather;
fn execute(self, _state: &mut (), _context: &RequestContext) -> Result<Self::Output> {
Ok(Weather {
temperature: 22.5,
conditions: "Partly cloudy".into(),
})
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct Echo {
message: String,
}
impl ToolMeta for Echo {}
impl Tool<()> for Echo {
type Output = String;
fn execute(self, _state: &mut (), _context: &RequestContext) -> Result<Self::Output> {
Ok(self.message)
}
}
#[test]
fn structured_output_advertises_a_schema_and_fills_both_halves() {
let schema = <GetWeather as AsToolSchema<()>>::schema();
let output_schema = schema.output_schema.expect("output schema advertised");
assert_eq!(output_schema["properties"]["temperature"]["type"], "number");
assert_eq!(output_schema["type"], "object");
let weather = GetWeather {
location: "Portland".into(),
}
.execute(&mut (), &RequestContext::default())
.unwrap();
let result = CallToolResult::from_content(weather.to_content(), weather.structured_content());
assert_eq!(
result.structured_content,
Some(json!({"temperature": 22.5, "conditions": "Partly cloudy"}))
);
let [ContentBlock::Text { text, .. }] = &result.content[..] else {
panic!("expected one text block");
};
assert_eq!(
serde_json::from_str::<Weather>(text).unwrap(),
Weather {
temperature: 22.5,
conditions: "Partly cloudy".into()
}
);
}
#[test]
fn string_output_stays_plain_text_with_no_structured_half() {
let schema = <Echo as AsToolSchema<()>>::schema();
assert_eq!(schema.output_schema, None);
assert_eq!(schema.title, None);
assert!(schema.annotations.is_none());
let output = Echo {
message: "hello".into(),
}
.execute(&mut (), &RequestContext::default())
.unwrap();
let [ContentBlock::Text { text, .. }] = &output.to_content()[..] else {
panic!("expected one text block");
};
assert_eq!(text, "hello");
assert_eq!(output.structured_content(), None);
assert_eq!(output.to_text(), "hello");
}
#[test]
fn tool_meta_reaches_the_advertised_definition() {
let schema = <GetWeather as AsToolSchema<()>>::schema();
assert_eq!(schema.name, "GetWeather");
assert_eq!(schema.description.as_deref(), Some("Look up the weather."));
assert_eq!(schema.title.as_deref(), Some("Weather Lookup"));
let annotations = schema.annotations.expect("annotations advertised");
assert_eq!(annotations.read_only_hint, Some(true));
assert_eq!(annotations.destructive_hint, None);
let examples = &schema.input_schema["examples"];
assert_eq!(examples[0]["description"], "by city");
assert_eq!(examples[0]["location"], "Portland");
}