Voltar ao início

Criando um Servidor MCP em Python para Previsão do Tempo

Este tutorial irá guiá-lo na construção de um servidor MCP de exemplo — um servidor de previsão do tempo — e na conexão com um host compatível (como o Claude for Desktop). O passo a passo começa com uma configuração básica e evolui para casos de uso mais avançados, sempre explicando o porquê de cada etapa.

O que você vai construir

Neste tutorial, você irá criar um servidor MCP de exemplo para previsão do tempo, que disponibiliza quatro ferramentas principais:

  • get_current_weather_city: retorna as condições meteorológicas atuais para uma cidade brasileira.
  • get_forecast_city: retorna a previsão do tempo para uma cidade brasileira.
  • get_weather_coordinates: retorna as condições meteorológicas para coordenadas específicas (latitude/longitude).
  • get_weather_state_capital: retorna as condições meteorológicas da capital de um estado brasileiro.

O servidor será utilizado por um host (como o Claude for Desktop), que se conecta a ele via protocolo MCP. Assim, LLMs poderão consultar informações meteorológicas em tempo real para diferentes cidades e estados do Brasil.


Conceitos principais do MCP

Servidores MCP podem fornecer três tipos de funcionalidades:

  1. Resources: dados semelhantes a arquivos (respostas de API, conteúdo de arquivos).
  2. Tools: funções que LLMs podem chamar (após permissão do usuário).
  3. Prompts: templates pré-definidos para guiar o LLM.

Neste tutorial, o foco será nas tools — funções que podem ser chamadas por modelos de linguagem para obter informações sob demanda.


Pré-requisitos

Antes de começar, certifique-se de atender aos seguintes requisitos:

  • Ter o Python 3.10 ou superior instalado.
  • Ter familiaridade básica com LLMs (como o Claude).
  • Possuir uma conta gratuita no OpenWeatherMap para obter sua chave de API (necessária para acessar os dados climáticos).
  • Ter o Claude for Desktop instalado em sua máquina (necessário para testar a integração MCP).

Preparando o ambiente

A seguir, você irá preparar o ambiente de desenvolvimento. Cada etapa é explicada para que você entenda o propósito de cada comando.

  1. Instale a ferramenta de inicialização uv

O uv é um gerenciador de ambientes e dependências rápido para Python, que facilita a criação e o gerenciamento do seu projeto.

curl -LsSf https://astral.sh/uv/install.sh | sh
  1. Crie um novo projeto e acesse a pasta

Este comando inicializa a estrutura do projeto e entra na pasta criada.

uv init weather
cd weather
  1. Crie e ative o ambiente virtual

O ambiente virtual isola as dependências do projeto, evitando conflitos com outros projetos Python.

uv venv
source .venv/bin/activate
  1. Instale as dependências necessárias

Aqui você instala o MCP SDK, o cliente HTTP assíncrono httpx e o dotenv para ler variáveis de ambiente.

uv add "mcp[cli]" httpx dotenv
  1. Crie o arquivo principal do servidor

Este será o arquivo onde você implementará o servidor MCP e as ferramentas de previsão do tempo.

touch weather.py
  1. Crie o arquivo .env com sua chave da API

O arquivo .env armazena sua chave da API do OpenWeatherMap de forma segura e fora do código-fonte.

OPENWEATHER_API_KEY=SUA_CHAVE_AQUI

Substitua SUA_CHAVE_AQUI pela chave obtida no site do OpenWeatherMap.


Definindo ferramentas

A partir deste ponto, você irá construir o código do servidor MCP, sempre com explicações antes de cada bloco para garantir o entendimento do que está sendo feito.

1. Importações e configuração inicial

Aqui você prepara o ambiente do seu projeto: importa as bibliotecas essenciais, configura o carregamento das variáveis do .env e inicializa o servidor MCP. Isso garante que seu código estará pronto para lidar com requisições HTTP, acessar a chave da API e expor as ferramentas para o protocolo MCP.

# Importa tipos para tipagem estática e módulos de utilidades
from typing import Any, Optional
import httpx  # Cliente HTTP assíncrono
from mcp.server.fastmcp import FastMCP  # Framework MCP
import os
from dotenv import load_dotenv  # Para carregar variáveis de ambiente do .env

# Inicializa o servidor FastMCP com o nome do serviço
mcp = FastMCP("weather-brazil")

# URLs base das APIs utilizadas e User-Agent customizado
INMET_API_BASE = "https://apitempo.inmet.gov.br"
OPENWEATHER_API_BASE = "https://api.openweathermap.org/data/2.5"
USER_AGENT = "weather-brazil-app/1.0"

# Carrega variáveis de ambiente do arquivo .env (ex: API KEY)
load_dotenv()
OPENWEATHER_API_KEY = os.getenv("OPENWEATHER_API_KEY")

2. Função utilitária para requisições HTTP

Agora vamos criar uma função que faz requisições HTTP de forma segura e assíncrona. Assim, você pode buscar dados de APIs externas (como a do OpenWeatherMap) sem travar o servidor e com tratamento de erros.

# Cabeçalho padrão para requisições HTTP
default_headers = {"User-Agent": USER_AGENT}

async def make_request(
    url: str, headers: Optional[dict] = None
) -> Optional[dict[str, Any]]:
    """
    Faz requisições HTTP GET de forma assíncrona, com tratamento de erros.
    Retorna o JSON da resposta ou None em caso de erro.
    """
    if headers is None:
        headers = default_headers

    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()  # Lança exceção se status não for 2xx
            return response.json()
        except httpx.HTTPStatusError as e:
            print(f"HTTP Error {e.response.status_code}: {e.response.text}")
            return None
        except httpx.TimeoutException:
            print("Request timeout")
            return None
        except Exception as e:
            print(f"Unexpected error: {str(e)}")
            return None

3. Funções de formatação dos dados

Os dados das APIs geralmente vêm em formato bruto. Aqui, você aprende a transformar essas informações em textos claros e bonitos para mostrar ao usuário, tanto para o clima atual quanto para a previsão.

def format_current_weather(data: Optional[dict]) -> str:
    """
    Formata os dados meteorológicos atuais para exibição amigável.
    Recebe o dicionário retornado pela API e devolve uma string formatada.
    """
    if not data:
        return "Não foi possível obter dados meteorológicos."

    weather = data.get("weather", [{}])[0]  # Descrição do clima
    main = data.get("main", {})  # Temperatura, umidade, pressão
    wind = data.get("wind", {})  # Velocidade do vento

    return f"""
Condições Atuais - {data.get("name", "Localização desconhecida")}

Temperatura: {main.get("temp", "N/A")}°C (Sensação térmica: {main.get("feels_like", "N/A")}°C)
Condição: {weather.get("description", "N/A").title()}
Umidade: {main.get("humidity", "N/A")}%
Pressão: {main.get("pressure", "N/A")} hPa
Vento: {wind.get("speed", "N/A")} m/s
Visibilidade: {data.get("visibility", "N/A") / 1000 if data.get("visibility") else "N/A"} km
"""


def format_forecast(data: Optional[dict]) -> str:
    """
    Formata a previsão meteorológica para exibição amigável.
    Mostra os próximos 5 períodos de previsão (3 em 3 horas).
    """
    if not data or "list" not in data:
        return "Não foi possível obter previsão meteorológica."

    forecasts = []
    city_name = data.get("city", {}).get("name", "Localização desconhecida")
    forecasts.append(f"Previsão para {city_name}\n")

    # Itera sobre os próximos 5 períodos
    for item in data["list"][:5]:
        dt_txt = item.get("dt_txt", "")  # Data/hora
        weather = item.get("weather", [{}])[0]
        main = item.get("main", {})
        wind = item.get("wind", {})

        forecast = f"""
{dt_txt}
Temperatura: {main.get("temp", "N/A")}°C (Min: {main.get("temp_min", "N/A")}°C, Max: {main.get("temp_max", "N/A")}°C)
Condição: {weather.get("description", "N/A").title()}
Umidade: {main.get("humidity", "N/A")}%
Vento: {wind.get("speed", "N/A")} m/s
Chuva: {item.get("pop", 0) * 100:.0f}% de probabilidade
"""
        forecasts.append(forecast)

    return "\n---\n".join(forecasts)

4. Definição dos tools MCP

Chegou a hora de criar as ferramentas (tools) que o LLM poderá chamar! Cada função aqui representa um comando que o usuário pode acionar para obter informações meteorológicas de diferentes formas: por cidade, previsão, coordenadas ou capital do estado.

@mcp.tool()
async def get_current_weather_city(city: str, state: str = "") -> str:
    """
    Obter condições meteorológicas atuais para uma cidade brasileira.
    Args:
        city: Nome da cidade (ex: São Paulo, Rio de Janeiro)
        state: Estado opcional (ex: SP, RJ) - ajuda na precisão
    """
    # Monta a query da cidade (ex: "Recife,PE,BR")
    query = f"{city}"
    if state:
        query += f",{state}"
    query += ",BR"  # Brasil

    url = f"{OPENWEATHER_API_BASE}/weather"
    params = {
        "q": query,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric",
        "lang": "pt_br",
    }
    # Constrói URL com parâmetros (ex: ...?q=Recife,PE,BR&appid=...)
    param_str = "&".join([f"{k}={v}" for k, v in params.items()])
    full_url = f"{url}?{param_str}"

    data = await make_request(full_url)
    return format_current_weather(data)


@mcp.tool()
async def get_forecast_city(city: str, state: str = "") -> str:
    """
    Obter previsão meteorológica para uma cidade brasileira.
    Args:
        city: Nome da cidade (ex: São Paulo, Rio de Janeiro)
        state: Estado opcional (ex: SP, RJ) - ajuda na precisão
    """
    # Monta a query da cidade (ex: "Porto Alegre,RS,BR")
    query = f"{city}"
    if state:
        query += f",{state}"
    query += ",BR"  # Brasil

    url = f"{OPENWEATHER_API_BASE}/forecast"
    params = {
        "q": query,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric",
        "lang": "pt_br",
    }
    # Constrói URL com parâmetros
    param_str = "&".join([f"{k}={v}" for k, v in params.items()])
    full_url = f"{url}?{param_str}"

    data = await make_request(full_url)
    return format_forecast(data)


@mcp.tool()
async def get_weather_coordinates(latitude: float, longitude: float) -> str:
    """
    Obter condições meteorológicas atuais para coordenadas específicas.
    Args:
        latitude: Latitude da localização (ex: -23.55)
        longitude: Longitude da localização (ex: -46.63)
    """
    url = f"{OPENWEATHER_API_BASE}/weather"
    params = {
        "lat": latitude,
        "lon": longitude,
        "appid": OPENWEATHER_API_KEY,
        "units": "metric",
        "lang": "pt_br",
    }
    # Constrói URL com parâmetros
    param_str = "&".join([f"{k}={v}" for k, v in params.items()])
    full_url = f"{url}?{param_str}"

    data = await make_request(full_url)
    return format_current_weather(data)

5. Dicionário de capitais e tool auxiliar

Para facilitar a vida do usuário, criamos um dicionário com todas as capitais do Brasil. Assim, basta informar o nome ou sigla do estado para receber o clima da capital correspondente, sem precisar lembrar o nome exato da cidade.

# Dicionário de capitais brasileiras para facilitar consultas por estado
CAPITAIS_BRASIL = {
    "acre": ("Rio Branco", "AC"),
    "alagoas": ("Maceió", "AL"),
    "amapá": ("Macapá", "AP"),
    "amazonas": ("Manaus", "AM"),
    "bahia": ("Salvador", "BA"),
    "ceará": ("Fortaleza", "CE"),
    "distrito federal": ("Brasília", "DF"),
    "espírito santo": ("Vitória", "ES"),
    "goiás": ("Goiânia", "GO"),
    "maranhão": ("São Luís", "MA"),
    "mato grosso": ("Cuiabá", "MT"),
    "mato grosso do sul": ("Campo Grande", "MS"),
    "minas gerais": ("Belo Horizonte", "MG"),
    "pará": ("Belém", "PA"),
    "paraíba": ("João Pessoa", "PB"),
    "paraná": ("Curitiba", "PR"),
    "pernambuco": ("Recife", "PE"),
    "piauí": ("Teresina", "PI"),
    "rio de janeiro": ("Rio de Janeiro", "RJ"),
    "rio grande do norte": ("Natal", "RN"),
    "rio grande do sul": ("Porto Alegre", "RS"),
    "rondônia": ("Porto Velho", "RO"),
    "roraima": ("Boa Vista", "RR"),
    "santa catarina": ("Florianópolis", "SC"),
    "são paulo": ("São Paulo", "SP"),
    "sergipe": ("Aracaju", "SE"),
    "tocantins": ("Palmas", "TO"),
}

@mcp.tool()
async def get_weather_state_capital(state: str) -> str:
    """
    Obter condições meteorológicas da capital de um estado brasileiro.
    Args:
        state: Nome do estado ou sigla (ex: São Paulo, SP, Minas Gerais, MG)
    """
    state_lower = state.lower()
    # Procura por nome completo ou sigla (case-insensitive)
    capital_info = None
    for state_name, (capital, sigla) in CAPITAIS_BRASIL.items():
        if state_lower == state_name or state_lower == sigla.lower():
            capital_info = (capital, sigla)
            break
    if not capital_info:
        return f"Estado '{state}' não encontrado. Use o nome completo ou sigla (ex: São Paulo ou SP)."
    capital, sigla = capital_info
    # Reutiliza o tool de cidade para buscar a capital
    return await get_current_weather_city(capital, sigla)

6. Execução do servidor

Por fim, este bloco garante que, ao rodar o script, o servidor MCP será iniciado corretamente. Ele também faz uma checagem para garantir que a chave da API está configurada, evitando erros comuns de configuração.

if __name__ == "__main__":
    # Mensagem inicial para o usuário
    print("🌦️ Servidor MCP Weather Brasil")
    print("⚠️  IMPORTANTE: Configure sua API key do OpenWeatherMap!")
    print("   1. Cadastre-se em: https://openweathermap.org/api")
    print("   2. Substitua OPENWEATHER_API_KEY no código")
    print("   3. Execute novamente")

    # Verifica se a API KEY está configurada
    if OPENWEATHER_API_KEY == "SUA_API_KEY_AQUI":
        print("\n❌ API key não configurada!")
        exit(1)

    # Inicia o servidor MCP usando transporte STDIO
    mcp.run(transport="stdio")

Conectando ao Claude for Desktop

Para integrar o servidor MCP ao Claude for Desktop, edite o arquivo de configuração conforme o seu sistema operacional:

  • macOS/Linux:
~/.config/claude_desktop/claude_desktop_config.json
  • Windows:
%APPDATA%\Claude\claude_desktop_config.json

Adicione a seguinte configuração, ajustando os caminhos conforme o local do seu projeto:

{
  "mcpServers": {
    "weather": {
      "command": "/Users/SEU_USUARIO/.local/bin/uv",
      "args": [
        "--directory",
        "/Users/SEU_USUARIO/PASTA_ONDE_ESTA_O_PROJETO/weather",
        "run",
        "weather.py"
      ],
      "env": {
        "PATH": "/Users/SEU_USUARIO/.local/bin:/usr/local/bin:/usr/bin:/bin"
      }
    }
  }
}

Salve o arquivo e reinicie o Claude for Desktop para que a integração seja reconhecida.


Testando no Claude

Abra o Claude e acesse a seção “Search and tools”. Você verá as quatro ferramentas disponíveis:

  • get_current_weather_city
  • get_forecast_city
  • get_weather_coordinates
  • get_weather_state_capital

Você pode chamar as ferramentas diretamente nas conversas. Exemplos de uso:

  • get_current_weather_city — "Como está o tempo em Recife, PE?"
  • get_forecast_city — "Qual a previsão para Porto Alegre, RS?"
  • get_weather_coordinates — "Me dê o clima para latitude -23.55 e longitude -46.63."
  • get_weather_state_capital — "Como está o tempo na capital de Minas Gerais?"

Assim, é possível consultar condições e previsões meteorológicas para cidades, capitais ou coordenadas específicas diretamente pelo Claude.


Depuração

Se aparecer a mensagem "No tools found":

  • Verifique se o servidor está sendo executado corretamente.
  • Revise a configuração no arquivo claude_desktop_config.json.
  • Confira se existe comunicação via STDIO (sem erros silenciosos).

Próximos passos

  • Adicionar parâmetros nos tools (ex: data ou localização).
  • Incluir resources (ex: enviar arquivos com dados climáticos).
  • Criar prompts reutilizáveis.
  • Usar HTTP/SSE como transporte em vez de STDIO.

Resumo

Neste tutorial, você aprendeu a:

  1. Criar um servidor MCP em Python.
  2. Expor quatro ferramentas:
    • get_current_weather_city
    • get_forecast_city
    • get_weather_coordinates
    • get_weather_state_capital
  3. Conectar o servidor ao Claude for Desktop via STDIO.

Agora você pode expandir para novos usos — como banco de dados, GitHub, e-mail, automações e muito mais.


Para mais informações sobre o protocolo MCP, consulte a documentação oficial: https://modelcontextprotocol.io/introduction