diff --git a/weather-server-go/README.md b/weather-server-go/README.md new file mode 100644 index 0000000..aed006a --- /dev/null +++ b/weather-server-go/README.md @@ -0,0 +1,19 @@ +# A Simple MCP Weather Server written in Go + +See the [Quickstart](https://modelcontextprotocol.io/quickstart) tutorial for more information. + +## Building + +```bash +go build -o weather +``` + +## Running + +```bash +./weather +``` + +The server will communicate via stdio and expose two MCP tools: +- `get_forecast` - Get weather forecast for a location (requires latitude and longitude) +- `get_alerts` - Get weather alerts for a US state (requires two-letter state code) diff --git a/weather-server-go/go.mod b/weather-server-go/go.mod new file mode 100644 index 0000000..3d61e3d --- /dev/null +++ b/weather-server-go/go.mod @@ -0,0 +1,10 @@ +module github.com/modelcontextprotocol/quickstart-resources/weather-server-go + +go 1.25.1 + +require github.com/modelcontextprotocol/go-sdk v1.0.0 + +require ( + github.com/google/jsonschema-go v0.3.0 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect +) diff --git a/weather-server-go/go.sum b/weather-server-go/go.sum new file mode 100644 index 0000000..89026b2 --- /dev/null +++ b/weather-server-go/go.sum @@ -0,0 +1,10 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= +github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= diff --git a/weather-server-go/main.go b/weather-server-go/main.go new file mode 100644 index 0000000..61a2704 --- /dev/null +++ b/weather-server-go/main.go @@ -0,0 +1,244 @@ +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.Client{} + 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) + } +}