Como criar um servidor MCP do zero em Python, passo a passo

Um servidor MCP em Python que funciona, de uma pasta vazia até o Claude Desktop. Escreva ferramentas, recursos e prompts com o SDK oficial 2.x, teste-os no Inspector, adicione uma ferramenta real de imagem com chamadas assíncronas à API e publique tudo via Streamable HTTP.

Como criar um servidor MCP do zero em Python, passo a passo
Cristian Da Conceicao
Fundador do Picasso IA

A maioria dos tutoriais de MCP para numa ferramenta de clima que devolve uma string fixa. Este aqui cria um servidor que você realmente pode manter. Você vai escrevê-lo do zero com o SDK oficial de Python, testá-lo sem nenhum cliente de IA, conectá-lo ao Claude Desktop e ao Claude Code, e terminar com uma ferramenta real que chama uma API de imagens e espera o resultado. Tudo abaixo exige Python 3.10 ou mais recente e é compatível com o MCP Python SDK 2.x (2.3.0 no PyPI no momento da escrita, outubro de 2026).

Se você copiou código de um tutorial de 2025 e deu de cara com ModuleNotFoundError: No module named 'mcp.server.fastmcp', está no lugar certo. A classe principal foi renomeada, e a seção de configuração mostra a correção em uma linha.

O que um servidor MCP realmente faz

O Model Context Protocol (MCP) é um padrão para que um aplicativo de IA chame o seu código. Três papéis importam. O host é o aplicativo com que a pessoa conversa, como o Claude Desktop ou uma IDE. O cliente fica dentro do host e fala o protocolo. O servidor é a parte que você constrói. Seu servidor nunca conversa diretamente com o modelo; ele apenas responde às solicitações de um cliente.

Três primitivas, três donos

Um servidor expõe exatamente três tipos de capacidade, e o que os diferencia é quem decide usá-los:

PrimitivaQuem acionaO que éExemplo
FerramentaO modeloUma função que executa uma açãoGerar uma imagem, escrever uma linha em um banco de dados
RecursoA aplicaçãoDados carregados no contexto do modeloUm arquivo, uma configuração, um catálogo
PromptO usuárioUm modelo de mensagem reutilizávelUm comando de barra

Se você já construiu uma API web, a correspondência é rápida. Um recurso se comporta como um GET, uma ferramenta se comporta como um POST, e um prompt é uma consulta salva que o usuário executa pelo nome.

Esboço à mão em um caderno de três blocos conectados sobre uma mesa de carvalho

💡 Regra prática: se o modelo deve decidir quando executá-lo, faça dele uma ferramenta. Se a aplicação deve anexá-lo, faça dele um recurso. Se uma pessoa deve escolhê-lo em um menu, faça dele um prompt.

Escolha o transporte logo no início

O transporte é a forma como os bytes se movem entre cliente e servidor. Você o escolhe com um argumento para mcp.run().

TransporteComo funcionaUse para
stdioO host inicia seu arquivo como subprocesso e se comunica pela entrada e saída padrão deleServidores locais, e é o padrão
streamable-httpUm servidor HTTP real em uma porta, com o endpoint em /mcpQualquer coisa que você publique
sseO transporte HTTP mais antigoNada novo; foi substituído na revisão do protocolo de 2025-03-26

Cabos de rede ligados a um switch em um pequeno armário de servidores

Comece com stdio. Você vai migrar para Streamable HTTP perto do fim, e o código das ferramentas permanece exatamente o mesmo.

Configure o Python em cinco minutos

Mãos de um desenvolvedor digitando em um editor de código em um notebook prateado

Instale o uv e o SDK

Você precisa de Python 3.10 ou mais recente e do uv. Crie um projeto e adicione o SDK:

uv init mcp-image-studio
cd mcp-image-studio
uv add "mcp[cli]" httpx

O extra cli instala o comando mcp com mcp dev, mcp run e mcp install. Um simples pip install "mcp[cli]" também funciona. O Inspector é um aplicativo Node.js, então npx precisa estar no seu PATH.

Uma renomeação que quebra código antigo

No SDK 1.x, a classe de alto nível se chamava FastMCP. Na 2.x, ela é MCPServer, e fica em um módulo diferente:

# SDK 1.x, seen in older tutorials
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")

# SDK 2.x, used in this article
from mcp.server import MCPServer
mcp = MCPServer("Demo")

Outra mudança pega as pessoas de surpresa: configurações de transporte como port saíram do construtor e foram para run(). Passar port= para MCPServer(...) gera um TypeError.

Escreva seu primeiro servidor

Crie server.py. Um arquivo, três decoradores, e todas as primitivas ficam registradas:

from mcp.server import MCPServer

mcp = MCPServer("Demo")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"


@mcp.prompt()
def summarize(text: str) -> str:
    """Summarize a piece of text in one sentence."""
    return f"Summarize the following text in one sentence:\n\n{text}"


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

Isso já é um servidor funcionando. A proteção if __name__ é importante: mcp dev, mcp run, mcp install e seus testes importam este arquivo, e um run() sem proteção iniciaria um servidor no momento em que qualquer coisa o carregasse.

Adicione uma ferramenta

O SDK lê três coisas da sua função. O nome vira o nome da ferramenta, a docstring vira a descrição que o modelo enxerga, e as dicas de tipo viram o esquema dos argumentos. Você não precisa escrever nenhum JSON Schema, porque a: int, b: int é o esquema. Se um cliente enviar uma string onde você declarou um inteiro, o SDK rejeita a chamada antes de a sua função rodar.

Dê um valor padrão a um parâmetro e ele se torna opcional. Para limites mais rígidos, envolva o tipo em Annotated com um Field do Pydantic:

from typing import Annotated, Literal
from pydantic import Field


@mcp.tool()
def search_books(
    query: str,
    limit: Annotated[int, Field(ge=1, le=50, description="Maximum results")] = 10,
    genre: Literal["fiction", "non-fiction", "poetry"] = "fiction",
) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r} (up to {limit})."

Os limites aparecem no esquema como minimum e maximum, e o Literal se torna um enum do qual o modelo precisa escolher.

Adicione um recurso e um prompt

Um {param} em uma URI de recurso o transforma em um modelo de recurso, então greeting://{name} não tem uma entrada única para listar até que alguém forneça um nome. Um prompt é ainda mais simples: a string que ele devolve vira uma mensagem do usuário. Os dois leem suas descrições da docstring, assim como as ferramentas.

Lance erros que o modelo consiga ler

Quando uma ferramenta falha, lance ToolError. Nunca devolva uma string de erro, porque uma string devolvida tem is_error=False e parece uma resposta bem-sucedida.

from mcp.server.mcpserver.exceptions import ToolError

CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}


@mcp.tool()
def get_author(title: str) -> str:
    """Look up the author of a book in the catalog."""
    if title not in CATALOG:
        raise ToolError(f"No book titled {title!r} in the catalog.")
    return CATALOG[title]

O modelo lê essa mensagem, percebe que adivinhou o título errado e chama de novo com um melhor. Um único raise dá a você um agente que se autocorrige. Qualquer outra exceção conta como falha: o modelo só vê que a chamada falhou, e o seu log recebe o traceback.

💡 Declare uma ferramenta async def sempre que ela fizer I/O, como uma chamada de API, uma leitura de arquivo ou uma consulta a banco de dados. Use def simples para todo o resto.

Teste e conecte

Execute o MCP Inspector

Antes de qualquer cliente de IA tocar no seu servidor, execute-o no Inspector:

uv run mcp dev server.py

Abra a URL que ele imprime. O Inspector inicia server.py como subprocesso via stdio, exatamente como um host real faria. Percorra as abas nesta ordem:

  • Tools: add aparece com um formulário montado a partir das suas dicas de tipo. Chame-a com a=1 e b=2 e você recebe 3.
  • Resources: a lista está vazia, e greeting aparece em Resource Templates. Informe World e você lê Hello, World!.
  • Prompts: summarize tem um argumento obrigatório text e devolve uma única mensagem do usuário.

Dois engenheiros revisando código em um notebook em uma mesa de pé

Escreva um teste em memória

A classe Client do SDK também se conecta em memória: entregue a ela o objeto do servidor e não há subprocesso nem porta. Adicione pytest com uv add --dev pytest, depois crie test_server.py:

import pytest
from mcp import Client

from server import mcp


@pytest.fixture
def anyio_backend():
    return "asyncio"


@pytest.mark.anyio
async def test_add():
    async with Client(mcp, raise_exceptions=True) as client:
        result = await client.call_tool("add", {"a": 1, "b": 2})
        assert result.structured_content == {"result": 3}

Mantenha raise_exceptions=True apenas nos testes. Ele torna visível a mensagem de erro real, em vez da Internal server error higienizada que um chamador remoto veria.

Conecte o Claude Desktop e o Claude Code

Todo host precisa da mesma coisa: o comando que inicia o seu servidor. Este funciona a partir de qualquer diretório, sem ambiente virtual para ativar:

uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

O Claude Desktop é o único host que o SDK consegue configurar para você:

uv run mcp install server.py

Isso grava uma entrada em claude_desktop_config.json, que fica em ~/Library/Application Support/Claude/ no macOS e em %APPDATA%\Claude\ no Windows. Feche o Claude Desktop por completo, não apenas a janela, e depois abra-o de novo. O aplicativo inicia seu servidor com o próprio ambiente, então passe segredos com -v NAME=value ou -f .env.

O Claude Code não precisa de arquivo nenhum. Registre o servidor com a CLI e depois execute /mcp dentro de uma sessão para confirmar que ele está conectado:

claude mcp add image-studio -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

O Cursor lê .cursor/mcp.json no campo mcpServers, e o VS Code lê .vscode/mcp.json em servers com "type": "stdio". O comando dentro dele é idêntico.

Notebook mostrando uma janela de chat ao lado de um terminal sobre uma mesa de madeira

Corrija um servidor que não aparece

Primeiro, execute você mesmo o comando de inicialização. Um servidor stdio saudável não imprime nada e espera que um host fale primeiro. Um traceback ou uma saída imediata é o seu bug real. Se ele espera em silêncio, verifique estas três causas:

SintomaCausaCorreção
O servidor nunca iniciaUm caminho relativo, porque o host inicia a partir do próprio diretório de trabalhoUse caminhos absolutos, incluindo o de uv (where uv no Windows, which uv nos demais)
As edições não têm efeitoOs hosts leem a configuração na inicializaçãoFeche o host por completo e abra-o de novo
A conexão cai logo de inícioAlgo escreveu no stdout, que é o canal do protocoloRegistre logs com o módulo logging, que escreve no stderr, e nunca dependa de print()

O Claude Desktop mantém um log por servidor, chamado mcp-server-<NAME>.log, em ~/Library/Logs/Claude no macOS e em %APPDATA%\Claude\logs no Windows. Esse arquivo é o stderr do seu servidor.

Crie uma ferramenta de imagem real

Um servidor se justifica quando uma ferramenta faz um trabalho que o modelo não consegue fazer sozinho. Esta recebe um prompt, pede à API do PicassoIA uma imagem e devolve a URL.

Desenhe o contrato da ferramenta

A API segue o estilo Replicate: você cria uma predição, consulta o status dela e depois lê o resultado. Estes são os fatos de que a ferramenta depende:

DetalheValor
URL basehttps://api.picassoia.com/v1
AutenticaçãoAuthorization: Bearer pia_sk_..., criado na página da API
Criar um jobPOST /v1/models/{owner}/{name}/predictions com {"input": {"prompt": "..."}}
Modelo usado aquiPicassoIA Image, slug picassoia/picassoia-image
Tamanho do promptDe 1 a 4.000 caracteres
Valores de statusstarting, processing, succeeded, failed, canceled
Concorrência5 predições por conta, compartilhadas entre todos os tokens e conexões MCP

A documentação da API afirma que é necessário um plano Infinite, e que uma requisição sem ele retorna 403 plan_required. As predições são descritas como gratuitas e não usam créditos. Verifique seu plano antes de depurar qualquer outra coisa.

Mulher desenhando um fluxo de API em um quadro branco em um loft iluminado pelo sol

Mantenha o contrato enxuto: uma ferramenta, dois parâmetros, uma URL de volta. Todo caminho de falha lança ToolError, então o modelo sempre recebe uma mensagem legível.

Trate jobs assíncronos sem bloquear

Como o job roda em uma GPU remota, a ferramenta precisa esperar sem travar o servidor. Isso significa async def, httpx.AsyncClient e asyncio.sleep:

import asyncio
import os
from typing import Literal

import httpx
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

API = "https://api.picassoia.com/v1"
MODEL = "picassoia/picassoia-image"
DONE = ("succeeded", "failed", "canceled")

mcp = MCPServer("Image Studio")
slots = asyncio.Semaphore(4)


@mcp.tool()
async def generate_image(
    prompt: str,
    aspect_ratio: Literal["1:1", "16:9", "9:16", "4:3"] = "16:9",
) -> str:
    """Generate an image from a text prompt and return its URL."""
    token = os.environ.get("PICASSOIA_API_TOKEN")
    if not token:
        raise ToolError("PICASSOIA_API_TOKEN is not set for this server.")

    headers = {"Authorization": f"Bearer {token}"}
    body = {"input": {"prompt": prompt, "aspect_ratio": aspect_ratio}}

    async with slots, httpx.AsyncClient(headers=headers, timeout=30) as http:
        response = await http.post(f"{API}/models/{MODEL}/predictions", json=body)
        prediction = response.json()
        if not response.is_success:
            raise ToolError(f"{prediction.get('code')}: {prediction.get('detail')}")

        while prediction["status"] not in DONE:
            eta = prediction.get("eta") or {}
            await asyncio.sleep(eta.get("next_poll_in_seconds", 2))
            prediction = (await http.get(prediction["urls"]["get"])).json()

    if prediction["status"] != "succeeded":
        raise ToolError(prediction.get("error") or prediction["status"])

    output = prediction["output"]
    return output[0] if isinstance(output, list) else output


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

Caneta apontando para anotações de resposta de API ao lado de um notebook

Três detalhes tornam isso seguro para deixar rodando:

  1. Consulte no ritmo do servidor. A resposta traz eta.next_poll_in_seconds, então você espera exatamente o tempo que a API pede.
  2. Limite a sua própria concorrência. O Semaphore(4) impede que um modelo muito ativo consuma todas as 5 vagas da conta.
  3. Leia o token do ambiente. Registre-o com mcp install server.py -v PICASSOIA_API_TOKEN=pia_sk_... e nunca o cole no arquivo.

💡 O campo output pode ser uma lista de URLs, uma única URL ou null. As duas últimas linhas tratam os dois primeiros casos, e a verificação succeeded logo acima delas descarta null na prática.

Como usar o Sonnet 5 no PicassoIA

Escrever código de ferramentas é onde um modelo de linguagem economiza mais tempo. O Claude Sonnet 5 no PicassoIA lida com programação em várias etapas e tarefas de uso de ferramentas, então você pode colar a ferramenta generate_image que já funciona e pedir a próxima.

  1. Abra a página do modelo do Claude Sonnet 5.
  2. Escreva o prompt. Cole seu servidor e peça: Adicione uma segunda ferramenta que liste minhas predições recentes com GET /v1/predictions. Reutilize o mesmo tratamento de erros. O campo prompt é o único obrigatório.
  3. Defina um prompt de sistema para evitar o problema da renomeação desde o início: Você escreve Python para o MCP SDK 2.x. Importe MCPServer de mcp.server e nunca use FastMCP.
  4. Escolha o esforço. O padrão low pula o pensamento estendido e é o mais rápido. Use high ou max para um bug que toca vários arquivos.
  5. Mantenha o máximo de tokens em 8192 para arquivos de servidor completos, ou reduza para um trecho rápido.
  6. Anexe uma captura de tela de um erro do Inspector, se tiver uma. O modelo lê imagens, e max_image_resolution (padrão de 0,5 megapixel) as reduz.
  7. Gere, copie e teste. Cole o resultado em server.py e rode-o com mcp dev antes de confiar nele.
ParâmetroObrigatórioPadrãoO que faz
promptSimnenhumSua solicitação
system_promptNãovazioDefine o papel e as restrições da sessão
effortNãolowProfundidade de raciocínio, do mais rápido ao mais profundo
max_tokensNão8192Limite de tamanho da saída
imageNãonenhumUma captura de tela ou diagrama como contexto

Prefere outro modelo? O Kimi K2.6 está na mesma categoria e é descrito como voltado para criar agentes e escrever código.

Publique via HTTP

Técnico caminhando por um corredor de racks de servidores

Troque o transporte

Mude uma linha no fim de server.py:

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=3001)

Os clientes agora se conectam a http://127.0.0.1:3001/mcp. Você também pode deixar o arquivo como está e executar uv run mcp run server.py --transport streamable-http. A chamada run() aceita estas opções:

  • host e port, com padrões 127.0.0.1 e 8000
  • streamable_http_path, com padrão /mcp
  • json_response=True para responder a cada POST com um único corpo JSON
  • stateless_http=True para um transporte novo a cada requisição

Registre o servidor remoto no Claude Code com claude mcp add --transport http image-studio https://mcp.example.com/mcp.

Proteja o servidor

Quando o seu servidor sai do localhost, três coisas mudam:

  • A allowlist de Host. O padrão aceita apenas 127.0.0.1, localhost e [::1]. Atrás de um nome de host real, toda requisição falha com 421 Misdirected Request e Invalid Host header. Corrija com transport_security= e liste tanto "mcp.example.com" quanto "mcp.example.com:*" em allowed_hosts.
  • Autorização. Seu servidor é um resource server do OAuth 2.1. Implemente TokenVerifier com um método assíncrono verify_token que devolve um token de acesso ou None, e passe token_verifier= junto com auth=.
  • TLS atrás de um proxy. Quando um balanceador de carga encerra o TLS, inicie o uvicorn com --proxy-headers para que ele confie nos cabeçalhos encaminhados.

💡 Um 421 é uma resposta HTTP comum, não um erro de protocolo, então o cliente só mostra uma falha genérica de transporte. O nome de host problemático aparece no log do servidor. Um servidor recém-publicado que recusa todas as conexões é um problema da allowlist de Host até que se prove o contrário.

Crie suas próprias imagens com o Picasso IA

Desenvolvedor recostado na cadeira depois de terminar o trabalho

Agora você tem um servidor que registra ferramentas, recursos e prompts, passa em um teste em memória, roda dentro do Claude e pode ser publicado atrás de um nome de host real. A parte que vale a sua próxima hora é a própria ferramenta: troque o slug do modelo, adicione uma ferramenta edit_image ou crie uma ferramenta de vídeo ao lado dela.

Experimente o modelo que o seu servidor acabou de chamar. O PicassoIA Image transforma um prompt em uma imagem pronta em segundos, e você pode testar qualquer prompt no navegador antes de automatizá-lo. Quando uma imagem fixa não basta, o PicassoIA Video e o Seedance 2.5 Lite animam um prompt ou uma foto em clipes curtos.

Abra o Picasso IA, escolha um modelo e rode o mesmo prompt que você daria à sua ferramenta. Depois, conecte-o ao seu servidor e deixe o Claude fazer os cliques.

Compartilhe este artigo

Escolha seu idioma