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?
| Aspecto | SDK Oficial Python | FastMCP |
|---|---|---|
| Boilerplate | Alto — registros manuais | Mínimo — decorators |
| Curva de aprendizado | Moderada | Baixa para devs Python |
| Async | Suportado | Nativo, padrão |
| Validação | Manual | Via type hints do Python |
| Testes | Manuais | mcp 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 httpxO 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.pyRegra 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.pyO 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()semfile=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 devantes de adicionar ao Cursor - Dependências declaradas em
pyproject.tomlourequirements.txt - Caminho absoluto no
argsdomcp.json
Criando MCP server com TypeScript
Guia completo para construir um servidor MCP customizado do zero com TypeScript, o SDK oficial da Anthropic e validação de schema com Zod.
Cursor Agent Mode: workflows avançados com MCPs
Como o Cursor Agent opera autonomamente, a diferença fundamental do Chat Mode, e como combinar MCPs com o agente para automatizar tarefas complexas de desenvolvimento.