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) } }