Skip to content

Commit 4991352

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

File tree

4 files changed

+282
-0
lines changed

4 files changed

+282
-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: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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, result T) error {
68+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
69+
if err != nil {
70+
return 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 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 fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
86+
}
87+
88+
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
89+
return fmt.Errorf("failed to decode response: %w", err)
90+
}
91+
92+
return nil
93+
}
94+
95+
func formatAlert(alert AlertFeature) string {
96+
props := alert.Properties
97+
event := cmp.Or(props.Event, "Unknown")
98+
areaDesc := cmp.Or(props.AreaDesc, "Unknown")
99+
severity := cmp.Or(props.Severity, "Unknown")
100+
description := cmp.Or(props.Description, "No description available")
101+
instruction := cmp.Or(props.Instruction, "No specific instructions provided")
102+
103+
return fmt.Sprintf(`
104+
Event: %s
105+
Area: %s
106+
Severity: %s
107+
Description: %s
108+
Instructions: %s
109+
`, event, areaDesc, severity, description, instruction)
110+
}
111+
112+
func formatPeriod(period ForecastPeriod) string {
113+
return fmt.Sprintf(`
114+
%s:
115+
Temperature: %d°%s
116+
Wind: %s %s
117+
Forecast: %s
118+
`, period.Name, period.Temperature, period.TemperatureUnit,
119+
period.WindSpeed, period.WindDirection, period.DetailedForecast)
120+
}
121+
122+
func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (
123+
*mcp.CallToolResult, any, error,
124+
) {
125+
// Get points data
126+
pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
127+
var pointsData PointsResponse
128+
if err := makeNWSRequest(ctx, pointsURL, &pointsData); err != nil {
129+
return &mcp.CallToolResult{
130+
Content: []mcp.Content{
131+
&mcp.TextContent{Text: "Unable to fetch forecast data for this location."},
132+
},
133+
}, nil, nil
134+
}
135+
136+
// Get forecast data
137+
forecastURL := pointsData.Properties.Forecast
138+
if forecastURL == "" {
139+
return &mcp.CallToolResult{
140+
Content: []mcp.Content{
141+
&mcp.TextContent{Text: "Unable to fetch forecast URL."},
142+
},
143+
}, nil, nil
144+
}
145+
146+
var forecastData ForecastResponse
147+
if err := makeNWSRequest(ctx, forecastURL, &forecastData); err != nil {
148+
return &mcp.CallToolResult{
149+
Content: []mcp.Content{
150+
&mcp.TextContent{Text: "Unable to fetch detailed forecast."},
151+
},
152+
}, nil, nil
153+
}
154+
155+
// Format the periods
156+
periods := forecastData.Properties.Periods
157+
if len(periods) == 0 {
158+
return &mcp.CallToolResult{
159+
Content: []mcp.Content{
160+
&mcp.TextContent{Text: "No forecast periods available."},
161+
},
162+
}, nil, nil
163+
}
164+
165+
// Show next 5 periods
166+
var forecasts []string
167+
for i := range min(5, len(periods)) {
168+
forecasts = append(forecasts, formatPeriod(periods[i]))
169+
}
170+
171+
result := strings.Join(forecasts, "\n---\n")
172+
173+
return &mcp.CallToolResult{
174+
Content: []mcp.Content{
175+
&mcp.TextContent{Text: result},
176+
},
177+
}, nil, nil
178+
}
179+
180+
func getAlerts(ctx context.Context, req *mcp.CallToolRequest, input AlertsInput) (
181+
*mcp.CallToolResult, any, error,
182+
) {
183+
// Build alerts URL
184+
stateCode := strings.ToUpper(input.State)
185+
alertsURL := fmt.Sprintf("%s/alerts/active/area/%s", NWSAPIBase, stateCode)
186+
187+
var alertsData AlertsResponse
188+
if err := makeNWSRequest(ctx, alertsURL, &alertsData); err != nil {
189+
return &mcp.CallToolResult{
190+
Content: []mcp.Content{
191+
&mcp.TextContent{Text: "Unable to fetch alerts or no alerts found."},
192+
},
193+
}, nil, nil
194+
}
195+
196+
// Check if there are any alerts
197+
if len(alertsData.Features) == 0 {
198+
return &mcp.CallToolResult{
199+
Content: []mcp.Content{
200+
&mcp.TextContent{Text: "No active alerts for this state."},
201+
},
202+
}, nil, nil
203+
}
204+
205+
// Format alerts
206+
var alerts []string
207+
for _, feature := range alertsData.Features {
208+
alerts = append(alerts, formatAlert(feature))
209+
}
210+
211+
result := strings.Join(alerts, "\n---\n")
212+
213+
return &mcp.CallToolResult{
214+
Content: []mcp.Content{
215+
&mcp.TextContent{Text: result},
216+
},
217+
}, nil, nil
218+
}
219+
220+
func main() {
221+
// Create MCP server
222+
server := mcp.NewServer(&mcp.Implementation{
223+
Name: "weather",
224+
Version: "1.0.0",
225+
}, nil)
226+
227+
// Add get_forecast tool
228+
mcp.AddTool(server, &mcp.Tool{
229+
Name: "get_forecast",
230+
Description: "Get weather forecast for a location",
231+
}, getForecast)
232+
233+
// Add get_alerts tool
234+
mcp.AddTool(server, &mcp.Tool{
235+
Name: "get_alerts",
236+
Description: "Get weather alerts for a US state",
237+
}, getAlerts)
238+
239+
// Run server on stdio transport
240+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
241+
log.Fatal(err)
242+
}
243+
}

0 commit comments

Comments
 (0)