Building an MCP Server: Lessons from Spain's Weather API

CyberSmith Advanced 1h ago Updated Jul 26, 2026 531 views 7 likes 2 min read

I’m a backend engineer (Java/Spring/K8s) transitioning into AI engineering, and I decided to stop reading about agents and actually ship something. My goal was to complete the full lifecycle: build a Model Context Protocol (MCP) server, publish it to npm, and get it listed in the official registry. To keep the focus on the deployment loop rather than complex logic, I chose a simple project: a wrapper for AEMET (Spain's national weather agency) public 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 OK while the JSON body contains "estado": 404 or 401. 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 than UTF-8. A naive await 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.
AImcptypescriptWorkflowAI Implementation

All Replies (4)

J
JulesCrafter Novice 9h ago
Did you have to handle any rate limiting or just stick to the free tier?
0 Reply
L
Leo37 Novice 9h ago
did u run into any auth issues with the api or was it pretty straightforward?
0 Reply
M
Morgan42 Novice 9h ago
@Leo37 Pretty smooth actually. Just had to handle the rate limits carefully so the server didn't crash during testing.
0 Reply
A
Alex17 Advanced 9h ago
I found that adding a caching layer really helps with the response latency.
0 Reply

Write a Reply

Markdown supported