use anyhow::Result; use rmcp::{ ServerHandler, ServiceExt, handler::server::{router::tool::ToolRouter, tool::Parameters}, model::*, schemars, tool, tool_handler, tool_router, }; use serde::Deserialize; use serde::de::DeserializeOwned; const NWS_API_BASE: &str = "https://api.weather.gov"; const USER_AGENT: &str = "weather-app/1.0"; #[derive(Debug, Deserialize)] struct AlertsResponse { features: Vec, } #[derive(Debug, Deserialize)] struct AlertFeature { properties: AlertProperties, } #[derive(Debug, Deserialize)] struct AlertProperties { event: Option, #[serde(rename = "areaDesc")] area_desc: Option, severity: Option, description: Option, instruction: Option, } #[derive(Debug, Deserialize)] struct PointsResponse { properties: PointsProperties, } #[derive(Debug, Deserialize)] struct PointsProperties { forecast: String, } #[derive(Debug, Deserialize)] struct ForecastResponse { properties: ForecastProperties, } #[derive(Debug, Deserialize)] struct ForecastProperties { periods: Vec, } #[derive(Debug, Deserialize)] struct ForecastPeriod { name: String, temperature: i32, #[serde(rename = "temperatureUnit")] temperature_unit: String, #[serde(rename = "windSpeed")] wind_speed: String, #[serde(rename = "windDirection")] wind_direction: String, #[serde(rename = "detailedForecast")] detailed_forecast: String, } async fn make_nws_request(url: &str) -> Result { let client = reqwest::Client::new(); let rsp = client .get(url) .header(reqwest::header::USER_AGENT, USER_AGENT) .header(reqwest::header::ACCEPT, "application/geo+json") .send() .await? .error_for_status()?; Ok(rsp.json::().await?) } fn format_alert(feature: &AlertFeature) -> String { let props = &feature.properties; format!( "Event: {}\nArea: {}\nSeverity: {}\nDescription: {}\nInstructions: {}", props.event.as_deref().unwrap_or("Unknown"), props.area_desc.as_deref().unwrap_or("Unknown"), props.severity.as_deref().unwrap_or("Unknown"), props .description .as_deref() .unwrap_or("No description available"), props .instruction .as_deref() .unwrap_or("No specific instructions provided") ) } fn format_period(period: &ForecastPeriod) -> String { format!( "{}:\nTemperature: {}°{}\nWind: {} {}\nForecast: {}", period.name, period.temperature, period.temperature_unit, period.wind_speed, period.wind_direction, period.detailed_forecast ) } #[derive(serde::Deserialize, schemars::JsonSchema)] pub struct MCPForecastRequest { latitude: f32, longitude: f32, } #[derive(serde::Deserialize, schemars::JsonSchema)] pub struct MCPAlertRequest { state: String, } pub struct Weather { tool_router: ToolRouter, } #[tool_router] impl Weather { fn new() -> Self { Self { tool_router: Self::tool_router(), } } #[tool(description = "Get weather alerts for a US state.")] async fn get_alerts( &self, Parameters(MCPAlertRequest { state }): Parameters, ) -> String { let url = format!( "{}/alerts/active/area/{}", NWS_API_BASE, state.to_uppercase() ); match make_nws_request::(&url).await { Ok(data) => { if data.features.is_empty() { "No active alerts for this state.".to_string() } else { data.features .iter() .map(format_alert) .collect::>() .join("\n---\n") } } Err(_) => "Unable to fetch alerts or no alerts found.".to_string(), } } #[tool(description = "Get weather forecast for a location.")] async fn get_forecast( &self, Parameters(MCPForecastRequest { latitude, longitude, }): Parameters, ) -> String { let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}"); let Ok(points_data) = make_nws_request::(&points_url).await else { return "Unable to fetch forecast data for this location.".to_string(); }; let forecast_url = points_data.properties.forecast; let Ok(forecast_data) = make_nws_request::(&forecast_url).await else { return "Unable to fetch forecast data for this location.".to_string(); }; let periods = &forecast_data.properties.periods; let forecast_summary: String = periods .iter() .take(5) // Next 5 periods only .map(format_period) .collect::>() .join("\n---\n"); forecast_summary } } #[tool_handler] impl ServerHandler for Weather { fn get_info(&self) -> ServerInfo { ServerInfo { capabilities: ServerCapabilities::builder().enable_tools().build(), ..Default::default() } } } #[tokio::main] async fn main() -> Result<()> { let transport = (tokio::io::stdin(), tokio::io::stdout()); let service = Weather::new().serve(transport).await?; service.waiting().await?; Ok(()) }