Create a tool that queries a real external API—learn how to make HTTP requests inside an MCP Server.
For our weather server, we'll use the wttr.in — a free weather API that doesn't require registration or an authentication key. It returns weather data as plain text or JSON, is extremely simple to use, and is perfect for learning. Just make a GET request, and the data arrives ready to use.
A wttr.in is accessible via a simple URL. To get data in JSON, just add the parameter ?format=j1:
# URL examples:
https://wttr.in/São Paulo?format=j1
https://wttr.in/Tokyo?format=j1
https://wttr.in/London?format=j1
The returned JSON contains temperature, weather conditions, humidity, feels-like temperature, wind speed, and more. Let’s extract the most relevant data to create a useful response.
wttr.in is an open-source project hosted on GitHub. It supports more than 50 languages, accepts city names in any language and also works with geographic coordinates. It’s used by millions of developers and requires no authentication — ideal for rapid prototyping and learning projects like ours.
Before implementing the tool, test the API directly in your browser or terminal. Go to https://wttr.in/São Paulo?format=j1 and inspect the JSON structure. This helps you understand which fields are available and how to extract the data we want. You can also test in the terminal with curl "wttr.in/São Paulo?format=j1".
To make HTTP requests in our MCP server, we need an HTTP library. In Python, the two main options are requests (synchronous) and httpx (with async support). For MCP servers, httpx is the preferred choice because it supports asynchronous calls natively, which fits the MCP architecture better.
Install httpx in your project using the package manager uv:
# Add httpx as a dependency
uv add httpx
With the httpx, we can use async with httpx.AsyncClient() to make asynchronous HTTP requests inside MCP tools. The tool function will be declared as async def, allowing the server to remain responsive while waiting for the external API response.
Now let’s implement the weather lookup tool. It will receive a city name as a string, make an HTTP request to wttr.in, parse the response JSON, and extract the most relevant information: temperature, weather conditions, humidity, and feels-like temperature.
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("Weather")
@mcp.tool()
async def consultar_clima(city: str) -> str:
"""Gets the current weather for a city."""
async with httpx.AsyncClient() the client:
url = f"https://wttr.in/{cidade}?format=j1"
response = await client.get(url, timeout=10.0)
response.raise_for_status()
data = response.json()
current = dados["current_condition"][0]
temp = current["temp_C"]
feeling = current["FeelsLikeC"]
humidity = current["humidity"]
description = current["weatherDesc"][0]["value"]
return (
f"Weather in {cidade}:\n"
f"Temperature: {temp}°C\n"
f"Feels like: {sensacao}°C\n"
f"Humidity: {umidade}%\n"
f"Condition: {descricao}"
)
Notice that we use async def e await in the tool. FastMCP supports async tools natively—just declare the function as async. O timeout=10.0 ensures the request doesn’t hang forever if the API doesn’t respond. Always set timeouts for external HTTP requests.
How you return tool data directly affects the quality of the response the model gives the user. Returning a readable, formatted string is much better than returning the API's raw JSON. The model receives this string and presents it to the user in a natural, conversational way.
The tool response should be a structured but readable string. The model reads this string and turns it into natural language for the user. Compare the two approaches:
{"temp_C": "25", "humidity": "60", "weatherDesc": [{"value": "Clear"}], ...}
Weather in São Paulo:
Temperature: 25°C
Humidity: 60%
Condition: Clear
When your tool depends on an external API, many things can go wrong: the city may not exist, the internet may go down, the API may be unavailable, or the request may take too long. Each of these scenarios needs to be handled individually so the server keeps working and the model receives a useful error message.
@mcp.tool()
async def consultar_clima(city: str) -> str:
"""Gets the current weather for a city."""
try:
async with httpx.AsyncClient() the client:
url = f"https://wttr.in/{cidade}?format=j1"
response = await client.get(url, timeout=10.0)
response.raise_for_status()
data = response.json()
# ... extract and return data
except httpx.HTTPStatusError:
return f"Error: city '{cidade}' not found."
except httpx.ConnectError:
return "Error: No internet connection."
except httpx.TimeoutException:
return "Error: The API took too long to respond."
except Exception the e:
return f"Unexpected error: {str(e)}"
Never trust external APIs! Any HTTP request can fail. A tool that works perfectly in your local test can break in production because the API went down, changed its response format, or hit the rate limit. Error handling is not optional — it's required for any tool that makes external requests.
The API returned an error code. City not found (404) or an internal API error (500). Return a clear message indicating the problem.
Couldn't connect to the API — this usually means there's no internet connection or DNS couldn't be resolved. Tell the user that the connection failed.
The API took longer than the defined timeout (10 seconds). The server may be overloaded. Let the user know that the API didn't respond in time.
Any unexpected error — malformed JSON, missing field, etc. The generic except at the end ensures no error goes unhandled.
With the tool implemented and error handling in place, configure the server in Claude Desktop and test with different cities and scenarios. The goal is to verify that the tool returns correct data and that errors are handled gracefully.
Test with natural questions to verify that Claude uses the tool correctly:
Observe how Claude interprets the data returned by the tool. It doesn't just repeat the numbers—it puts them in context. If the temperature is 35°C, it can say "it's quite hot." If the humidity is 90%, it can warn about the muggy conditions. The clearer the tool's response, the better the model's interpretation.
This weather server is a classic example of "API bridge" — a very common pattern in MCP servers. The server acts as a bridge between the model and an external API, translating the model's request into an HTTP call and the HTTP response into text for the model. This same pattern can be applied to any API: weather forecasts, stock quotes, news, financial data, service status, and much more.
Next Module: 3.6 — Adding Resources