Construindo um Servidor MCP com o SDK Python
O SDK oficial Python do MCP oferece um pequeno conjunto de decoradores para registrar ferramentas, recursos e prompts.
Todo o resto, como a conexão, negociação de capacidades e roteamento de requisições, é tratado para você.
Resumo
Um servidor MCP em Python é construído em torno do FastMCP, a classe de servidor de alto nível do SDK.
Você registra capacidades com três decoradores: @mcp.tool(), @mcp.resource() e @mcp.prompt().
As dicas de tipo em cada função servem como o esquema de entrada que o cliente vê durante a negociação de capacidades.
Erros levantados dentro de um manipulador são convertidos em respostas de erro estruturadas em vez de travar o servidor.
Esta página se baseia no esqueleto mínimo da página de noções básicas e aprofunda em argumentos tipados, recursos parametrizados e tratamento de erros.
Receita
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("tickets-server")
@mcp.tool()
def create_ticket(title: str, priority: str = "normal") -> str:
"""Cria um ticket de suporte e retorna seu ID."""
return "T-1042"
@mcp.resource("tickets://{ticket_id}")
def get_ticket(ticket_id: str) -> str:
"""Busca um único ticket por ID."""
return f"Ticket {ticket_id}: aberto"
@mcp.prompt()
def triage_prompt(ticket_id: str) -> str:
return f"Revise o ticket {ticket_id} e recomende um próximo passo."
if __name__ == "__main__":
mcp.run(transport="stdio")Quando usar isso:
- Você deseja expor uma API interna, banco de dados ou sistema de arquivos para o Claude como ferramentas chamáveis.
- Você precisa de dados somente leitura (configuração, documentação, registros) disponíveis para um cliente sem que isso conte como uma "ação".
- Você deseja modelos de prompt reutilizáveis compartilhados entre todos os clientes que se conectam a este servidor.
- Você está construindo primeiro para desenvolvimento local;
stdioé o transporte correto antes de adicionar uma camada de rede.
Exemplo de Trabalho
from dataclasses import dataclass
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("tickets-server")
@dataclass
class Ticket:
id: str
title: str
priority: str
status: str
_tickets: dict[str, Ticket] = {
"T-1": Ticket(id="T-1", title="Impressora offline", priority="low", status="open"),
"T-2": Ticket(id="T-2", title="Queda de VPN", priority="high", status="closed"),
}
_next_id = 3
@mcp.tool()
def create_ticket(title: str, priority: str = "normal") -> str:
"""Cria um novo ticket de suporte e retorna seu ID.
priority deve ser um de: low, normal, high.
"""
global _next_id
if priority not in ("low", "normal", "high"):
raise ValueError(f"prioridade inválida '{priority}', esperado low, normal ou high")
ticket_id = f"T-{_next_id}"
_next_id += 1
_tickets[ticket_id] = Ticket(id=ticket_id, title=title, priority=priority, status="open")
return ticket_id
@mcp.tool()
def close_ticket(ticket_id: str) -> str:
"""Marca um ticket como fechado."""
ticket = _tickets.get(ticket_id)
if ticket is None:
raise ValueError(f"nenhum ticket com id '{ticket_id}'")
ticket.status = "closed"
return f"{ticket_id} fechado"
@mcp.resource("tickets://{ticket_id}")
def get_ticket(ticket_id: str) -> str:
"""Busca os detalhes de um único ticket por ID."""
ticket = _tickets.get(ticket_id)
if ticket is None:
raise ValueError(f"nenhum ticket com id '{ticket_id}'")
return f"{ticket.id} [{ticket.priority}] {ticket.title} - {ticket.status}"
@mcp.resource("tickets://open")
def list_open_tickets() -> str:
"""Lista todos os tickets abertos."""
open_ids = [t.id for t in _tickets.values() if t.status == "open"]
return "\n".join(open_ids) if open_ids else "nenhum ticket aberto"
@mcp.prompt()
def triage_prompt(ticket_id: str) -> str:
"""Constrói um prompt pedindo uma recomendação de triagem para um ticket."""
return f"Revise o ticket {ticket_id} e recomende se deve ser escalado ou fechado."
if __name__ == "__main__":
mcp.run(transport="stdio")O que isso demonstra:
- Duas ferramentas,
create_ticketeclose_ticket, que modificam o estado do lado do servidor e validam sua própria entrada antes de fazê-lo. - Um recurso parametrizado,
tickets://{ticket_id}, ao lado de um recurso fixo,tickets://open, mostrando ambos os estilos. - Erros de validação levantados como
ValueErrorsimples, que o SDK transforma em uma resposta de erro estruturada em vez de travar o processo. - Um modelo de prompt que referencia um ID de ticket, mantido intencionalmente simples, pois os prompts devem permanecer declarativos.
Mergulho Profundo
Como Funciona
FastMCP("tickets-server")cria uma instância de servidor e imediatamente conecta as fases de conexão e negociação do ciclo de vida da requisição.- Cada chamada
@mcp.tool()registra a função decorada na tabela de ferramentas interna do servidor, com chave pelo nome da função, a menos que você passe um nome explícito. - As dicas de tipo e a docstring da função são inspecionadas no momento do registro para construir o esquema JSON enviado aos clientes durante a negociação de capacidades.
- Quando um cliente chama uma ferramenta, o SDK valida os argumentos recebidos contra esse esquema antes que o corpo da sua função seja executado.
- Recursos seguem o mesmo padrão de registro, mas são endereçados por URI; um
{placeholder}no modelo de URI se torna um parâmetro passado para sua função.
Tratamento de Erros
Levantar uma exceção Python padrão dentro de qualquer manipulador é a maneira correta de sinalizar falha.
@mcp.tool()
def get_priority_multiplier(priority: str) -> float:
multipliers = {"low": 0.5, "normal": 1.0, "high": 2.0}
if priority not in multipliers:
raise ValueError(f"prioridade desconhecida '{priority}'")
return multipliers[priority]O SDK captura a exceção e a transforma em um resultado de erro estruturado que o cliente pode exibir ou raciocinar sobre, em vez de deixar que ela trave o processo do servidor.
Prefira levantar cedo com uma mensagem clara em vez de retornar uma string como "erro: entrada inválida", que o cliente trataria como um resultado bem-sucedido.
Ferramentas vs. Recursos vs. Prompts em Resumo
| Capacidade | Propósito | Tem Efeitos Colaterais | Exemplo |
|---|---|---|---|
| Ferramenta | Realizar uma ação | Frequentemente sim | create_ticket, send_email |
| Recurso | Ler dados por URI | Não | tickets://open, config://settings |
| Prompt | Modelo de prompt reutilizável | Não | triage_prompt |
Notas Python
# Use dicas de tipo precisas, elas se tornam o esquema que o cliente valida.
@mcp.tool()
def set_priority(ticket_id: str, priority: str) -> str:
...
# Argumentos opcionais com padrões são refletidos como opcionais no esquema.
@mcp.tool()
def search_tickets(query: str, limit: int = 10) -> list[str]:
...Dicas de tipo precisas importam mais aqui do que em código interno típico, pois se tornam parte de um contrato do qual um cliente depende, não apenas documentação para outro desenvolvedor.
Parâmetros e Valores de Retorno
| Decorador | Registra | Convenção de Tipo de Retorno |
|---|---|---|
@mcp.tool() | Uma ação chamável | Qualquer valor serializável em JSON, ou um resumo em string |
@mcp.resource(uri) | Conteúdo somente leitura em um URI | Uma string ou bloco de conteúdo estruturado |
@mcp.prompt() | Um modelo de prompt reutilizável | Uma string, ou uma lista de objetos de mensagem para modelos de múltiplos turnos |
Armadilhas
- Dicas de tipo vagas produzem um esquema inútil. Um parâmetro tipado como
stronde na verdade é um conjunto fixo de valores (comopriority) não dá ao cliente nenhuma dica sobre entradas válidas. Correção: valide contra um conjunto explícito dentro da função e levante umValueErrorclaro, ou use uma dica de tipoLiteralonde o SDK a suporta. - Retornar um erro em string em vez de levantar. Retornar
"erro: não encontrado"de uma ferramenta parece um resultado bem-sucedido para o cliente. Correção: levante uma exceção; deixe o SDK convertê-la em uma resposta de erro adequada. - Modificar estado compartilhado sem proteção para chamadas concorrentes. Um dicionário no nível do módulo como
_ticketspode ser acessado por requisições sobrepostas se o servidor lidar com mais de uma sessão. Correção: use um banco de dados ou outro armazenamento seguro contra concorrência para o estado real assim que você passar da prototipagem local. - Esquecer a docstring. Uma ferramenta sem docstring é registrada corretamente, mas não fornece nada para o Claude raciocinar ao decidir se deve chamá-la. Correção: sempre escreva uma docstring de uma linha descrevendo o que a ferramenta faz e quaisquer restrições em seus argumentos.
- Renomear os argumentos de uma ferramenta em uma versão posterior. Clientes existentes que chamam a ferramenta com os nomes de argumento antigos começarão a falhar na validação. Correção: trate os nomes dos argumentos como parte do seu contrato público e consulte estratégias de versionamento de esquema antes de alterá-los.
- Bloquear I/O dentro de um manipulador. Uma ferramenta que faz uma chamada de rede síncrona lenta prende o servidor para essa requisição. Correção: use
async defpara funções de ferramenta que realizam I/O; o SDK suporta manipuladores assíncronos diretamente.
Alternativas
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| SDK TypeScript MCP | Seu serviço ou equipe existente é baseado em TypeScript/Node | Você deseja reutilizar uma pilha Python de dados ou ML existente |
Classes de protocolo MCP de baixo nível (sem FastMCP) | Você precisa de controle granular sobre o protocolo não exposto pela API de alto nível | Você só precisa de ferramentas, recursos e prompts padrão; FastMCP cobre isso |
| Uma API REST simples (sem MCP) | O consumidor não é um cliente LLM e não precisa de negociação de capacidades | Você quer que o Claude descubra e chame sua funcionalidade dinamicamente |
FAQs
Eu tenho que usar FastMCP, ou posso usar o protocolo diretamente?
FastMCP é a API de alto nível recomendada e cobre a grande maioria dos casos de uso. O SDK também expõe classes de protocolo de nível inferior para cenários avançados que necessitam de controle mais fino, mas a maioria dos servidores nunca precisa delas.
Como o SDK sabe quais argumentos uma ferramenta espera?
- Ele inspeciona as dicas de tipo da função no momento do registro.
- Essas dicas são convertidas em um esquema JSON enviado aos clientes durante a negociação de capacidades.
- O SDK valida os argumentos recebidos contra esse esquema antes que sua função seja executada.
O que acontece se minha função de ferramenta levantar uma exceção?
@mcp.tool()
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("não é possível dividir por zero")
return a / bO SDK captura a exceção e retorna um erro estruturado para o cliente em vez de travar o processo do servidor.
Um recurso pode aceitar parâmetros como uma ferramenta?
Sim. Um modelo de URI com um {placeholder}, como tickets://{ticket_id}, passa esse segmento para sua função como um argumento, da mesma forma que um argumento de ferramenta funciona.
Devo usar uma ferramenta ou um recurso para dados somente leitura?
Prefira um recurso. Ferramentas implicam uma ação que o cliente escolhe invocar, enquanto recursos são destinados a conteúdo que o cliente lê diretamente. Usar uma ferramenta para recuperação pura de dados funciona, mas é uma incompatibilidade semântica que pode confundir como um cliente apresenta a capacidade.
Os manipuladores de ferramentas podem ser assíncronos?
Sim, async def é suportado diretamente pelo decorador. Use-o para qualquer manipulador que faça I/O de rede ou de arquivo, para que uma chamada lenta não bloqueie outras requisições no mesmo servidor.
Como testar um servidor sem um cliente real?
Escreva testes unitários que chamem suas funções manipuladoras diretamente e, separadamente, exercite-as através de um cliente mock que fale o protocolo. Veja a página dedicada sobre testes de servidores MCP para o padrão completo.
Qual é a diferença entre a docstring de uma ferramenta e sua descrição mostrada ao Claude?
A docstring é a descrição. O SDK a lê diretamente da função e a inclui no manifesto de capacidades, então Claude vê exatamente o que você escreveu como comentário de documentação.
Um servidor pode registrar ferramentas síncronas e assíncronas?
Sim, o SDK lida com ambos. Escolha com base se o manipulador individual faz trabalho bloqueante, não com base em uma regra de servidor.
Existe um limite para quantas ferramentas um servidor pode registrar?
O SDK em si não impõe um limite fixo, mas um servidor com dezenas de ferramentas vagamente relacionadas se torna difícil de raciocinar tanto para Claude quanto para os mantenedores humanos. Agrupe capacidades relacionadas em servidores focados em vez de um único servidor "catch-all".
Como altero os argumentos de uma ferramenta sem quebrar clientes existentes?
Adicione novos argumentos como opcionais com padrões sensatos em vez de renomear ou remover os existentes. Para mudanças maiores, consulte a página dedicada sobre versionamento de esquemas de ferramentas.
O FastMCP requer uma versão específica do Python?
O SDK tem como alvo Python moderno (mínimo 3.10+) para suportar os recursos de dica de tipo dos quais ele depende para a geração de esquemas. Verifique os requisitos publicados do SDK para o mínimo exato no momento da instalação.
Relacionados
- Noções Básicas de Servidores MCP - o esqueleto mínimo no qual este artigo se baseia
- Como Servidores MCP Lidam com Requisições - o ciclo de vida no qual esses manipuladores se integram
- Construindo um Servidor MCP com o SDK TypeScript - os mesmos padrões em TypeScript
- Testando Servidores MCP Antes da Implantação - exercitando esses manipuladores com um cliente mock
- Referência de Versionamento e Limitação de Taxa de Esquemas de Ferramentas MCP - mantendo as alterações de ferramentas compatíveis com versões anteriores
Versões de Stack: Escrito contra a linha de modelos Claude atual em ~junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão) e Claude Haiku 4.5 - e os atuais SDKs do Model Context Protocol Python/TypeScript. Nomes de modelos, versões de SDK e a especificação MCP mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs e modelcontextprotocol.io antes de confiar neles.