Você já tem uma API, um banco ou um script interno que resolve um problema real. Agora quer que um assistente de IA, como o Claude, use isso sem copiar e colar respostas no chat. O caminho padrão hoje é expor esse sistema como um servidor MCP. Em Python, a forma mais curta de fazer isso é o FastMCP: você escreve funções comuns, coloca um decorador e o protocolo cuida do resto.
Neste tutorial você vai criar um servidor MCP com FastMCP do zero, com as três peças que o protocolo oferece: ferramentas, recursos e prompts. Todo o código foi executado com FastMCP 4.1 e Python 3.11, e a saída real aparece mais abaixo.
O que você vai construir
Um servidor para o catálogo de uma biblioteca, pequeno o bastante para caber no artigo e completo o bastante para mostrar cada conceito:
- Ferramentas (tools): ações que o modelo pode chamar. Aqui,
buscar_livrosereservar_livro. - Recursos (resources): dados que o cliente pode ler por um endereço, como um arquivo. Aqui, as regras de empréstimo e a ficha de cada livro.
- Prompts: modelos de mensagem prontos que o usuário escolhe no cliente. Aqui, um prompt que pede uma recomendação de leitura.
A diferença importa: uma ferramenta é decidida pelo modelo, um recurso é escolhido pela aplicação e um prompt é escolhido pelo usuário.
Instalando o FastMCP
Crie um ambiente e instale o pacote:
uv venv && source .venv/bin/activate
uv pip install fastmcp
Com pip funciona igual: pip install fastmcp.
O servidor completo
Salve como server.py:
"""Servidor MCP de exemplo: catálogo de livros de uma biblioteca."""
from typing import Annotated
from fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP("biblioteca")
LIVROS = {
"978-8575228050": {"titulo": "Python Fluente", "autor": "Luciano Ramalho", "disponivel": True},
"978-8550804606": {"titulo": "Código Limpo", "autor": "Robert C. Martin", "disponivel": False},
}
RESERVAS: set[str] = set()
@mcp.tool(annotations={"readOnlyHint": True})
def buscar_livros(
termo: Annotated[str, Field(description="Parte do título ou do autor", min_length=2)],
so_disponiveis: bool = False,
) -> list[dict]:
"""Busca livros do catálogo pelo título ou pelo autor."""
termo = termo.casefold()
return [
{"isbn": isbn, **livro}
for isbn, livro in LIVROS.items()
if termo in (livro["titulo"] + " " + livro["autor"]).casefold()
and (livro["disponivel"] or not so_disponiveis)
]
@mcp.tool(annotations={"destructiveHint": False, "idempotentHint": True})
def reservar_livro(isbn: str) -> dict:
"""Reserva um livro disponível. Reservar de novo o mesmo livro não muda nada."""
livro = LIVROS.get(isbn)
if livro is None:
raise ValueError(f"ISBN {isbn} não existe no catálogo")
if isbn in RESERVAS:
return {"isbn": isbn, "status": "reservado"}
if not livro["disponivel"]:
raise ValueError(f"'{livro['titulo']}' já está emprestado")
livro["disponivel"] = False
RESERVAS.add(isbn)
return {"isbn": isbn, "status": "reservado"}
@mcp.resource("biblioteca://regras")
def regras() -> str:
"""Regras de empréstimo da biblioteca."""
return "Prazo de 14 dias. Até 3 livros por pessoa. Renovação online uma vez."
@mcp.resource("biblioteca://livros/{isbn}")
def ficha_do_livro(isbn: str) -> dict:
"""Ficha completa de um livro pelo ISBN."""
return {"isbn": isbn, **LIVROS[isbn]}
@mcp.prompt
def recomendar(tema: str, nivel: str = "iniciante") -> str:
"""Pede ao modelo uma recomendação de leitura usando o catálogo."""
return (
f"Use a ferramenta buscar_livros para encontrar livros sobre {tema} "
f"para um leitor {nivel}. Recomende no máximo 3 e explique o porquê."
)
if __name__ == "__main__":
mcp.run()
São menos de 70 linhas, e nenhuma delas fala de JSON-RPC, de mensagens ou de transporte. Vamos ver o que cada parte faz.
Ferramentas: a assinatura vira o contrato
O FastMCP lê a assinatura e a docstring da função e gera, sozinho, o que o modelo vai receber: o nome, a descrição e um JSON Schema dos argumentos. Para buscar_livros, o schema gerado é este:
{
"type": "object",
"additionalProperties": false,
"properties": {
"termo": {"description": "Parte do título ou do autor", "minLength": 2, "type": "string"},
"so_disponiveis": {"default": false, "type": "boolean"}
},
"required": ["termo"]
}
Três detalhes desse exemplo valem para qualquer servidor:
- A docstring é o manual do modelo. O modelo decide se chama a ferramenta lendo essa frase. Escreva o que ela faz e quando usar, não como foi implementada.
- Valide na fronteira. O
Field(min_length=2)viraminLengthno schema, e o FastMCP recusa uma chamada que não respeita o schema antes de chegar na sua função. - Anotações dizem o risco.
readOnlyHintavisa o cliente que a ferramenta só lê;idempotentHintavisa que repetir a chamada é seguro. Clientes usam isso para decidir quando pedir confirmação ao usuário. Por isso oreservar_livrodevolve o mesmo resultado na segunda chamada em vez de falhar: a anotação promete isso, e o código precisa cumprir.
Quando algo dá errado, levante uma exceção com uma mensagem útil. O FastMCP converte em um resultado com isError: true e o texto da mensagem, que o modelo lê e pode usar para corrigir a próxima tentativa. Chamar reservar_livro com um ISBN inexistente devolve exatamente ISBN 000 não existe no catálogo.
Recursos: dados com endereço
Um recurso é lido, não executado. O @mcp.resource("biblioteca://regras") publica um texto fixo; o biblioteca://livros/{isbn} é um modelo de recurso: o trecho entre chaves vira o argumento da função. Use recursos para contexto que a aplicação anexa à conversa, como documentação, configuração ou um registro específico, e deixe as ferramentas para ações e buscas.
Prompts: atalhos para o usuário
O @mcp.prompt publica um modelo de mensagem com argumentos. Em clientes que suportam prompts, ele aparece como um comando que o usuário escolhe, e o texto gerado já orienta o modelo a usar as ferramentas certas. É uma boa forma de distribuir o "jeito certo de pedir" junto com o servidor.
Testando sem um assistente

Você não precisa de um LLM para testar o servidor. O FastMCP traz um cliente em Python. Salve como cliente.py:
import asyncio
from fastmcp import Client
async def main():
async with Client("server.py") as client:
tools = await client.list_tools()
print("ferramentas:", [t.name for t in tools])
resultado = await client.call_tool("buscar_livros", {"termo": "python"})
print("buscar_livros:", resultado.data)
regras = await client.read_resource("biblioteca://regras")
print("regras:", regras[0].text)
prompt = await client.get_prompt("recomendar", {"tema": "testes"})
print("prompt:", prompt.messages[0].content.text)
asyncio.run(main())
Passar o caminho server.py faz o cliente iniciar o servidor como subprocesso e conversar com ele por stdio. A linha de comando do FastMCP também ajuda: fastmcp list server.py mostra as ferramentas e fastmcp call server.py buscar_livros termo=código chama uma delas. Esta é a saída real dos dois comandos:
Para explorar visualmente, fastmcp dev inspector server.py abre o MCP Inspector no navegador. Esses mesmos testes cabem no seu pytest: o cliente em memória deixa você testar o servidor como testa qualquer outra função.
Rodando por stdio ou por HTTP
O mcp.run() sem argumentos usa stdio: o cliente inicia o processo na máquina do usuário. É o modo certo para ferramentas locais, como acesso a arquivos ou a um repositório.
Para um serviço compartilhado, use HTTP:
mcp.run(transport="http", host="0.0.0.0", port=8000)
O endpoint fica em http://<host>:8000/mcp e o mesmo Client conecta com Client("http://localhost:8000/mcp"). Em produção, coloque autenticação na frente (um token por usuário ou OAuth) e trate cada ferramenta como um endpoint público: valide tudo, registre as chamadas e dê a cada token só a permissão de que ele precisa.
Conectando ao Claude
No Claude Code, um comando registra o servidor:
# servidor local, por stdio
claude mcp add biblioteca -- python /caminho/para/server.py
# servidor remoto, por HTTP
claude mcp add --transport http biblioteca https://mcp.exemplo.com/mcp
A partir daí, pedir "tem algum livro de Python disponível? Se tiver, reserve" faz o Claude chamar buscar_livros, ler o resultado e, se fizer sentido, chamar reservar_livro.
Boas práticas para desenhar a API das ferramentas
- Poucas ferramentas, bem nomeadas. Prefira
buscar_livrosereservar_livroa umexecutar_acao(tipo, payload). O modelo escolhe melhor quando o nome já diz a intenção. - Argumentos explícitos e tipados. Enums, limites e descrições no schema evitam chamadas erradas antes que elas aconteçam.
- Respostas pequenas e estruturadas. Devolva o que o modelo precisa para o próximo passo, não a tabela inteira. Pagine listas grandes.
- Erros que ensinam. "ISBN não existe" ajuda o modelo a se corrigir; um stack trace não ajuda.
- Escrita idempotente e marcada. Se uma ferramenta muda dados, marque com as anotações e faça a repetição ser segura.
- Separe leitura de escrita. Ferramentas só de leitura podem rodar sem confirmação; as de escrita merecem atenção do usuário.
Próximos passos
Troque o dicionário LIVROS pela sua API ou pelo seu banco, mantenha as mesmas regras de desenho e você terá um servidor MCP útil em uma tarde. O protocolo é o mesmo para qualquer cliente compatível, então o trabalho vale para o Claude, para IDEs e para os agentes que você mesmo escrever.

