Go is a good fit for MCP servers that need to be small, fast, and easy to deploy as a single binary. Instead of shipping a Node project or a Python environment, you can compile one executable and point an MCP host at it.
This guide walks through a simple weather server in Go. It exposes two tools, get_forecast and get_alerts, then runs over stdio so Claude Desktop or another MCP client can launch it locally.
Prerequisites
You need Go 1.24 or later and a client that can run local MCP servers. Claude Desktop is the easiest host to test against.
Check your Go version:
go version
Create the project:
mkdir weather
cd weather
go mod init weather
go get github.com/modelcontextprotocol/go-sdk/mcp
touch main.go
1. Define the Server Shape
Open main.go and start with the imports, constants, and response structs. The National Weather Service API returns nested JSON, so we define only the fields the tools need.
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, such as CA or NY"`
}
The input structs matter because MCP clients read the generated schema when deciding how to call a tool. Clear field names and schema descriptions help the model pass the right arguments.
2. Add API Helpers
A tool should return useful text, not raw API noise. Add a small generic request helper and formatting functions.
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, err
}
req.Header.Set("User-Agent", UserAgent)
req.Header.Set("Accept", "application/geo+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
}
var result T
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
For production tools, keep returned text short and structured. Agents do better with labeled fields than with long blobs.
3. Register Tools
Each MCP tool is a Go function with typed input. The SDK wraps the function as a callable tool and returns content to the client.
func getForecast(ctx context.Context, req *mcp.CallToolRequest, input ForecastInput) (*mcp.CallToolResult, any, error) {
pointsURL := fmt.Sprintf("%s/points/%f,%f", NWSAPIBase, input.Latitude, input.Longitude)
pointsData, err := makeNWSRequest[PointsResponse](ctx, pointsURL)
if err != nil || pointsData.Properties.Forecast == "" {
return textResult("Unable to fetch forecast data for this location."), nil, nil
}
forecastData, err := makeNWSRequest[ForecastResponse](ctx, pointsData.Properties.Forecast)
if err != nil || len(forecastData.Properties.Periods) == 0 {
return textResult("Unable to fetch detailed forecast."), nil, nil
}
var parts []string
for i := range min(5, len(forecastData.Properties.Periods)) {
p := forecastData.Properties.Periods[i]
parts = append(parts, fmt.Sprintf("%s: %d°%s, %s %s. %s", p.Name, p.Temperature, p.TemperatureUnit, p.WindSpeed, p.WindDirection, p.DetailedForecast))
}
return textResult(strings.Join(parts, "\n")), nil, nil
}
In the full file, define PointsResponse, ForecastResponse, and AlertsResponse structs that mirror the API fields you read. The alerts tool follows the same pattern: uppercase the state code, call /alerts/active/area/{state}, format each alert, and return a joined text result.
A helper keeps the return shape clean:
func textResult(text string) *mcp.CallToolResult {
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: text}}}
}
4. Run on stdio
Now create the MCP server, attach both tools, and run it on stdio transport.
func main() {
server := mcp.NewServer(&mcp.Implementation{
Name: "weather",
Version: "1.0.0",
}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "get_forecast", Description: "Get weather forecast for a location"}, getForecast)
mcp.AddTool(server, &mcp.Tool{Name: "get_alerts", Description: "Get weather alerts for a US state"}, getAlerts)
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
Stdio is sensitive: do not write debug logs to stdout. Use log.Println, fmt.Fprintln(os.Stderr, ...), or a logger that writes to stderr. Anything printed to stdout can corrupt the JSON-RPC stream between the host and your server.
Build the server:
go build -o weather .
5. Connect Claude Desktop
Open Claude Desktop’s MCP config file. On macOS, it lives here:
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
Add the compiled binary using an absolute path:
{
"mcpServers": {
"weather": {
"command": "/ABSOLUTE/PATH/TO/weather/weather"
}
}
}
Quit and reopen Claude Desktop. Ask for the weather in Sacramento or active weather alerts in Texas. If the server appears but calls fail, check Claude’s MCP logs in ~/Library/Logs/Claude/ and run the binary directly from a terminal to catch startup errors.
FAQ
Why build an MCP server in Go?
Go produces a single binary, starts quickly, and has strong standard-library support for HTTP clients, JSON, and concurrency. That makes it a strong choice for local tools and deployable internal agent services.
Should I use stdio or HTTP?
Use stdio for local desktop tools launched by a host. Use HTTP when the server needs to run remotely, serve multiple clients, or sit behind normal service infrastructure.
Can the same server expose resources and prompts?
Yes. Tools are only one MCP capability. Once the basic server works, you can add resources for readable context and prompts for reusable workflows. Start with one or two tools first, then expand after the host integration is stable.