1use rmcp::{
2 handler::server::{router::tool::ToolRouter, ServerHandler, wrapper::Parameters},
3 model::*,
4 ErrorData as McpError,
5};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug)]
10pub struct WeatherServer {
11 pub tool_router: ToolRouter<Self>,
12 client: reqwest::Client,
13}
14
15impl Default for WeatherServer {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl WeatherServer {
22 pub fn new() -> Self {
23 Self {
24 tool_router: Self::tool_router(),
25 client: reqwest::Client::new(),
26 }
27 }
28
29 async fn fetch_weather(&self, location: &str) -> Result<WttrResponse, McpError> {
30 let url = format!("https://wttr.in/{}?format=j1", urlencoding::encode(location));
31
32 let response = self.client
33 .get(&url)
34 .header("User-Agent", "rmcp-weather/0.1.0")
35 .send()
36 .await
37 .map_err(|e| McpError::internal_error(format!("HTTP request failed: {}", e), None))?;
38
39 if !response.status().is_success() {
40 return Err(McpError::internal_error(
41 format!("Weather API returned status: {}", response.status()),
42 None,
43 ));
44 }
45
46 response
47 .json::<WttrResponse>()
48 .await
49 .map_err(|e| McpError::internal_error(format!("Failed to parse weather data: {}", e), None))
50 }
51}
52
53#[derive(Debug, Deserialize)]
55pub struct WttrResponse {
56 pub current_condition: Vec<CurrentCondition>,
57 pub nearest_area: Vec<NearestArea>,
58 pub weather: Vec<WeatherDay>,
59}
60
61#[derive(Debug, Deserialize)]
62pub struct CurrentCondition {
63 pub temp_F: String,
64 pub temp_C: String,
65 #[serde(rename = "FeelsLikeF")]
66 pub feels_like_f: String,
67 #[serde(rename = "FeelsLikeC")]
68 pub feels_like_c: String,
69 pub humidity: String,
70 pub weatherDesc: Vec<WeatherDesc>,
71 pub windspeedMiles: String,
72 pub windspeedKmph: String,
73 pub winddir16Point: String,
74 pub precipMM: String,
75 pub visibility: String,
76 pub pressure: String,
77 pub uvIndex: String,
78}
79
80#[derive(Debug, Deserialize)]
81pub struct WeatherDesc {
82 pub value: String,
83}
84
85#[derive(Debug, Deserialize)]
86pub struct NearestArea {
87 pub areaName: Vec<AreaValue>,
88 pub region: Vec<AreaValue>,
89 pub country: Vec<AreaValue>,
90}
91
92#[derive(Debug, Deserialize)]
93pub struct AreaValue {
94 pub value: String,
95}
96
97#[derive(Debug, Deserialize)]
98pub struct WeatherDay {
99 pub date: String,
100 pub maxtempF: String,
101 pub maxtempC: String,
102 pub mintempF: String,
103 pub mintempC: String,
104 pub hourly: Vec<HourlyForecast>,
105}
106
107#[derive(Debug, Deserialize)]
108pub struct HourlyForecast {
109 pub time: String,
110 pub tempF: String,
111 pub tempC: String,
112 pub weatherDesc: Vec<WeatherDesc>,
113 pub chanceofrain: String,
114}
115
116#[derive(Debug, Serialize, Deserialize, JsonSchema)]
118pub struct LocationParams {
119 #[schemars(description = "Location to get weather for (city name, zip code, or 'lat,lon')")]
120 pub location: String,
121}
122
123#[derive(Debug, Serialize, Deserialize, JsonSchema)]
124pub struct ForecastParams {
125 #[schemars(description = "Location to get forecast for")]
126 pub location: String,
127 #[schemars(description = "Number of days (1-3, default 3)")]
128 #[serde(default)]
129 pub days: Option<u8>,
130}
131
132#[rmcp::tool_router]
133impl WeatherServer {
134 #[rmcp::tool(description = "Get current weather conditions for a location")]
135 pub async fn get_weather(
136 &self,
137 Parameters(params): Parameters<LocationParams>,
138 ) -> Result<CallToolResult, McpError> {
139 let data = self.fetch_weather(¶ms.location).await?;
140
141 let current = data.current_condition.first()
142 .ok_or_else(|| McpError::internal_error("No current conditions", None))?;
143
144 let area = data.nearest_area.first()
145 .map(|a| format!("{}, {}",
146 a.areaName.first().map(|v| v.value.as_str()).unwrap_or("Unknown"),
147 a.region.first().map(|v| v.value.as_str()).unwrap_or("")
148 ))
149 .unwrap_or_else(|| params.location.clone());
150
151 let desc = current.weatherDesc.first()
152 .map(|d| d.value.as_str())
153 .unwrap_or("Unknown");
154
155 let output = format!(
156 "Weather for {}:\n\
157 Conditions: {}\n\
158 Temperature: {}°F / {}°C\n\
159 Feels like: {}°F / {}°C\n\
160 Humidity: {}%\n\
161 Wind: {} mph {} ({})\n\
162 Visibility: {} miles\n\
163 Pressure: {} mb\n\
164 UV Index: {}",
165 area, desc,
166 current.temp_F, current.temp_C,
167 current.feels_like_f, current.feels_like_c,
168 current.humidity,
169 current.windspeedMiles, current.winddir16Point, current.windspeedKmph,
170 current.visibility,
171 current.pressure,
172 current.uvIndex
173 );
174
175 Ok(CallToolResult::success(vec![Content::text(output)]))
176 }
177
178 #[rmcp::tool(description = "Get weather forecast for upcoming days")]
179 pub async fn get_forecast(
180 &self,
181 Parameters(params): Parameters<ForecastParams>,
182 ) -> Result<CallToolResult, McpError> {
183 let data = self.fetch_weather(¶ms.location).await?;
184 let days = params.days.unwrap_or(3).min(3) as usize;
185
186 let area = data.nearest_area.first()
187 .map(|a| format!("{}, {}",
188 a.areaName.first().map(|v| v.value.as_str()).unwrap_or("Unknown"),
189 a.region.first().map(|v| v.value.as_str()).unwrap_or("")
190 ))
191 .unwrap_or_else(|| params.location.clone());
192
193 let mut output = format!("Forecast for {} ({} days):\n\n", area, days);
194
195 for day in data.weather.iter().take(days) {
196 output.push_str(&format!(
197 "{}:\n High: {}°F / {}°C | Low: {}°F / {}°C\n",
198 day.date, day.maxtempF, day.maxtempC, day.mintempF, day.mintempC
199 ));
200
201 for hour in day.hourly.iter().step_by(3) {
203 let time_hr = hour.time.parse::<u32>().unwrap_or(0) / 100;
204 let desc = hour.weatherDesc.first()
205 .map(|d| d.value.as_str())
206 .unwrap_or("?");
207 output.push_str(&format!(
208 " {:02}:00 - {}°F, {}, {}% rain\n",
209 time_hr, hour.tempF, desc, hour.chanceofrain
210 ));
211 }
212 output.push('\n');
213 }
214
215 Ok(CallToolResult::success(vec![Content::text(output)]))
216 }
217}
218
219#[rmcp::tool_handler]
220impl ServerHandler for WeatherServer {
221 fn get_info(&self) -> ServerInfo {
222 ServerInfo {
223 protocol_version: ProtocolVersion::V_2024_11_05,
224 capabilities: ServerCapabilities::builder()
225 .enable_tools()
226 .build(),
227 server_info: Implementation::from_build_env(),
228 instructions: Some("Weather information server using wttr.in".into()),
229 }
230 }
231}