244 lines
6 KiB
Go
244 lines
6 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
const (
|
|
NWSAPIBase = "https://api.weather.gov"
|
|
UserAgent = "weather-app/1.0"
|
|
)
|
|
|
|
type ForecastInput struct {
|
|
Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
|
|
Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
|
|
}
|
|
|
|
type AlertsInput struct {
|
|
State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
|
|
}
|
|
|
|
type PointsResponse struct {
|
|
Properties struct {
|
|
Forecast string `json:"forecast"`
|
|
} `json:"properties"`
|
|
}
|
|
|
|
type ForecastResponse struct {
|
|
Properties struct {
|
|
Periods []ForecastPeriod `json:"periods"`
|
|
} `json:"properties"`
|
|
}
|
|
|
|
type ForecastPeriod struct {
|
|
Name string `json:"name"`
|
|
Temperature int `json:"temperature"`
|
|
TemperatureUnit string `json:"temperatureUnit"`
|
|
WindSpeed string `json:"windSpeed"`
|
|
WindDirection string `json:"windDirection"`
|
|
DetailedForecast string `json:"detailedForecast"`
|
|
}
|
|
|
|
type AlertsResponse struct {
|
|
Features []AlertFeature `json:"features"`
|
|
}
|
|
|
|
type AlertFeature struct {
|
|
Properties AlertProperties `json:"properties"`
|
|
}
|
|
|
|
type AlertProperties struct {
|
|
Event string `json:"event"`
|
|
AreaDesc string `json:"areaDesc"`
|
|
Severity string `json:"severity"`
|
|
Description string `json:"description"`
|
|
Instruction string `json:"instruction"`
|
|
}
|
|
|
|
func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
req.Header.Set("Accept", "application/geo+json")
|
|
|
|
client := http.DefaultClient
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result T
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func formatAlert(alert AlertFeature) string {
|
|
props := alert.Properties
|
|
event := cmp.Or(props.Event, "Unknown")
|
|
areaDesc := cmp.Or(props.AreaDesc, "Unknown")
|
|
severity := cmp.Or(props.Severity, "Unknown")
|
|
description := cmp.Or(props.Description, "No description available")
|
|
instruction := cmp.Or(props.Instruction, "No specific instructions provided")
|
|
|
|
return fmt.Sprintf(`
|
|
Event: %s
|
|
Area: %s
|
|
Severity: %s
|
|
Description: %s
|
|
Instructions: %s
|
|
`, event, areaDesc, severity, description, instruction)
|
|
}
|
|
|
|
func formatPeriod(period ForecastPeriod) string {
|
|
return fmt.Sprintf(`
|
|
%s:
|
|
Temperature: %d°%s
|
|
Wind: %s %s
|
|
Forecast: %s
|
|
`, period.Name, period.Temperature, period.TemperatureUnit,
|
|
period.WindSpeed, period.WindDirection, period.DetailedForecast)
|
|
}
|
|
|
|
func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
|
|
*mcp.CallToolResult, any, error,
|
|
) {
|
|
// Get points data
|
|
pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
|
|
pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
|
|
if err != nil {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
// Get forecast data
|
|
forecastURL := pointsData.Properties.Forecast
|
|
if forecastURL == "" {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "Unable to fetch forecast URL."},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
|
|
if err != nil {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "Unable to fetch detailed forecast."},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
// Format the periods
|
|
periods := forecastData.Properties.Periods
|
|
if len(periods) == 0 {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "No forecast periods available."},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
// Show next 5 periods
|
|
var forecasts []string
|
|
for i := range min(5, len(periods)) {
|
|
forecasts = append(forecasts, formatPeriod(periods[i]))
|
|
}
|
|
|
|
result := strings.Join(forecasts, "\n---\n")
|
|
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: result},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
|
|
*mcp.CallToolResult, any, error,
|
|
) {
|
|
// Build alerts URL
|
|
stateCode := strings.ToUpper(input.State)
|
|
alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
|
|
|
|
alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
|
|
if err != nil {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
// Check if there are any alerts
|
|
if len(alertsData.Features) == 0 {
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: "No active alerts for this state."},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
// Format alerts
|
|
var alerts []string
|
|
for _, feature := range alertsData.Features {
|
|
alerts = append(alerts, formatAlert(feature))
|
|
}
|
|
|
|
result := strings.Join(alerts, "\n---\n")
|
|
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: result},
|
|
},
|
|
}, nil, nil
|
|
}
|
|
|
|
func main() {
|
|
// Create MCP server
|
|
server := mcp.NewServer(&mcp.Implementation{
|
|
Name: "weather",
|
|
Version: "1.0.0",
|
|
}, nil)
|
|
|
|
// Add get_forecast tool
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "get_forecast",
|
|
Description: "Get weather forecast for a location",
|
|
}, getForecast)
|
|
|
|
// Add get_alerts tool
|
|
mcp.AddTool(server, &mcp.Tool{
|
|
Name: "get_alerts",
|
|
Description: "Get weather alerts for a US state",
|
|
}, getAlerts)
|
|
|
|
// Run server on stdio transport
|
|
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|