Servidores MCP em Python: Guia Prático para Construir Agentes Conectados em 2026
Construa um servidor MCP em Python com FastMCP, Pydantic e OAuth 2.1. Tutorial passo a passo com stdio, Streamable HTTP, Inspector e code execution Anthropic para 2026.
Um servidor MCP em Python é um processo que expõe ferramentas, recursos e prompts a um modelo de linguagem usando o Model Context Protocol, o padrão aberto que a Anthropic publicou em novembro de 2024 e que, em meados de 2026, já ultrapassa 97 milhões de downloads mensais de SDK e 9.400 servidores no registro oficial. Com a SDK mcp[cli] e o decorador @mcp.tool() do FastMCP, você troca centenas de integrações ponto-a-ponto por um único protocolo que qualquer host MCP (Claude Code, Cursor, ChatGPT, Codex) consegue consumir.
O MCP virou padrão de fato em 2026: governado pela Linux Foundation desde dezembro de 2025 e suportado nativamente por Anthropic, OpenAI, Google, Microsoft e AWS.
Use o FastMCP (parte do pacote oficial mcp[cli] v1.27+) para gerar schemas, validar argumentos com Pydantic e expor ferramentas com um decorador.
Para produção remota, prefira o transporte Streamable HTTP (especificação de novembro de 2025) e autenticação OAuth 2.1. O SSE legado está deprecado.
Teste sempre com npx @modelcontextprotocol/inspector antes de conectar o servidor a um agente real. É o equivalente do Postman para MCP.
O padrão code execution with MCP (Anthropic, 2026) carrega ferramentas sob demanda e filtra dados antes de chegarem ao modelo, reduzindo drasticamente o uso de contexto.
Servidores de referência do repositório oficial são exemplos educacionais. Em produção, escope o acesso a arquivos, valide tudo no servidor e nunca confie em nomes de parâmetros emitidos pelo LLM.
O que é o Model Context Protocol?
O Model Context Protocol (MCP) é um padrão aberto que separa a tarefa de fornecer contexto a um LLM da tarefa de gerar a resposta. Na prática: o seu agente (Claude, GPT, Gemini, qualquer um) vira um cliente MCP que conversa por JSON-RPC com servidores que expõem ferramentas e dados. A analogia que pegou foi a do USB-C: em vez de cada par modelo-ferramenta exigir um adaptador customizado, todos falam o mesmo protocolo.
Honestamente, eu venho montando pipelines com tool use desde 2023 e a parte mais cansativa sempre foi a colagem. Escrever um wrapper para a API X, outro para o banco Y, mais um para o sistema Z. O MCP elimina essa colagem. Você publica um servidor, e qualquer host MCP (Claude Desktop, Claude Code, Cursor, Codex, n8n) conecta sem código adicional. Isso vale tanto para um servidor local rodando em stdio quanto para um servidor SaaS na nuvem.
Em 2026, o ecossistema explodiu. A especificação 2.4 trouxe MFA para chamadas de alto risco, logs de auditoria em tempo real via Admin Console e fluxos de consentimento melhorados. Plataformas corporativas como Salesforce Agentforce, Cloudflare e Azure OpenAI já suportam MCP nativamente. Se você ainda está escrevendo integrações ad-hoc por dentro de prompts gigantes, está fazendo o equivalente a soldar fios em 2026, e eu tenho pouca paciência com prompt theatre quando existe um padrão.
As três primitivas de um servidor MCP
Um servidor MCP expõe três tipos de capacidades, e entender a diferença antes de codar evita meses de refatoração depois:
Tools (ferramentas): funções que o LLM pode chamar com aprovação do usuário. Use quando há um efeito colateral ou cálculo. Exemplo: create_invoice(customer_id, amount).
Resources (recursos): dados parecidos com arquivos, identificados por URI, lidos pelo cliente. Use para conteúdo que o modelo pode querer carregar como contexto. Exemplo: file:///docs/policy.md ou db://orders/2026-06.
Prompts: templates pré-escritos que o usuário (não o modelo) pode invocar. Use para fluxos repetíveis: revisão de PR, geração de release notes, triagem de bug.
So, o erro mais comum que vejo é tratar tudo como tool. Se o conteúdo é estático ou paginado e o modelo só precisa ler, é um resource: o cliente decide quando carregar e o protocolo permite listagem preguiçosa. Isso preserva contexto, que é a moeda mais cara em qualquer pipeline de agente. Se você quer entender por que essa distinção importa para o design do agente em si, vale revisitar nosso material sobre engenharia de contexto para agentes de IA, que mostra como o orçamento de tokens vira gargalo.
Como criar um servidor MCP em Python passo a passo
Vamos construir um servidor MCP funcional que expõe uma ferramenta para consultar previsão do tempo e um recurso para listar cidades cacheadas. Requisitos: Python 3.10+ e uv (recomendado pela documentação oficial, mas pip funciona).
from mcp.server.fastmcp import FastMCP
from pydantic import Field
import httpx
mcp = FastMCP("weather-mcp", json_response=True)
# Cache simples em memoria; para producao use Redis ou similar
_CITY_CACHE: dict[str, dict] = {}
@mcp.tool()
async def get_forecast(
city: str = Field(..., description="Nome da cidade, ex: 'Sao Paulo'"),
days: int = Field(3, ge=1, le=7, description="Dias a prever (1 a 7)"),
) -> dict:
"Retorna previsao do tempo para a cidade nos proximos dias."
async with httpx.AsyncClient(timeout=10) as client:
# Open-Meteo nao exige chave, bom para exemplos
geo = await client.get(
"https://geocoding-api.open-meteo.com/v1/search",
params={"name": city, "count": 1, "language": "pt"},
)
geo.raise_for_status()
results = geo.json().get("results") or []
if not results:
raise ValueError(f"Cidade nao encontrada: {city}")
loc = results[0]
forecast = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": loc["latitude"],
"longitude": loc["longitude"],
"daily": "temperature_2m_max,temperature_2m_min,precipitation_sum",
"forecast_days": days,
"timezone": "auto",
},
)
forecast.raise_for_status()
data = forecast.json()
_CITY_CACHE[city.lower()] = {"lat": loc["latitude"], "lon": loc["longitude"]}
return {
"city": loc["name"],
"country": loc.get("country"),
"daily": data["daily"],
}
@mcp.resource("weather://cached-cities")
def list_cached_cities() -> str:
"Lista cidades ja consultadas neste servidor."
if not _CITY_CACHE:
return "Nenhuma cidade em cache."
lines = [f"- {name}: ({c['lat']}, {c['lon']})" for name, c in _CITY_CACHE.items()]
return "\n".join(lines)
@mcp.prompt()
def daily_briefing(city: str) -> str:
"Template para um briefing diario de clima."
return (
f"Gere um briefing matinal de 3 paragrafos para {city}, "
"destacando temperatura, chuva e recomendacoes de vestuario. "
"Use a ferramenta get_forecast com days=1."
)
if __name__ == "__main__":
mcp.run(transport="stdio")
Quatro coisas para notar nesse código. Primeira: o decorador @mcp.tool() gera o JSON Schema automaticamente a partir das anotações Pydantic, e Field(..., ge=1, le=7) vira validação no servidor antes da função executar. Segunda: a função é async, então httpx assíncrono é grátis. Terceira: erros viram exceções normais; o FastMCP traduz para JSON-RPC errors com stack trace truncado. Quarta: o transport stdio é o default e é o que o Claude Desktop espera.
stdio vs Streamable HTTP: qual transporte usar?
Em 2026 existem dois transportes que importam: stdio (processo local) e Streamable HTTP (remoto, introduzido na spec de novembro de 2025, substituindo o antigo HTTP+SSE). A escolha define quase tudo: deploy, autenticação, latência, modelo de operação.
Dimensão
stdio
Streamable HTTP
Modelo de execução
Subprocess local lançado pelo host
Serviço remoto sobre HTTPS
Latência típica
<5ms (IPC local)
20 a 200ms (rede)
Autenticação
Implícita (processo do usuário)
OAuth 2.1 obrigatória em produção
Estado entre chamadas
Memória do processo (até ele morrer)
Sessão HTTP ou storage externo
Deploy
uv run server.py via config do host
Container atrás de load balancer
Caso de uso
Ferramentas pessoais, Claude Desktop, Claude Code
SaaS multi-tenant, integrações enterprise
Compatibilidade legada
Universal
Requer cliente MCP >= jun/2025
Minha regra prática: comece com stdio. Quase todo servidor MCP útil começa como uma ferramenta pessoal que você roda localmente para automatizar algo no seu fluxo de trabalho. Quando outras pessoas precisam usar, você muda transport="stdio" para transport="streamable-http", adiciona OAuth e empacota num container. A SDK abstrai a maior parte da diferença; o que muda é a infraestrutura ao redor.
Como autenticar servidores MCP em produção com OAuth 2.1
Para qualquer servidor MCP exposto pela internet, a especificação de junho de 2025 padronizou OAuth 2.1: o servidor MCP é classificado como Resource Server, e anuncia onde está o Authorization Server via endpoint .well-known/oauth-authorization-server. Isso resolve o problema dos primeiros 6 meses do MCP, em que cada um inventou seu esquema de API key.
Em Python, a integração mais direta é usar um provider OAuth externo (Auth0, Clerk, Cognito, ou seu IdP corporativo) e validar o JWT nas requests:
from mcp.server.fastmcp import FastMCP
from mcp.server.auth import BearerAuthProvider
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://auth.example.com/.well-known/jwks.json"
AUDIENCE = "https://mcp.example.com"
ISSUER = "https://auth.example.com/"
class JwtAuth(BearerAuthProvider):
def __init__(self):
self._jwks = PyJWKClient(JWKS_URL)
async def validate_token(self, token: str) -> dict:
signing_key = self._jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
options={"require": ["exp", "sub", "aud", "iss"]},
)
mcp = FastMCP(
"weather-mcp",
auth=JwtAuth(),
json_response=True,
)
@mcp.tool()
async def get_forecast(city: str, ctx) -> dict:
# ctx.auth tem as claims validadas; use sub/scope para autorizacao fina
if "forecast:read" not in ctx.auth.get("scope", "").split():
raise PermissionError("Escopo forecast:read necessario")
# ... resto da implementacao
...
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
Três detalhes que viram bug em produção. (1) Sempre valide aud e iss, não só a assinatura, porque JWT sem audiência aceita tokens de outros serviços. (2) Faça cache da JWKS, mas com TTL, já que rotação de chaves quebra servidores que cachearam para sempre. (3) Escopos granulares no nível da tool, não no nível do servidor: forecast:read e forecast:admin separados evitam que um agente com escopo de leitura faça chamadas escritas.
Como testar um servidor MCP com o Inspector
O MCP Inspector é o equivalente do Postman para servidores MCP. Lance com:
npx @modelcontextprotocol/inspector uv run python server.py
Ele abre uma UI no navegador onde você lista tools, executa chamadas com argumentos arbitrários, inspeciona logs JSON-RPC e valida schemas. Eu uso isso religiosamente antes de conectar a um cliente real. Economiza horas de "por que o Claude não está vendo minha tool".
Para CI, escreva testes unitários direto contra o objeto mcp:
import pytest
from mcp.testing import McpTestClient
from server import mcp
@pytest.mark.asyncio
async def test_forecast_validates_days():
client = McpTestClient(mcp)
async with client.session() as session:
with pytest.raises(ValueError):
await session.call_tool("get_forecast", {"city": "Sao Paulo", "days": 10})
@pytest.mark.asyncio
async def test_forecast_known_city():
client = McpTestClient(mcp)
async with client.session() as session:
result = await session.call_tool("get_forecast", {"city": "Sao Paulo", "days": 1})
assert "daily" in result.content[0].text
assert "temperature_2m_max" in result.content[0].text
Avaliação primeiro, prompt theatre depois. Essa é a postura que distingue um agente que funciona de uma demo de palco. Para a parte de avaliar a saída do agente que consome essas tools, vale combinar com o que escrevi sobre avaliação de LLMs com DeepEval e Ragas: o servidor MCP é apenas um lado da equação, e quem não mede o outro lado está chutando.
Code execution com MCP: o padrão Anthropic para reduzir contexto
Em fevereiro de 2026, a Anthropic publicou um artigo de engenharia mostrando que agentes com acesso a centenas de tools MCP gastam mais contexto descrevendo as próprias tools do que executando trabalho útil. A solução: usar code execution como meta-tool. Em vez de expor get_forecast, get_invoice, send_email diretamente, exponha um sandbox Python que importa essas tools sob demanda.
A vantagem é tripla. (1) Você carrega só o schema das tools que o modelo realmente usa em cada turno. (2) Lógica composta como "para cada fatura vencida, mande email" vira uma execução, não dezenas de tool calls. (3) Você filtra dados antes de chegar ao modelo, em vez de despejar 50 KB de JSON no contexto. Quem já leu nosso guia de function calling e tool use em produção reconhece o padrão. É a evolução natural quando o número de tools cresce.
Esqueleto mínimo de uma tool de code execution num servidor MCP:
import asyncio
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("agent-runtime")
# Em producao: use um sandbox isolado (Pyodide, Modal, E2B, gVisor).
# Este exemplo ilustra o shape, NAO o isolamento.
ALLOWED_MODULES = {"weather_tools", "invoice_tools"}
@mcp.tool()
async def run_python(code: str, timeout_s: int = 10) -> dict:
"Executa codigo Python com acesso aos modulos de tools permitidos."
proc = await asyncio.create_subprocess_exec(
"python", "-I", "-c", code,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout_s)
except asyncio.TimeoutError:
proc.kill()
return {"error": "timeout"}
return {
"stdout": stdout.decode()[:8000],
"stderr": stderr.decode()[:2000],
"exit_code": proc.returncode,
}
Boas práticas de produção e segurança
Depois de ter colocado meia dúzia desses servidores em produção, esses são os calos que eu carrego.
Escopar acesso a arquivos
O servidor oficial @modelcontextprotocol/server-filesystem aceita um diretório raiz. Nunca passe /. Defina o menor diretório possível e teste com o Inspector que tools de leitura/escrita rejeitam paths fora do escopo. Path traversal via ../ ainda é vetor: valide com pathlib.Path.resolve() e compare com o root. (Eu cai nessa armadilha num projeto interno em 2025 e perdi uma tarde inteira diagnosticando.)
Rate limiting por sub do token
OAuth te dá identidade. Use-a: limite chamadas por sub, não por IP. Agentes legítimos frequentemente rodam atrás de NAT corporativo, e um IP pode ser um time inteiro.
Telemetria estruturada desde o dia 1
Logue cada tool call com: session_id, tool_name, arg_hash (não os args inteiros, por causa de PII), latency_ms, status. Sem isso, você não consegue responder "qual tool está engasgando o agente em produção?". E essa pergunta vai surgir.
Versionar o protocolo, não só a aplicação
Anuncie protocolVersion nas respostas e quebre tools com nomes versionados (get_forecast.v2) em vez de mudar shape silenciosamente. Clientes MCP existem em dezenas de hosts, e rollouts em sincronia são fantasia.
RAG vs tool: a fronteira certa
Tools são para ações e queries dinâmicas. Para corpus estático e busca semântica, mantenha um pipeline RAG separado e exponha-o via uma única tool de busca, em vez de tentar fazer um servidor MCP com 200 resources mapeando cada documento. Se essa parte é nova para você, nosso material sobre pipelines RAG em produção mostra onde traçar a linha sem inflar o servidor.
Perguntas frequentes
Qual a diferença entre MCP e function calling tradicional?
Function calling é a feature do LLM que decide chamar uma função; MCP é o protocolo que define como essas funções são descobertas, descritas e executadas entre processos. Você normalmente usa os dois juntos: o modelo faz function calling para invocar uma tool, e a tool é entregue pelo seu servidor MCP. Sem MCP, cada integração é colada manualmente no app do agente.
Preciso usar FastMCP ou posso escrever o protocolo na mão?
Pode escrever na mão (o protocolo é JSON-RPC 2.0 documentado), mas FastMCP, que vem dentro do pacote oficial mcp[cli], cuida de geração de schema, validação Pydantic, lifecycle e roteamento de transportes. Para 95% dos casos, FastMCP é a escolha certa. Reserve a implementação manual para servidores embutidos em runtimes muito restritos.
Servidores MCP funcionam com OpenAI e Gemini também?
Sim. Apesar de a Anthropic ter criado o MCP, em 2026 ele é suportado nativamente por OpenAI (via Codex e ChatGPT), Google (Gemini Code Assist), Microsoft (Azure OpenAI) e AWS. O servidor é o mesmo, o cliente que muda. Por isso o investimento em escrever um servidor MCP rende mais do que escrever uma integração específica para um único host.
Posso publicar meu servidor MCP no registro oficial?
Sim. O registro oficial em modelcontextprotocol.io aceita submissões via PR no repositório modelcontextprotocol/servers. Servidores precisam de README com instruções de instalação, licença explícita e, idealmente, suíte de testes. Em meados de 2026 o registro já lista mais de 9.400 servidores, então diferenciação por qualidade e nicho importa mais do que por quantidade de tools.
Como debugar um servidor MCP que o Claude Desktop não enxerga?
Três checks na ordem: (1) abra o log do Claude Desktop (em macOS, ~/Library/Logs/Claude/mcp-server-*.log) para ver se o processo sobe; (2) execute o servidor diretamente com uv run server.py e cole stdin de um request initialize JSON-RPC para confirmar que ele responde; (3) rode o MCP Inspector apontando para o mesmo comando. Em 9 de cada 10 casos é caminho errado no config ou dependência faltando no ambiente do servidor.
Guia prático de structured outputs em LLMs com JSON Schema: implementação em OpenAI, Anthropic e Gemini, unificação via Instructor e Pydantic-AI, armadilhas de produção e como validar respostas com segurança.
Langfuse (MIT, self-host) vs Helicone (proxy 5 min) vs Arize Phoenix (eval rigoroso): comparação prática de observabilidade LLM em 2026, com código OpenTelemetry, métricas e o dashboard que você vai desejar ter construído primeiro.
Compare como Anthropic, OpenAI e Google implementam cache de prompts em 2026: TTLs, preços, exemplos de código em Python e as regras práticas para maximizar hits em produção e cortar até 90% do custo de tokens de entrada.