Skip to main content

Command Palette

Search for a command to run...

MCP (Model Context Protocol): Do Zero ao Avançado com Caso de Uso Real

Arquitetura, primitivas, servidor em Python, testes com Inspector e client próprio.

Published
12 min read
MCP (Model Context Protocol): Do Zero ao Avançado com Caso de Uso Real

MCP (Model Context Protocol): Do Zero ao Avançado com Caso de Uso Real

Se você trabalha com AI Agents em produção, já esbarrou nesse problema: cada integração é uma gambiarra diferente. Um agent que acessa banco de dados usa uma API. Outro que lê arquivos usa outra. Um terceiro que manda e-mail, mais uma. Cada ferramenta tem seu formato, seu protocolo, sua autenticação.

O MCP resolve isso. É um protocolo aberto que padroniza como aplicações de AI se conectam a sistemas externos. Pense num USB-C pra AI: um conector universal.

Neste artigo eu vou cobrir tudo. Da teoria à prática, com código que você roda no seu terminal sem depender de nenhum produto específico.

O que é MCP

MCP (Model Context Protocol) é um protocolo open-source criado pela Anthropic que define como aplicações de AI (Claude, ChatGPT, VS Code Copilot, Cursor) se conectam a fontes de dados, ferramentas e workflows externos.

Antes do MCP, cada aplicação implementava suas integrações do zero. O resultado: fragmentação. Cada vendor com seu SDK, seu formato, seu jeito de expor ferramentas. O MCP propõe um padrão. Você implementa uma vez, e qualquer client compatível consegue usar.

Hoje o protocolo é suportado por Claude Desktop, ChatGPT, VS Code, Cursor, MCPJam e dezenas de outros clients.

Arquitetura: Host, Client e Server

A arquitetura do MCP tem três participantes:

  • MCP Host: a aplicação de AI que orquestra tudo. Exemplos: Claude Desktop, VS Code, Cursor.

  • MCP Client: um componente dentro do Host que mantém conexão com um MCP Server específico. Cada conexão tem seu próprio client.

  • MCP Server: o programa que expõe funcionalidades (dados, ferramentas, prompts) pro client consumir.

Na prática, quando o VS Code se conecta a dois MCP Servers diferentes (digamos, um de filesystem e um do Sentry), ele cria dois MCP Clients internos, cada um com sua conexão dedicada.

Duas camadas

O protocolo opera em duas camadas:

Data Layer: define o formato das mensagens usando JSON-RPC 2.0. Aqui ficam o lifecycle management (inicialização, negociação de capabilities, encerramento), as primitivas (tools, resources, prompts) e notificações.

Transport Layer: define como as mensagens trafegam. Dois mecanismos:

  • STDIO: comunicação via stdin/stdout entre processos locais. Zero overhead de rede. Ideal pra servidores que rodam na mesma máquina.

  • Streamable HTTP: comunicação via HTTP POST com Server-Sent Events opcionais. Suporta autenticação OAuth, API keys e headers customizados. Pra servidores remotos.

As 3 Primitivas do MCP

Todo MCP Server expõe funcionalidades através de três primitivas. Entender a diferença entre elas é fundamental.

Tools (controladas pelo modelo)

Tools são funções que o LLM pode chamar ativamente. O modelo decide quando usá-las com base no contexto da conversa.

Exemplos: buscar voos, enviar mensagem, criar evento no calendário, consultar banco de dados.

Cada tool tem um schema JSON que define seus inputs e outputs. O client lista as tools disponíveis (tools/list) e o modelo chama as que precisa (tools/call).

{
  "name": "consultarCEP",
  "description": "Consulta endereço a partir de um CEP brasileiro",
  "inputSchema": {
    "type": "object",
    "properties": {
      "cep": {
        "type": "string",
        "description": "CEP com 8 dígitos"
      }
    },
    "required": ["cep"]
  }
}

Resources (controladas pela aplicação)

Resources são fontes de dados passivas. Elas provêem acesso somente-leitura a informações que a aplicação pode usar como contexto.

Cada resource tem um URI único (ex: file:///docs/README.md, db://users/schema) e declara seu MIME type. Existem dois padrões de descoberta:

  • Direct Resources: URIs fixos que apontam pra dados específicos.

  • Resource Templates: URIs dinâmicos com parâmetros. Ex: api://clientes/{id}/pedidos.

Prompts (controlados pelo usuário)

Prompts são templates pré-construídos que orientam o modelo a trabalhar com tools e resources de forma específica. O usuário escolhe qual prompt usar.

Exemplo: um prompt "Analise este repositório" que instrui o modelo a ler a estrutura de arquivos, verificar dependências e gerar um relatório de code review.

Primitiva Quem controla Acesso Exemplo
Tools Modelo (LLM) Leitura e escrita Enviar e-mail, criar registro
Resources Aplicação Somente leitura Ler arquivo, schema do banco
Prompts Usuário Template "Resuma minha reunião"

Mão na massa: criando um MCP Server em Python

Chega de teoria. Vou construir um MCP Server que consulta a API ViaCEP (pública e gratuita) pra buscar endereços a partir de CEPs brasileiros. Um caso de uso simples, mas que mostra toda a mecânica do protocolo.

Setup do projeto

Você precisa de Python 3.10+ e do uv (gerenciador de pacotes da Astral):

# Instalar uv (se ainda não tem)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Criar o projeto
uv init mcp-cep
cd mcp-cep

# Criar e ativar ambiente virtual
uv venv
source .venv/bin/activate

# Instalar dependências
uv add "mcp[cli]" httpx

O servidor completo

Crie o arquivo server.py:

from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# Inicializa o servidor
mcp = FastMCP("cep-brasil")

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


async def buscar_cep_api(cep: str) -> dict[str, Any] | None:
    """Faz a requisição pra API ViaCEP."""
    cep_limpo = cep.replace("-", "").replace(".", "").strip()
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(
                f"{VIACEP_BASE}/{cep_limpo}/json/",
                timeout=10.0
            )
            response.raise_for_status()
            data = response.json()
            if "erro" in data:
                return None
            return data
        except Exception:
            return None


@mcp.tool()
async def consultar_cep(cep: str) -> str:
    """Consulta um endereço completo a partir de um CEP brasileiro.

    Args:
        cep: CEP com 8 dígitos (aceita formato 01001-000 ou 01001000)
    """
    data = await buscar_cep_api(cep)
    if not data:
        return f"CEP {cep} não encontrado ou inválido."

    return (
        f"CEP: {data['cep']}\n"
        f"Logradouro: {data.get('logradouro', 'N/A')}\n"
        f"Complemento: {data.get('complemento', '')}\n"
        f"Bairro: {data.get('bairro', 'N/A')}\n"
        f"Cidade: {data.get('localidade', 'N/A')}\n"
        f"Estado: {data.get('uf', 'N/A')}\n"
        f"DDD: {data.get('ddd', 'N/A')}"
    )


@mcp.tool()
async def buscar_endereco(uf: str, cidade: str, logradouro: str) -> str:
    """Busca CEPs a partir de um endereço parcial.

    Args:
        uf: Sigla do estado com 2 letras (ex: SP, RJ, PR)
        cidade: Nome da cidade
        logradouro: Nome da rua (mínimo 3 caracteres)
    """
    if len(logradouro) < 3:
        return "O logradouro precisa ter no mínimo 3 caracteres."

    url = f"{VIACEP_BASE}/{uf}/{cidade}/{logradouro}/json/"
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, timeout=10.0)
            response.raise_for_status()
            resultados = response.json()
        except Exception:
            return "Erro ao buscar endereço."

    if not resultados:
        return "Nenhum endereço encontrado."

    saida = []
    for r in resultados[:5]:
        saida.append(
            f"CEP: {r['cep']} | {r.get('logradouro', '')} - "
            f"{r.get('bairro', '')}, {r.get('localidade', '')}/{r.get('uf', '')}"
        )
    return "\n".join(saida)


@mcp.resource("cep://info")
def info_resource() -> str:
    """Informações sobre o servidor de CEP."""
    return (
        "Servidor MCP de consulta de CEPs brasileiros.\n"
        "Fonte de dados: ViaCEP (viacep.com.br)\n"
        "Tools disponíveis: consultar_cep, buscar_endereco"
    )


def main():
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()

Alguns pontos importantes:

  • FastMCP usa type hints e docstrings pra gerar os schemas das tools automaticamente. Sem JSON Schema na mão.

  • @mcp.tool() registra a função como tool. Nome, descrição e parâmetros são extraídos do código.

  • @mcp.resource() registra um resource com URI fixo.

  • O transport stdio é o mais simples pra desenvolvimento local.

Testando o servidor

Agora vem a parte mais importante: como testar isso sem depender de nenhum produto pago ou específico. Duas opções.

Opção 1: MCP Inspector (visual, zero config)

O MCP Inspector é a ferramenta oficial de teste. Roda no browser, não precisa instalar nada além do Node.js:

npx @modelcontextprotocol/inspector uv --directory /caminho/pro/mcp-cep run python server.py

Isso abre uma interface web onde você:

  • Vê todas as tools e resources registradas

  • Testa cada tool com inputs customizados

  • Inspeciona os schemas gerados

  • Monitora as mensagens JSON-RPC trocadas

  • Verifica erros e logs

É a forma mais rápida de validar se o servidor tá funcionando. Você vê o consultar_cep listado, coloca um CEP, clica pra executar e vê o resultado. Sem precisar de Claude, ChatGPT ou qualquer outro client.

Exemplo da consulta via MCP com MCP Inspector

Tools

Consultar CEP

Resultado da consulta

Opção 2: Client Python (programático, completo)

Pra quem quer integrar de verdade, o SDK do MCP permite construir seu próprio client. Crie um arquivo client.py:

import asyncio
import sys
from contextlib import AsyncExitStack

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


class MCPClient:
    def __init__(self):
        self.session: ClientSession | None = None
        self.exit_stack = AsyncExitStack()

    async def connect(self, server_script: str):
        """Conecta ao MCP Server via STDIO."""
        server_params = StdioServerParameters(
            command="python",
            args=[server_script],
            env=None
        )

        stdio_transport = await self.exit_stack.enter_async_context(
            stdio_client(server_params)
        )
        read_stream, write_stream = stdio_transport
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(read_stream, write_stream)
        )

        await self.session.initialize()

        # Lista as tools disponíveis
        response = await self.session.list_tools()
        print("Tools disponíveis:")
        for tool in response.tools:
            print(f"  - {tool.name}: {tool.description}")

    async def call_tool(self, name: str, args: dict) -> str:
        """Chama uma tool no servidor."""
        result = await self.session.call_tool(name, args)
        return result.content[0].text

    async def cleanup(self):
        await self.exit_stack.aclose()


async def main():
    client = MCPClient()
    try:
        await client.connect("server.py")

        # Testa consulta de CEP
        print("\n--- Consultando CEP 01001-000 ---")
        resultado = await client.call_tool(
            "consultar_cep", {"cep": "01001-000"}
        )
        print(resultado)

        # Testa busca por endereço
        print("\n--- Buscando 'Paulista' em São Paulo ---")
        resultado = await client.call_tool(
            "buscar_endereco",
            {"uf": "SP", "cidade": "São Paulo", "logradouro": "Paulista"}
        )
        print(resultado)

    finally:
        await client.cleanup()


if __name__ == "__main__":
    asyncio.run(main())

Instale a dependência do client e rode:

uv add mcp
python client.py

Saída esperada:

Tools disponíveis:
  - consultar_cep: Consulta um endereço completo a partir de um CEP brasileiro.
  - buscar_endereco: Busca CEPs a partir de um endereço parcial.

--- Consultando CEP 01001-000 ---
CEP: 01001-000
Logradouro: Praça da Sé
Complemento: lado ímpar
Bairro: Sé
Cidade: São Paulo
Estado: SP
DDD: 11

--- Buscando 'Paulista' em São Paulo ---
CEP: 01310-100 | Avenida Paulista - Bela Vista, São Paulo/SP
CEP: 01310-000 | Avenida Paulista - Bela Vista, São Paulo/SP
...

Nenhuma dependência de produto externo. Você criou o servidor e o client, e eles se comunicam via protocolo MCP puro.

Integrando com AI Hosts

Depois de validar que o servidor funciona, aí sim você conecta num Host de verdade. O MCP é suportado nativamente por Claude Desktop, VS Code (Copilot), Cursor, ChatGPT e outros. Cada um tem sua forma de configurar, mas o server é o mesmo. A documentação de cada client explica como apontar pro seu servidor.

Esse é o ponto forte do protocolo: você escreve o server uma vez e qualquer client compatível consome.

Indo além: conceitos avançados

Transporte remoto com Streamable HTTP

O exemplo acima usa STDIO (local). Pra expor seu MCP Server remotamente, use Streamable HTTP:

def main():
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

Isso sobe um servidor HTTP que aceita conexões de clients remotos. Você pode colocar atrás de um reverse proxy, adicionar autenticação OAuth, e escalar horizontalmente.

Notificações e progresso

Pra operações longas, o MCP suporta notificações de progresso. O servidor pode enviar updates pro client enquanto processa, em vez de deixar o usuário esperando sem feedback.

Múltiplos servidores

Um MCP Host pode se conectar a vários servidores simultaneamente. Cada um expõe suas próprias tools e resources. O modelo vê todas as tools disponíveis e decide quais usar com base no contexto.

Isso permite composição: um server de CEP + um server de banco de dados + um server de e-mail = um agent que consulta endereço, verifica cadastro no banco e notifica o cliente. Tudo padronizado.

Segurança e controle

O protocolo prevê mecanismos de controle:

  • Aprovação por tool: o client pode pedir confirmação do usuário antes de executar cada tool.

  • Permissões granulares: pré-aprovar operações seguras (leitura) e exigir aprovação pra escrita.

  • Logs de atividade: registrar todas as execuções de tools com resultados.

Quando usar (e quando não usar) MCP

Use quando:

  • Você quer que seu AI Agent acesse dados ou execute ações em sistemas externos

  • Precisa de uma integração que funcione com múltiplos clients (Claude, ChatGPT, VS Code)

  • Quer padronizar as integrações do seu time em vez de cada um inventar seu formato

Não use quando:

  • Seu caso é uma API simples que só precisa de uma chamada HTTP direta

  • Você não tem um MCP Host/Client na arquitetura

  • O overhead do protocolo não se justifica pra uma integração pontual

Ecossistema atual

O MCP já tem um ecossistema considerável de servidores prontos: filesystem, PostgreSQL, GitHub, Slack, Google Drive, entre outros. A lista completa fica no repositório oficial.

Os SDKs oficiais cobrem Python e TypeScript. A comunidade já tem implementações em Go, Rust e Java.

Pra fechar

O MCP é a cola que faltava entre AI Agents e o mundo real. Em vez de cada aplicação reinventar como conectar um modelo a ferramentas externas, agora existe um padrão. Você implementa um MCP Server uma vez e qualquer client compatível consome.

O protocolo é simples o suficiente pra criar um servidor funcional em menos de 100 linhas de Python, mas robusto o suficiente pra rodar em produção com autenticação, transporte HTTP e múltiplos servidores simultâneos.

Se você tá construindo AI Agents ou qualquer aplicação que precisa conectar LLMs a sistemas externos, vale estudar o MCP agora. O ecossistema tá crescendo rápido e quem sair na frente vai ter vantagem.

Todo o código deste artigo foi montado para rodar localmente com Python + uv e serve como base prática para adaptar ao seu ambiente.

A documentação oficial fica em modelcontextprotocol.io.


Me segue no LinkedIn pra mais conteúdo sobre AI Agents, arquitetura e engenharia de software na prática.