~ / tutoriais /como-criar-mcp-server $ _

Como criar seu primeiro MCP server (tool + resource) e plugar no Claude

Lucas Souza Lucas Souza 10 min de leitura Tutoriais
Como criar seu primeiro MCP server (tool + resource) e plugar no Claude

Você já plugou um MCP server pronto no seu Claude. O do GitHub, o do Postgres, o do Slack. Funciona, é mágico, e aí bate a pergunta: e se eu quiser expor as minhas coisas? Sua API interna, seus padrões de time, aquele banco que só você entende.

O Model Context Protocol é o padrão aberto que a Anthropic lançou em novembro de 2024 para conectar modelos a dados e ferramentas externas sem cada integração virar um projeto à parte (anúncio oficial). Consumir MCP server dos outros é fácil. Construir o seu é onde a coisa fica interessante — e é exatamente o que falta em português.

Neste tutorial você vai escrever um MCP server do zero em Python: uma tool que consulta CEP de verdade e um resource que expõe os padrões de engenharia do seu time. No fim, você pluga no Claude e vê o agente chamar uma ferramenta que você escreveu.

TL;DR

  • O que é: um MCP server é um processo que expõe capacidades (tools, resources, prompts) para qualquer cliente que fale o Model Context Protocol — Claude, Cursor, e por aí vai.
  • Stack: Python 3.10+, SDK oficial mcp[cli], uv pra gerenciar o projeto, httpx pra chamada HTTP.
  • Custo/Acesso: tudo open-source e gratuito. O exemplo usa a ViaCEP, que não pede chave.
  • Repositório/Link útil: tutorial oficial de build server e o Python SDK.

O contexto: por que um MCP server importa?

Antes do MCP, conectar modelo a ferramenta era o que os engenheiros chamavam de problema M×N. Tem M modelos e N ferramentas? Você escrevia M vezes N integrações na mão. Dez aplicações falando com cem ferramentas davam mil adaptadores possíveis, cada um do seu jeito.

O MCP corta isso. Vira M+N. Você escreve um servidor uma vez, e qualquer cliente que fala o protocolo consome. É a mesma ideia do USB-C: antes, cada aparelho tinha um conector; depois, uma porta só pra tudo.

Se você quer o panorama do protocolo antes de pôr a mão no código, a gente já destrinchou o que é MCP e por que virou padrão. Aqui o foco é construir.

Um MCP server expõe três tipos de capacidade. Vale gravar a tabela, porque a diferença entre elas é a parte que mais confunde quem está começando (docs oficiais):

Building block O que é Quem controla
Tools Funções que o modelo chama pra agir (escrever no banco, bater numa API, mandar e-mail). O modelo
Resources Dados read-only que entram como contexto (arquivo, schema, doc). A aplicação
Prompts Templates de instrução que o usuário dispara de propósito. O usuário

Guarde isso: tool é ação, resource é contexto. A gente vai construir uma de cada e a distinção vai ficar concreta.

Pré-requisitos e ferramentas

Antes de começar, você precisa de:

  • [ ] Python 3.10 ou superior (o SDK exige a partir da 1.2.0).
  • [ ] uv instalado — é o gerenciador de pacotes e venv que a doc oficial recomenda.
  • [ ] Conhecimento básico de Python e async. Nada além de async def.
  • [ ] Um cliente MCP pra testar no fim: Claude for Desktop ou o MCP Inspector (já vem no SDK).

Sem uv? Uma linha resolve:

curl -LsSf https://astral.sh/uv/install.sh | sh

Mão na massa: seu primeiro MCP server

Passo 1: o esqueleto do projeto

Cria o projeto, o ambiente e instala o SDK com as ferramentas de linha de comando:

uv init meu-mcp
cd meu-mcp
uv add "mcp[cli]" httpx

O mcp[cli] traz o SDK e o comando mcp (a gente usa ele pra testar daqui a pouco). O httpx é só pra fazer a chamada HTTP da nossa tool. Agora cria o arquivo servidor.py.

Passo 2: instanciar o FastMCP

O coração é uma linha. O FastMCP é a classe que faz o trabalho chato — ele lê seus type hints e suas docstrings e gera as definições de tool e resource automaticamente. Você escreve Python normal; ele monta o JSON Schema.

import httpx
from mcp.server.fastmcp import FastMCP

# O nome é como o servidor aparece no cliente.
mcp = FastMCP("ferramentas-do-time")

VIACEP = "https://viacep.com.br/ws"

Passo 3: expor uma tool (a ação)

Tool é o que o modelo chama quando decide fazer alguma coisa. Decorou com @mcp.tool(), virou tool. O nome da função, os parâmetros tipados e a docstring viram a descrição que o modelo lê pra decidir se chama.

Vamos fazer uma que consulta CEP de verdade na ViaCEP — sem chave, sem firula:

@mcp.tool()
async def consultar_cep(cep: str) -> str:
    """Consulta um CEP brasileiro e devolve o endereço.

    Args:
        cep: CEP com 8 dígitos, com ou sem traço (ex: 01310-100)
    """
    cep = cep.replace("-", "").strip()
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"{VIACEP}/{cep}/json/", timeout=10.0)

    if resp.status_code != 200:
        return f"Erro ao consultar o CEP {cep}."

    dados = resp.json()
    if dados.get("erro"):
        return f"CEP {cep} nao encontrado."

    return (
        f"{dados['logradouro']}, {dados['bairro']} - "
        f"{dados['localidade']}/{dados['uf']} (CEP {dados['cep']})"
    )

Repara que aquela docstring não é enfeite. É ela que o modelo usa pra entender o que a ferramenta faz e o que mandar em cep. Docstring ruim, tool que o modelo chama errado. Trate a descrição como parte do contrato, não como comentário.

Passo 4: expor um resource (o contexto)

Agora a outra metade. Resource é dado read-only que a aplicação injeta como contexto — o modelo não "chama" um resource pra agir, ele . Cada resource tem uma URI única, e o FastMCP suporta URI com parâmetro (o chamado resource template).

Vamos expor os padrões de engenharia do time. O cliente pede padroes://commits, padroes://testes, e recebe o texto:

PADROES = {
    "commits": "Use Conventional Commits. Mensagem no imperativo, em portugues.",
    "testes": "Todo PR sobe com teste. Pest pra unit, sem mock de banco.",
    "branches": "feature/<ticket>-descricao. Nada de push direto na main.",
}


@mcp.resource("padroes://{area}")
def padrao_do_time(area: str) -> str:
    """Devolve o padrao de engenharia do time para uma area."""
    return PADROES.get(area, f"Sem padrao definido para '{area}'.")

O {area} na URI é o parâmetro. padroes://testes casa com area="testes". É o mesmo padrão de um resource template tipo weather://forecast/{cidade} da doc oficial — URI dinâmica, contexto sob demanda.

Passo 5: rodar e testar

Fecha o arquivo dizendo qual transporte usar. Pra rodar local, é stdio (o cliente conversa com o servidor por stdin/stdout):

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

Não precisa subir nada pra inspecionar. O SDK traz o MCP Inspector, uma UI que conecta no seu servidor e deixa você listar e disparar tools e resources na mão:

uv run mcp dev servidor.py

Abre no navegador, lista a consultar_cep e o padroes://{area}, e você testa cada um antes de plugar em qualquer modelo. Esse passo economiza muita dor: se quebra aqui, o problema é seu servidor, não o cliente.

Passo 6: plugar no Claude

Com o servidor funcionando, registra ele no Claude for Desktop. Abre o claude_desktop_config.json e adiciona seu servidor na chave mcpServers:

{
  "mcpServers": {
    "ferramentas-do-time": {
      "command": "uv",
      "args": [
        "--directory",
        "/caminho/absoluto/para/meu-mcp",
        "run",
        "servidor.py"
      ]
    }
  }
}

Use o caminho absoluto da pasta (roda pwd que ele te dá). Salva, reinicia o Claude for Desktop, e pronto: peça "consulta o CEP 01310-100" e veja o agente chamar a sua tool. Detalhe pra quem está no Linux como eu: o Claude for Desktop ainda não tem build pra Linux, então o Inspector do passo 5 é o seu caminho de teste — ou plugue num cliente que rode no seu SO.

Tool ou resource: qual usar?

Essa é a decisão que separa quem entendeu MCP de quem só copiou o exemplo.

Pergunte: o modelo precisa decidir chamar isso, ou é contexto que a aplicação já sabe que quer entregar?

Se é uma ação com efeito — consultar uma API, gravar no banco, abrir um ticket — é tool. O modelo controla, e por isso tool geralmente passa por aprovação do usuário antes de executar.

Se é um dado estável que serve de pano de fundo — a documentação do projeto, o schema do banco, os padrões do time — é resource. A aplicação controla quando e como injeta. Não tem efeito colateral, então é seguro ler à vontade.

O erro clássico do iniciante é transformar tudo em tool. Aí o modelo fica chamando função pra ler dado parado, gastando turno e abrindo brecha de erro onde não precisava. Contexto read-only é resource. Ponto.

Limitações e pontos de atenção

Antes de levar isso a sério, três armadilhas:

  • Nunca escreva em stdout num servidor stdio. Aquele print() de debug corrompe as mensagens JSON-RPC e quebra o servidor inteiro, do nada. Logue em stderr (print(..., file=sys.stderr)) ou em arquivo. É o bug número um de quem está começando.
  • Tool é superfície de ataque. Se sua tool roda SQL, deleta arquivo ou bate numa API com credencial, o modelo agora tem esse poder. Valide entrada, limite escopo, e trate aprovação do usuário como parte do design — não como detalhe. E quando esse servidor sair do seu laptop pra valer, o jogo muda: é o que MCP em produção exige — OAuth, schema validado e gateway no meio.
  • Descrição é contrato. Modelo não lê seu código, lê a docstring e os nomes. Tool mal descrita é tool chamada na hora errada, com argumento errado. Capricha nessa parte como se fosse a API pública que é.

FAQ rápido

Preciso saber async pra escrever um MCP server? Pra tool que faz I/O (HTTP, banco), async def é o caminho natural e o FastMCP lida com isso. Mas tool e resource síncronos também funcionam — o padrao_do_time do exemplo é def normal.

Funciona só com Claude? Não. O M de MCP é de protocolo aberto. O mesmo servidor pluga em qualquer cliente que fale MCP — Cursor, Zed, sua própria aplicação via SDK. Foi esse o ponto desde o começo.

Posso usar TypeScript em vez de Python? Pode. O SDK oficial tem versões em TypeScript, Java, C#, Kotlin e Ruby, todas com o mesmo conceito de tools e resources. Escolhi Python aqui pela curva mais curta.

Como debugo quando o modelo não chama minha tool? Quase sempre é descrição. Abra o MCP Inspector (uv run mcp dev), confirme que a tool aparece com a descrição certa, e reescreva a docstring deixando explícito quando usar.

O próximo passo

Você saiu do "consumo MCP dos outros" pro "exponho o meu". Escreveu um servidor com uma tool que age e um resource que dá contexto, testou no Inspector e plugou no Claude. Isso é a base de tudo: um agente interno da sua empresa não é outra coisa senão um modelo com MCP servers bem desenhados em volta.

E é aí que o jogo muda. A parte difícil não é o decorator — é decidir o que vira tool, o que vira resource, onde mora a credencial, como o agente erra com segurança. Isso é arquitetura, não prompt bonito. Se você quer ver esse tipo de decisão sendo tomada com código rodando e dor de produção na mesa, é o que a gente destrincha no Workshop Arquitetando Soluções de IA, um workshop prático de como montar soluções de software com agents de IA.

Agora pega o servidor.py, troca a ViaCEP pela sua API interna e o padroes:// pela doc do seu projeto. O esqueleto é o mesmo. O que muda é o problema que ele resolve.

Lucas Souza
Lucas Souza

{AI Engineer} — apaixonado por Laravel, arquitetura de software e construir produtos com impacto. Compartilho aqui tutoriais, descobertas e reflexões sobre o dia a dia de engenharia.

Você também pode gostar

VirguIA

beer & code assistant

conectando…

Não foi possível iniciar o chat agora.

tocando