Merge branch 'main' into main
This commit is contained in:
commit
1c738b3992
6 changed files with 2471 additions and 1 deletions
72
.github/workflows/claude.yml
vendored
Normal file
72
.github/workflows/claude.yml
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Check repository permissions
|
||||
run: |
|
||||
# Check if user has at least triage rights on the repository
|
||||
PERMISSION=$(gh api "repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission" -q '.permission' 2>/dev/null)
|
||||
|
||||
case "$PERMISSION" in
|
||||
admin|maintain|push|triage)
|
||||
echo "✓ User has $PERMISSION rights on the repository"
|
||||
exit 0
|
||||
;;
|
||||
pull)
|
||||
echo "✗ User only has pull (read-only) rights"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "✗ User does not have sufficient permissions"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -14,4 +14,8 @@ wheels/
|
|||
.venv
|
||||
|
||||
# Environment variables file
|
||||
.env
|
||||
.env
|
||||
|
||||
# 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.
|
||||
206
weather-server-rust/src/main.rs
Normal file
206
weather-server-rust/src/main.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
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<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 Ok(points_data) = make_nws_request::<PointsResponse>(&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::<ForecastResponse>(&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::<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