Kaique Mitsuo Silva Yamamoto
Arquitetura softwareCursor + MCP

Criando MCP server com Python e FastMCP

Como construir servidores MCP com Python usando FastMCP — o framework com menos boilerplate, decorators Pythônicos e suporte nativo a async.

Criando MCP server com Python e FastMCP

O FastMCP é o framework Python mais popular para criar servidores MCP. Ele abstrai toda a comunicação JSON-RPC e permite definir ferramentas com simples decorators — o mesmo padrão que você já usa em FastAPI, Flask e Typer.


Por que FastMCP em vez do SDK oficial?

AspectoSDK Oficial PythonFastMCP
BoilerplateAlto — registros manuaisMínimo — decorators
Curva de aprendizadoModeradaBaixa para devs Python
AsyncSuportadoNativo, padrão
ValidaçãoManualVia type hints do Python
TestesManuaismcp dev integrado

Na prática, o que leva 60 linhas no SDK oficial leva 20 no FastMCP.


Setup do ambiente

# Com uv (recomendado)
uv init meu-mcp-python
cd meu-mcp-python
uv add "mcp[cli]" httpx

# Com pip
pip install fastmcp httpx

O pacote mcp[cli] inclui o FastMCP e o inspector de desenvolvimento.


Servidor mínimo funcional

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("meu-servidor")

@mcp.tool()
def somar(a: float, b: float) -> str:
    """Soma dois números e retorna o resultado.

    Args:
        a: Primeiro número
        b: Segundo número
    """
    return f"{a} + {b} = {a + b}"

if __name__ == "__main__":
    mcp.run()

Execute e teste:

# Testar com o inspector interativo
mcp dev server.py

# Rodar em produção (stdio)
python server.py

Regra de ouro: nunca use print() sem stderr

Assim como no TypeScript, stdout é reservado para o protocolo JSON-RPC. Qualquer print() padrão corrompe as mensagens.

import sys

# ❌ ERRADO — corrompe o protocolo
print("processando...")

# ✅ CORRETO — vai para stderr
print("processando...", file=sys.stderr)

# Alternativa: use logging para stderr
import logging
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("processando...")

Tools assíncronas com httpx

O padrão recomendado para chamadas HTTP em FastMCP é usar httpx com async/await:

# weather.py
import sys
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather-br")

API_BASE = "https://api.open-meteo.com/v1"

async def http_get(url: str, params: dict = {}) -> dict | None:
    """Requisição HTTP com tratamento de erro centralizado."""
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, params=params, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            print(f"Erro HTTP: {e}", file=sys.stderr)
            return None

@mcp.tool()
async def previsao_tempo(latitude: float, longitude: float) -> str:
    """Busca previsão do tempo para coordenadas geográficas.

    Args:
        latitude: Latitude em graus decimais (ex: -23.5 para São Paulo)
        longitude: Longitude em graus decimais (ex: -46.6 para São Paulo)
    """
    data = await http_get(
        f"{API_BASE}/forecast",
        params={
            "latitude": latitude,
            "longitude": longitude,
            "hourly": "temperature_2m,precipitation_probability",
            "forecast_days": 1,
            "timezone": "America/Sao_Paulo",
        },
    )

    if not data:
        return "Não foi possível obter dados meteorológicos."

    horas = data["hourly"]["time"][:6]
    temps = data["hourly"]["temperature_2m"][:6]
    chuva = data["hourly"]["precipitation_probability"][:6]

    linhas = [f"{h[11:]}: {t}°C, {c}% chance de chuva"
              for h, t, c in zip(horas, temps, chuva)]

    return "Próximas 6 horas:\n" + "\n".join(linhas)

@mcp.tool()
async def geocodificar(cidade: str) -> str:
    """Obtém coordenadas de uma cidade brasileira.

    Args:
        cidade: Nome da cidade (ex: "São Paulo", "Rio de Janeiro")
    """
    data = await http_get(
        "https://geocoding-api.open-meteo.com/v1/search",
        params={"name": cidade, "count": 3, "language": "pt", "format": "json"},
    )

    if not data or not data.get("results"):
        return f"Cidade '{cidade}' não encontrada."

    import json
    resultados = [
        {
            "nome": r["name"],
            "estado": r.get("admin1", "—"),
            "pais": r.get("country", "—"),
            "latitude": r["latitude"],
            "longitude": r["longitude"],
        }
        for r in data["results"]
    ]

    return json.dumps(resultados, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    mcp.run()

Lendo variáveis de ambiente

import os
import sys
from mcp.server.fastmcp import FastMCP

# Carregar no startup — falha rápido se faltar algo crítico
MINHA_API_KEY = os.environ.get("MINHA_API_KEY")
if not MINHA_API_KEY:
    print("ERRO: variável MINHA_API_KEY não definida", file=sys.stderr)
    sys.exit(1)

mcp = FastMCP("api-autenticada")

@mcp.tool()
async def buscar_dado(id: str) -> str:
    """Busca um dado autenticado.

    Args:
        id: ID do recurso
    """
    import httpx
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"https://api.exemplo.com/dados/{id}",
            headers={"Authorization": f"Bearer {MINHA_API_KEY}"}
        )
        return resp.text

if __name__ == "__main__":
    mcp.run()

Configuração no mcp.json:

{
  "mcpServers": {
    "api-autenticada": {
      "command": "python",
      "args": ["/caminho/absoluto/server.py"],
      "env": {
        "MINHA_API_KEY": "sk-..."
      }
    }
  }
}

Exemplo real: scraper de dados para análise

# scraper_mcp.py
import sys
import json
import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("scraper-financeiro")

@mcp.tool()
async def cotacao_cripto(simbolo: str) -> str:
    """Busca cotação atual de uma criptomoeda.

    Args:
        simbolo: Símbolo da moeda (ex: bitcoin, ethereum, solana)
    """
    async with httpx.AsyncClient() as client:
        try:
            resp = await client.get(
                f"https://api.coingecko.com/api/v3/simple/price",
                params={
                    "ids": simbolo.lower(),
                    "vs_currencies": "brl,usd",
                    "include_24hr_change": "true",
                    "include_market_cap": "true",
                },
                headers={"accept": "application/json"},
                timeout=15.0,
            )
            resp.raise_for_status()
            data = resp.json()

            if simbolo.lower() not in data:
                return f"Criptomoeda '{simbolo}' não encontrada."

            coin = data[simbolo.lower()]
            return json.dumps({
                "brl": f"R$ {coin['brl']:,.2f}",
                "usd": f"$ {coin['usd']:,.2f}",
                "variacao_24h": f"{coin.get('brl_24h_change', 0):.2f}%",
                "market_cap_brl": f"R$ {coin.get('brl_market_cap', 0):,.0f}",
            }, ensure_ascii=False, indent=2)

        except httpx.HTTPStatusError as e:
            return f"Erro na API: {e.response.status_code}"

@mcp.tool()
async def listar_moedas_populares() -> str:
    """Lista as 10 criptomoedas mais populares por capitalização de mercado."""
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            "https://api.coingecko.com/api/v3/coins/markets",
            params={
                "vs_currency": "brl",
                "order": "market_cap_desc",
                "per_page": 10,
                "page": 1,
            },
            timeout=15.0,
        )
        resp.raise_for_status()
        moedas = resp.json()

        resultado = [
            {
                "rank": i + 1,
                "nome": m["name"],
                "simbolo": m["symbol"].upper(),
                "preco_brl": f"R$ {m['current_price']:,.2f}",
                "variacao_24h": f"{m['price_change_percentage_24h']:.2f}%",
            }
            for i, m in enumerate(moedas)
        ]

        return json.dumps(resultado, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    mcp.run()

Agora o Cursor pode responder: "Qual é o preço do Bitcoin em reais hoje?" consultando a API diretamente, sem alucinações de valores desatualizados.


Usando uvx para execução sem instalação global

O uvx (parte do uv) executa scripts Python em ambientes isolados, sem precisar instalar dependências globalmente. Ideal para MCPs.

{
  "mcpServers": {
    "scraper": {
      "command": "uvx",
      "args": ["--from", "httpx", "python", "/caminho/server.py"]
    }
  }
}

Para projetos com pyproject.toml:

{
  "mcpServers": {
    "meu-mcp": {
      "command": "uvx",
      "args": ["--from", "/caminho/do/projeto", "python", "server.py"]
    }
  }
}

Testando com o inspector

# Inspector interativo (abre UI em http://127.0.0.1:6274)
mcp dev server.py

# Teste direto no terminal
mcp run server.py

O inspector permite chamar cada tool com parâmetros customizados e inspecionar as respostas antes de conectar ao Cursor.


Configuração no mcp.json

{
  "mcpServers": {
    "weather-br": {
      "command": "python",
      "args": ["/home/usuario/mcps/weather.py"],
      "env": {}
    },
    "scraper-financeiro": {
      "command": "python",
      "args": ["/home/usuario/mcps/scraper_mcp.py"],
      "env": {}
    }
  }
}

Checklist antes de usar em produção

  • Nenhum print() sem file=sys.stderr
  • Variáveis de ambiente lidas com os.environ.get() e validação no startup
  • Timeout definido em todas as chamadas HTTP (timeout=30.0)
  • Tratamento de exceções em cada tool
  • Testado com mcp dev antes de adicionar ao Cursor
  • Dependências declaradas em pyproject.toml ou requirements.txt
  • Caminho absoluto no args do mcp.json

On this page