Build a Weather Service Servlet with Go
If you want to start with some working servlets, see our official servlet repo: dylibso/mcp.run-servlets
Welcome! In this tutorial, you'll build your first servlet using Go. We'll create a weather service that AI models can use to fetch real-time weather data. By the end, you'll have a working servlet that can tell you the weather anywhere in the world!
What Are We Building?
A servlet is a tool that AI models can use to interact with external services. Our weather servlet will:
- Connect to the WeatherAPI service
- Fetch current weather conditions for any location
- Format the data in a user-friendly way
Here's what it looks like in action:
Pre-requisites
Before starting, ensure you have the following:
- Go
- TinyGo
- A GitHub Account for mcp.run authentication
- A free API key from WeatherAPI
- The XTP CLI
Create an account on mcp.run
1. Create an account on mcp.run:
Visit https://mcp.run and click "Sign Up".
2. Register your first servlet:
Click "Publish" and go through the Registration flow. You can make your servlet "Private" while it is under development.
Use weather-service
as the name, and you can leave the rest of the fields
blank. We'll fill these out later!
Once you're done, click "Register Servlet" and you'll see some instructions to follow on your servlet page. Be sure to follow those closely.
Project Setup
First, let's create our project:
- Use the XTP CLI to genearate a new Servlet
xtp plugin init \
--extension-point ext_01je4jj1tteaktf0zd0anm8854 \
--feature stub-with-code-samples \
--path weather-service \
--name weather-service
The --name parameter (weather-service) MUST match what you'll use when publishing to mcp.run later.
-
Choose "Go" when prompted
-
Open the project in VS Code
Open main.go
, and change it to:
package main
import (
"fmt"
)
// Main call handler for the servlet
func Call(input CallToolRequest) (CallToolResult, error) {
if err := loadConfig(); err != nil {
return CallToolResult{}, err
}
args := input.Params.Arguments.(map[string]interface{})
switch input.Params.Name {
case "get-weather":
return handleGetWeather(args)
default:
return CallToolResult{}, fmt.Errorf("unknown tool: %s", input.Params.Name)
}
}
// Handler for the get-weather tool
func handleGetWeather(args map[string]interface{}) (CallToolResult, error) {
location, ok := args["location"].(string)
if !ok {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some("location parameter is required"),
}},
}, nil
}
weather, err := getCurrentWeather(location)
if err != nil {
return CallToolResult{
IsError: some(true),
Content: []Content{{
Type: ContentTypeText,
Text: some(err.Error()),
}},
}, nil
}
response := fmt.Sprintf(
"Current weather in %s, %s:\nTemperature: %.1f°C (%.1f°F)\nCondition: %s\nHumidity: %d%%",
weather.Location.Name,
weather.Location.Country,
weather.Current.TempC,
weather.Current.TempF,
weather.Current.Condition.Text,
weather.Current.Humidity,
)
return CallToolResult{
Content: []Content{{
Type: ContentTypeText,
Text: some(response),
}},
}, nil
}
// Tool description for mcp.run
func Describe() (ListToolsResult, error) {
return ListToolsResult{
Tools: []ToolDescription{
{
Name: "get-weather",
Description: "Get current weather conditions for any location",
InputSchema: map[string]interface{}{
"type": "object",
"required": []string{"location"},
"properties": map[string]interface{}{
"location": map[string]interface{}{
"type": "string",
"description": "Location name, city, zip/postal code, or coordinates",
},
},
},
},
},
}, nil
}
This file defines how AI models interact with our weather tool. A servlet can
have multiple tools, but we're keeping it simple with just one: get-weather
.
There are two main functions:
Describe()
: Tells AI models what our Servlet can doCall()
: Handles incoming requests from AI models
And then create a new file called weatherapi.go
:
package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/extism/go-pdk"
)
// Configuration handling
var config struct {
apiKey string
}
// Helper for optional values
func some[T any](t T) *T {
return &t
}
// Load WeatherAPI configuration
func loadConfig() error {
if config.apiKey != "" {
return nil
}
// Servlets are WebAssembly modules, which means we don't have access
// to platform-specific functionality like OS environment variables.
// Instead, we have helpers like this to get configuration set by the
// installer of the servlet on mcp.run
key, ok := pdk.GetConfig("WEATHER_API_KEY")
if !ok {
return errors.New("WeatherAPI key not configured")
}
config.apiKey = key
return nil
}
// WeatherAPI response structure
type WeatherResponse struct {
Location struct {
Name string `json:"name"`
Country string `json:"country"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
} `json:"location"`
Current struct {
TempC float64 `json:"temp_c"`
TempF float64 `json:"temp_f"`
Condition struct {
Text string `json:"text"`
} `json:"condition"`
Humidity int `json:"humidity"`
} `json:"current"`
}
// Make API request to get current weather
func getCurrentWeather(location string) (*WeatherResponse, error) {
url := fmt.Sprintf(
"http://api.weatherapi.com/v1/current.json?key=%s&q=%s&aqi=no",
config.apiKey,
location,
)
// Servlets are WebAssembly modules, which means we don't have access
// to platform-specific functionality like a full networking stack.
// Instead, we have helpers like this to make HTTP requests to hostnames
// which are allow-listed by the installer of the servlet on mcp.run
req := pdk.NewHTTPRequest(pdk.MethodGet, url)
resp := req.Send()
if resp.Status() != 200 {
return nil, fmt.Errorf(
"weather API error: %d - %s",
resp.Status(),
string(resp.Body()),
)
}
var weather WeatherResponse
if err := json.Unmarshal(resp.Body(), &weather); err != nil {
return nil, fmt.Errorf("failed to parse weather data: %v", err)
}
return &weather, nil
}
Build and test your Servlet
You can use the XTP CLI to test your Servlet locally:
# Build your servlet
xtp plugin build
# Set your API key as an environment variable
export WEATHER_API_KEY="your-key-here"
xtp plugin call dist/plugin.wasm call --input='{
"params": {
"name": "get-weather",
"arguments": {
"location": "London"
}
}
}' \
--wasi \
--allow-host api.weatherapi.com \
--config "WEATHER_API_KEY=$WEATHER_API_KEY"
You should see something like:
{
"content": [{
"text": "Current weather in London, United Kingdom:\nTemperature: 9.2°C (48.6°F)\nCondition: Mist\nHumidity: 93%",
"type": "text"
}]
}
Publish your Servlet on mcp.run
- Register your servlet at https://mcp.run/publish
- Name:
weather-service
(MUST match the name from earlier!) - Description: "Get real-time weather data for any location"
- Configuration:
- Add
WEATHER_API_KEY
variable for WeatherAPI key.It's also a nice touch to include an example value and some instructions or a URL in the description about where to find their API key, so it's easier for installers to get started.
- Add
- Domain permissions:
- Add
api.weatherapi.com
- Add
- Publish:
xtp plugin push
That's it! Your servlet is now available for AI models to use.
Using Your Servlet
- Click on the
Install
button on your Servlet page - Provide your WeatherAPI key
- Now the AI model can use it you have installed the
mcpx
client, see this page for more details.
Support
If you get stuck and need some help, please reach out! Visit our support page to learn how best to get in touch.