Building an MCP Server: Lessons from Spain's Weather API
For those unfamiliar, MCP is an open protocol that allows AI clients like Claude Desktop or IDE agents to call external tools through a standardized interface. You build a server that exposes typed tools, and the client handles the discovery and invocation. I used Node.js, TypeScript, and the official @modelcontextprotocol/sdk with Zod for input validation.
The "boring" weather API actually provided a great deep dive into a specific architectural pattern: the two-step fetch.
The Two-Step Data Pattern
AEMET doesn't return data immediately. When you request a forecast, the API returns a JSON object containing a URL pointer to the actual data.
GET /opendata/api/prediccion/especifica/municipio/diaria/{ine_code}
Header: api_key: [your_key]The response looks like this:
{
"descripcion": "exito",
"estado": 200,
"datos": "https://opendata.aemet.es/opendata/sh/abc123",
"metadatos": "https://opendata.aemet.es/opendata/sh/def456"
}It's essentially the S3 presigned URL pattern. You hit the index, get a temporary link, and then make a second request to that link to get the payload. To keep my AI workflow clean, I isolated this logic into a single helper function so the tools themselves only see the final, typed data.
async function fetchAemet(path: string): Promise<any> {
// 1. call the endpoint with api_key → { estado, datos, metadatos }
// 2. validate estado
// 3. fetch the datos URL → decode → parse
}Real-world Implementation Traps
While the two-step flow was documented, two other issues caused significant friction during this practical tutorial:
- Status Code Mismatch: The API can return a transport-level
200 OKwhile the JSON body contains"estado": 404or401. If you only check the HTTP response status, your agent will try to process "successful" requests that actually contain error messages. - Character Encoding: This was the biggest headache. AEMET often serves content in
ISO-8859-1(latin1) rather thanUTF-8. A naiveawait response.json()results in mangled Spanish accents (e.g., "Cádiz" becomes "Cdiz"). You have to read the body as a buffer and decode it explicitly to avoid garbage data.
This project was a great way to get hands-on experience with LLM agent infrastructure without overcomplicating the domain logic. For more resources on building these, check out promptcube3.com.