use async_trait::async_trait;
use pmcp::{RequestHandlerExtra, Result, Server, ServerCapabilities, ToolHandler};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
enum WeatherCondition {
Sunny,
Cloudy,
Rainy,
Stormy,
Snowy,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Temperature {
celsius: f64,
fahrenheit: f64,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Wind {
speed_kmh: f64,
direction: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct WeatherData {
temperature: Temperature,
conditions: WeatherCondition,
humidity: f64, wind: Wind,
location: String,
timestamp: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Product {
id: String,
name: String,
price: f64,
currency: String,
category: String,
in_stock: bool,
rating: f64, }
#[derive(Debug, Serialize, Deserialize, Clone)]
struct UserProfile {
user_id: String,
username: String,
email: String,
full_name: String,
age: u32,
preferences: HashMap<String, Value>,
is_premium: bool,
join_date: String,
}
struct WeatherTool;
#[async_trait]
impl ToolHandler for WeatherTool {
async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> Result<Value> {
let city = args
.get("city")
.and_then(|v| v.as_str())
.unwrap_or("Unknown City");
let country = args
.get("country")
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
let weather_data = WeatherData {
temperature: Temperature {
celsius: 22.5,
fahrenheit: 72.5,
},
conditions: WeatherCondition::Sunny,
humidity: 65.0,
wind: Wind {
speed_kmh: 15.2,
direction: "NW".to_string(),
},
location: format!("{}, {}", city, country),
timestamp: chrono::Utc::now().to_rfc3339(),
};
Ok(json!({
"content": [{
"type": "text",
"text": format!("Weather data for {}", weather_data.location)
}],
"structuredContent": weather_data,
"isError": false
}))
}
}
struct ProductTool;
#[async_trait]
impl ToolHandler for ProductTool {
async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> Result<Value> {
let product_id = args
.get("product_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let product = Product {
id: product_id.to_string(),
name: format!("Product {}", product_id),
price: 99.99,
currency: "USD".to_string(),
category: "Electronics".to_string(),
in_stock: true,
rating: 4.5,
};
Ok(json!({
"content": [{
"type": "text",
"text": format!("Product information for {}", product.name)
}],
"structuredContent": product,
"isError": false
}))
}
}
struct UserProfileTool;
#[async_trait]
impl ToolHandler for UserProfileTool {
async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> Result<Value> {
let user_id = args
.get("user_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let mut preferences = HashMap::new();
preferences.insert("theme".to_string(), json!("dark"));
preferences.insert("notifications".to_string(), json!(true));
preferences.insert("language".to_string(), json!("en"));
let user_profile = UserProfile {
user_id: user_id.to_string(),
username: format!("user_{}", user_id),
email: format!("user_{}@example.com", user_id),
full_name: "John Doe".to_string(),
age: 30,
preferences,
is_premium: true,
join_date: "2024-01-15T10:30:00Z".to_string(),
};
Ok(json!({
"content": [{
"type": "text",
"text": format!("User profile for {}", user_profile.username)
}],
"structuredContent": user_profile,
"isError": false
}))
}
}
struct ValidatedDataTool;
#[async_trait]
impl ToolHandler for ValidatedDataTool {
async fn handle(&self, args: Value, _extra: RequestHandlerExtra) -> Result<Value> {
let required_fields = ["name", "email", "age"];
let mut missing_fields = Vec::new();
for field in &required_fields {
if args.get(field).is_none() {
missing_fields.push(field.to_string());
}
}
if !missing_fields.is_empty() {
return Ok(json!({
"content": [{
"type": "text",
"text": format!("Validation failed: missing required fields: {}",
missing_fields.join(", "))
}],
"structuredContent": {
"validation_result": {
"is_valid": false,
"missing_fields": missing_fields,
"error_code": "MISSING_REQUIRED_FIELDS"
}
},
"isError": true
}));
}
Ok(json!({
"content": [{
"type": "text",
"text": "Data validation passed successfully"
}],
"structuredContent": {
"validation_result": {
"is_valid": true,
"validated_data": args,
"timestamp": chrono::Utc::now().to_rfc3339()
}
},
"isError": false
}))
}
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
println!("🏗️ Structured Output Schema Example");
println!("====================================");
let server = Server::builder()
.name("structured-output-schema-server")
.version("1.0.0")
.capabilities(ServerCapabilities::tools_only())
.tool("get_weather", WeatherTool)
.tool("get_product", ProductTool)
.tool("get_user_profile", UserProfileTool)
.tool("validate_data", ValidatedDataTool)
.build()?;
println!("📋 Available tools with structured output:");
println!(" • get_weather - Returns structured weather data");
println!(" • get_product - Returns structured product information");
println!(" • get_user_profile - Returns structured user profiles");
println!(" • validate_data - Demonstrates structured validation");
println!();
println!("🚀 Server starting on stdio...");
println!("💡 Each tool returns both human-readable content and structured data");
println!("📊 Structured data includes type information and validation");
println!();
server.run_stdio().await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_weather_data_serialization() {
let weather = WeatherData {
temperature: Temperature {
celsius: 20.0,
fahrenheit: 68.0,
},
conditions: WeatherCondition::Sunny,
humidity: 50.0,
wind: Wind {
speed_kmh: 10.0,
direction: "N".to_string(),
},
location: "Test City".to_string(),
timestamp: "2024-01-15T10:30:00Z".to_string(),
};
let json_str = serde_json::to_string(&weather).unwrap();
let deserialized: WeatherData = serde_json::from_str(&json_str).unwrap();
assert_eq!(weather.location, deserialized.location);
assert_eq!(
weather.temperature.celsius,
deserialized.temperature.celsius
);
}
#[test]
fn test_product_data_validation() {
let product = Product {
id: "test-123".to_string(),
name: "Test Product".to_string(),
price: 29.99,
currency: "USD".to_string(),
category: "Test".to_string(),
in_stock: true,
rating: 4.0,
};
assert!(product.rating >= 0.0 && product.rating <= 5.0);
assert!(product.price >= 0.0);
assert!(!product.id.is_empty());
}
#[test]
fn test_weather_conditions_serialization() {
let conditions = vec![
WeatherCondition::Sunny,
WeatherCondition::Cloudy,
WeatherCondition::Rainy,
WeatherCondition::Stormy,
WeatherCondition::Snowy,
];
for condition in conditions {
let json = serde_json::to_value(&condition).unwrap();
let deserialized: WeatherCondition = serde_json::from_value(json).unwrap();
let _ = deserialized;
}
}
}