diff --git a/mcp-client-python/uv.lock b/mcp-client-python/uv.lock index 56ba4f7..d0281eb 100644 --- a/mcp-client-python/uv.lock +++ b/mcp-client-python/uv.lock @@ -671,11 +671,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] diff --git a/mcp-client-typescript/package-lock.json b/mcp-client-typescript/package-lock.json index ea0688a..aed41b0 100644 --- a/mcp-client-typescript/package-lock.json +++ b/mcp-client-typescript/package-lock.json @@ -806,9 +806,9 @@ } }, "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", "peer": true, "engines": { 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..4ec28f3 --- /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.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) + } +} diff --git a/weather-server-python/uv.lock b/weather-server-python/uv.lock index 5ca2381..781a352 100644 --- a/weather-server-python/uv.lock +++ b/weather-server-python/uv.lock @@ -548,11 +548,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] diff --git a/weather-server-typescript/package-lock.json b/weather-server-typescript/package-lock.json index a5c592c..609c1f0 100644 --- a/weather-server-typescript/package-lock.json +++ b/weather-server-typescript/package-lock.json @@ -544,9 +544,9 @@ } }, "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", "peer": true, "engines": {