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
|
||||
.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