Servidor MCP em Python com FastMCP: tools, resources e prompts

Tutorial com código testado: crie um servidor MCP em Python com FastMCP, com ferramentas, recursos e prompts, teste sem LLM e conecte ao Claude.

Avatar de Danrley Pereira
Danrley Pereira
Servidor MCP em Python com FastMCP: tools, resources e prompts

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_livros e reservar_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) vira minLength no 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. readOnlyHint avisa o cliente que a ferramenta só lê; idempotentHint avisa que repetir a chamada é seguro. Clientes usam isso para decidir quando pedir confirmação ao usuário. Por isso o reservar_livro devolve 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

Saída real de fastmcp list e do cliente Python chamando ferramentas, recurso e prompt do servidor MCP da biblioteca
Saída real de fastmcp list e do cliente Python chamando ferramentas, recurso e prompt do servidor MCP da biblioteca

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_livros e reservar_livro a um executar_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.

Gostou do artigo?

Compartilhe com seus amigos e ajude a espalhar conhecimento!