Add weather-server-rust
This commit is contained in:
parent
dfc92478e2
commit
4e81e6e470
5 changed files with 2403 additions and 0 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -12,3 +12,7 @@ wheels/
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|
||||||
|
# Rust-generated files
|
||||||
|
debug
|
||||||
|
target
|
||||||
|
|
|
||||||
2171
weather-server-rust/Cargo.lock
generated
Normal file
2171
weather-server-rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
14
weather-server-rust/Cargo.toml
Normal file
14
weather-server-rust/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "weather"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rmcp = { version = "0.3", features = ["server", "macros", "transport-io"] }
|
||||||
|
tokio = { version = "1.46", features = ["full"] }
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
anyhow = "1.0"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] }
|
||||||
3
weather-server-rust/README.md
Normal file
3
weather-server-rust/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# A Simple MCP Weather Server written in Rust
|
||||||
|
|
||||||
|
See the [Quickstart](https://modelcontextprotocol.io/quickstart) tutorial for more information.
|
||||||
211
weather-server-rust/src/main.rs
Normal file
211
weather-server-rust/src/main.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use rmcp::{
|
||||||
|
handler::server::{router::tool::ToolRouter, tool::Parameters},
|
||||||
|
model::*,
|
||||||
|
schemars, tool, tool_handler, tool_router, ServerHandler, ServiceExt,
|
||||||
|
};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
const NWS_API_BASE: &str = "https://api.weather.gov";
|
||||||
|
const USER_AGENT: &str = "weather-app/1.0";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AlertsResponse {
|
||||||
|
features: Vec<AlertFeature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AlertFeature {
|
||||||
|
properties: AlertProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AlertProperties {
|
||||||
|
event: Option<String>,
|
||||||
|
#[serde(rename = "areaDesc")]
|
||||||
|
area_desc: Option<String>,
|
||||||
|
severity: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
instruction: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<ForecastPeriod>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<T: DeserializeOwned>(url: &str) -> Result<T> {
|
||||||
|
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::<T>().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<Weather>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<MCPAlertRequest>,
|
||||||
|
) -> String {
|
||||||
|
let url = format!(
|
||||||
|
"{}/alerts/active/area/{}",
|
||||||
|
NWS_API_BASE,
|
||||||
|
state.to_uppercase()
|
||||||
|
);
|
||||||
|
|
||||||
|
match make_nws_request::<AlertsResponse>(&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::<Vec<_>>()
|
||||||
|
.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<MCPForecastRequest>,
|
||||||
|
) -> String {
|
||||||
|
let points_url = format!("{NWS_API_BASE}/points/{latitude},{longitude}");
|
||||||
|
let points_data = match make_nws_request::<PointsResponse>(&points_url).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => {
|
||||||
|
return "Unable to fetch forecast data for this location.".to_string();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let forecast_url = points_data.properties.forecast;
|
||||||
|
|
||||||
|
let forecast_data = match make_nws_request::<ForecastResponse>(&forecast_url).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => {
|
||||||
|
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::<Vec<String>>()
|
||||||
|
.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(())
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue