Skip to content

Commit 80ba22e

Browse files
committed
Add weather-server-go
1 parent e0c5614 commit 80ba22e

File tree

4 files changed

+283
-0
lines changed

4 files changed

+283
-0
lines changed

weather-server-go/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# A Simple MCP Weather Server written in Go
2+
3+
See the [Quickstart](https://modelcontextprotocol.io/quickstart) tutorial for more information.
4+
5+
## Building
6+
7+
```bash
8+
go build -o weather
9+
```
10+
11+
## Running
12+
13+
```bash
14+
./weather
15+
```
16+
17+
The server will communicate via stdio and expose two MCP tools:
18+
- `get_forecast` - Get weather forecast for a location (requires latitude and longitude)
19+
- `get_alerts` - Get weather alerts for a US state (requires two-letter state code)

weather-server-go/go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/modelcontextprotocol/quickstart-resources/weather-server-go
2+
3+
go 1.25.1
4+
5+
require github.com/modelcontextprotocol/go-sdk v1.0.0
6+
7+
require (
8+
github.com/google/jsonschema-go v0.3.0 // indirect
9+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
10+
)

weather-server-go/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3+
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
4+
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
5+
github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
6+
github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
7+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
8+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
9+
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
10+
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=

weather-server-go/main.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package main
2+
3+
import (
4+
"cmp"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"log"
10+
"net/http"
11+
"strings"
12+
13+
"github.com/modelcontextprotocol/go-sdk/mcp"
14+
)
15+
16+
const (
17+
NWSAPIBase = "https://api.weather.gov"
18+
UserAgent = "weather-app/1.0"
19+
)
20+
21+
type ForecastInput struct {
22+
Latitude float64 `json:"latitude" jsonschema:"Latitude of the location"`
23+
Longitude float64 `json:"longitude" jsonschema:"Longitude of the location"`
24+
}
25+
26+
type AlertsInput struct {
27+
State string `json:"state" jsonschema:"Two-letter US state code (e.g. CA, NY)"`
28+
}
29+
30+
type PointsResponse struct {
31+
Properties struct {
32+
Forecast string `json:"forecast"`
33+
} `json:"properties"`
34+
}
35+
36+
type ForecastResponse struct {
37+
Properties struct {
38+
Periods []ForecastPeriod `json:"periods"`
39+
} `json:"properties"`
40+
}
41+
42+
type ForecastPeriod struct {
43+
Name string `json:"name"`
44+
Temperature int `json:"temperature"`
45+
TemperatureUnit string `json:"temperatureUnit"`
46+
WindSpeed string `json:"windSpeed"`
47+
WindDirection string `json:"windDirection"`
48+
DetailedForecast string `json:"detailedForecast"`
49+
}
50+
51+
type AlertsResponse struct {
52+
Features []AlertFeature `json:"features"`
53+
}
54+
55+
type AlertFeature struct {
56+
Properties AlertProperties `json:"properties"`
57+
}
58+
59+
type AlertProperties struct {
60+
Event string `json:"event"`
61+
AreaDesc string `json:"areaDesc"`
62+
Severity string `json:"severity"`
63+
Description string `json:"description"`
64+
Instruction string `json:"instruction"`
65+
}
66+
67+
func makeNWSRequest[T any](ctx context.Context, url string) (*T, error) {
68+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to create request: %w", err)
71+
}
72+
73+
req.Header.Set("User-Agent", UserAgent)
74+
req.Header.Set("Accept", "application/geo+json")
75+
76+
client := &http.Client{}
77+
resp, err := client.Do(req)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to make request to %s: %w", url, err)
80+
}
81+
defer resp.Body.Close()
82+
83+
if resp.StatusCode != http.StatusOK {
84+
body, _ := io.ReadAll(resp.Body)
85+
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
86+
}
87+
88+
var result T
89+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
90+
return nil, fmt.Errorf("failed to decode response: %w", err)
91+
}
92+
93+
return &result, nil
94+
}
95+
96+
func formatAlert(alert AlertFeature) string {
97+
props := alert.Properties
98+
event := cmp.Or(props.Event, "Unknown")
99+
areaDesc := cmp.Or(props.AreaDesc, "Unknown")
100+
severity := cmp.Or(props.Severity, "Unknown")
101+
description := cmp.Or(props.Description, "No description available")
102+
instruction := cmp.Or(props.Instruction, "No specific instructions provided")
103+
104+
return fmt.Sprintf(`
105+
Event: %s
106+
Area: %s
107+
Severity: %s
108+
Description: %s
109+
Instructions: %s
110+
`, event, areaDesc, severity, description, instruction)
111+
}
112+
113+
func formatPeriod(period ForecastPeriod) string {
114+
return fmt.Sprintf(`
115+
%s:
116+
Temperature: %d°%s
117+
Wind: %s %s
118+
Forecast: %s
119+
`, period.Name, period.Temperature, period.TemperatureUnit,
120+
period.WindSpeed, period.WindDirection, period.DetailedForecast)
121+
}
122+
123+
func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
124+
*mcp.CallToolResult, any, error,
125+
) {
126+
// Get points data
127+
pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
128+
pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
129+
if err != nil {
130+
return &mcp.CallToolResult{
131+
Content: []mcp.Content{
132+
&mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
133+
},
134+
}, nil, nil
135+
}
136+
137+
// Get forecast data
138+
forecastURL := pointsData.Properties.Forecast
139+
if forecastURL == "" {
140+
return &mcp.CallToolResult{
141+
Content: []mcp.Content{
142+
&mcp.TextContent{Text: "Unable to fetch forecast URL."},
143+
},
144+
}, nil, nil
145+
}
146+
147+
forecastData, err := makeNWSRequest[ForecastResponse](ctx, forecastURL)
148+
if err != nil {
149+
return &mcp.CallToolResult{
150+
Content: []mcp.Content{
151+
&mcp.TextContent{Text: "Unable to fetch detailed forecast."},
152+
},
153+
}, nil, nil
154+
}
155+
156+
// Format the periods
157+
periods := forecastData.Properties.Periods
158+
if len(periods) == 0 {
159+
return &mcp.CallToolResult{
160+
Content: []mcp.Content{
161+
&mcp.TextContent{Text: "No forecast periods available."},
162+
},
163+
}, nil, nil
164+
}
165+
166+
// Show next 5 periods
167+
var forecasts []string
168+
for i := range min(5, len(periods)) {
169+
forecasts = append(forecasts, formatPeriod(periods[i]))
170+
}
171+
172+
result := strings.Join(forecasts, "\n---\n")
173+
174+
return &mcp.CallToolResult{
175+
Content: []mcp.Content{
176+
&mcp.TextContent{Text: result},
177+
},
178+
}, nil, nil
179+
}
180+
181+
func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
182+
*mcp.CallToolResult, any, error,
183+
) {
184+
// Build alerts URL
185+
stateCode := strings.ToUpper(input.State)
186+
alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
187+
188+
alertsData, err := makeNWSRequest[AlertsResponse](ctx, alertsURL)
189+
if err != nil {
190+
return &mcp.CallToolResult{
191+
Content: []mcp.Content{
192+
&mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
193+
},
194+
}, nil, nil
195+
}
196+
197+
// Check if there are any alerts
198+
if len(alertsData.Features) == 0 {
199+
return &mcp.CallToolResult{
200+
Content: []mcp.Content{
201+
&mcp.TextContent{Text: "No active alerts for this state."},
202+
},
203+
}, nil, nil
204+
}
205+
206+
// Format alerts
207+
var alerts []string
208+
for _, feature := range alertsData.Features {
209+
alerts = append(alerts, formatAlert(feature))
210+
}
211+
212+
result := strings.Join(alerts, "\n---\n")
213+
214+
return &mcp.CallToolResult{
215+
Content: []mcp.Content{
216+
&mcp.TextContent{Text: result},
217+
},
218+
}, nil, nil
219+
}
220+
221+
func main() {
222+
// Create MCP server
223+
server := mcp.NewServer(&mcp.Implementation{
224+
Name: "weather",
225+
Version: "1.0.0",
226+
}, nil)
227+
228+
// Add get_forecast tool
229+
mcp.AddTool(server, &mcp.Tool{
230+
Name: "get_forecast",
231+
Description: "Get weather forecast for a location",
232+
}, getForecast)
233+
234+
// Add get_alerts tool
235+
mcp.AddTool(server, &mcp.Tool{
236+
Name: "get_alerts",
237+
Description: "Get weather alerts for a US state",
238+
}, getAlerts)
239+
240+
// Run server on stdio transport
241+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
242+
log.Fatal(err)
243+
}
244+
}

0 commit comments

Comments
 (0)