MÓDULO 13 · CONCEITO 13 DE 15

MCP — Model Context Protocol

O protocolo aberto da Anthropic para conectar LLMs a ferramentas e dados externos: arquitetura, primitivas, implementação e segurança

Tempo de leitura ~22 min Pré-requisito 03 · Agentic Coding · 12 · O Engenheiro Sênior na Era da IA Próximo 14 · Tipos de Agentes de IA

Modelos de linguagem são isolados por natureza: eles processam texto de entrada e geram texto de saída, mas não têm acesso ao mundo externo — arquivos do seu sistema, banco de dados da sua empresa, APIs de serviços que você usa, estado atual de um repositório. Para tornar LLMs úteis em fluxos de trabalho reais de engenharia, é preciso conectá-los a esse mundo externo de forma estruturada. Foi exatamente para isso que a Anthropic publicou, em novembro de 2024, o Model Context Protocol (MCP): um protocolo aberto que define como LLMs se conectam a ferramentas e fontes de dados externas.

MCP não é uma ferramenta proprietária — é uma especificação de protocolo, análoga ao que HTTP é para comunicação web ou LSP (Language Server Protocol) é para ferramentas de desenvolvimento. Qualquer aplicação pode implementar um servidor MCP para expor suas capacidades; qualquer cliente pode consumir esses servidores. Claude Code, Cursor, e outros clientes de LLM já têm suporte nativo a MCP. Para engenheiros que constroem fluxos de trabalho com LLMs, MCP é a camada de integração que substitui hacks ad-hoc de injeção de contexto.

O problema que MCP resolve

Antes de MCP existir, conectar LLMs a sistemas externos era um problema resolvido de formas inconsistentes: alguns produtos injetavam conteúdo de arquivos diretamente no prompt, outros implementavam tool calling específico por aplicação, outros ainda tinham sistemas proprietários de plugins. Cada implementação era uma ilha — um servidor de ferramentas construído para Cursor não funcionava com Claude Code, um plugin do ChatGPT não funcionava com Claude.

MCP resolve esse problema de fragmentação com uma camada de abstração padronizada. Um servidor MCP implementado uma vez funciona com qualquer cliente que suporte o protocolo. É o mesmo princípio do LSP: antes do LSP, cada editor precisava implementar suporte específico para cada linguagem; depois do LSP, um servidor de linguagem funciona com qualquer editor que suporte o protocolo. MCP aplica o mesmo padrão à camada de ferramentas de LLMs.

analogia

MCP está para LLMs assim como LSP (Language Server Protocol) está para editores de código. LSP padronizou como editores se comunicam com servidores de linguagem. MCP padroniza como LLMs se comunicam com servidores de ferramentas e dados. A mesma solução para o mesmo problema de fragmentação.

Arquitetura: três papéis

A arquitetura MCP tem três componentes com papéis distintos:

Host é a aplicação que o usuário interage — Claude Desktop, Claude Code, Cursor, ou qualquer aplicação que embute um LLM. O host gerencia o ciclo de vida das conexões MCP, cria e destrói clientes MCP, e controla que servidores estão disponíveis.

Cliente MCP é a camada dentro do host que se comunica com servidores MCP. O cliente mantém uma conexão 1:1 com um servidor específico, negocia as capacidades disponíveis na inicialização, e traduz as solicitações do LLM em chamadas de protocolo.

Servidor MCP é o programa que expõe capacidades externas — acesso a arquivos, chamadas a APIs, operações de banco de dados. Um servidor pode rodar localmente (mesmo processo, processo separado) ou remotamente (servidor HTTP). Cada servidor expõe um conjunto de primitivas que o cliente descobre na inicialização.

O fluxo de uma operação: o LLM, ao processar um prompt, decide que precisa de informação externa (um arquivo, dados de banco, resultado de uma query). O modelo solicita o uso de uma ferramenta. O cliente MCP traduz essa solicitação em uma chamada ao servidor MCP correspondente. O servidor executa a operação e retorna o resultado. O cliente injeta o resultado no contexto do LLM, que continua o processamento.

As três primitivas do protocolo

MCP define três tipos de capacidade que um servidor pode expor. Cada tipo tem propósito distinto e comportamento diferente no protocolo:

Resources (Recursos)

Resources são dados que o servidor expõe para leitura — análogos a arquivos ou endpoints GET. Um resource tem um URI único, um tipo MIME, e conteúdo (texto ou binário). O cliente pode listar os resources disponíveis e ler o conteúdo de um resource específico. Resources são para dados que o LLM precisa consultar sem modificar: o conteúdo de um arquivo de configuração, o schema de um banco de dados, logs de um período específico.

Resources podem ser estáticos (conteúdo fixo retornado sempre igual) ou dinâmicos (conteúdo calculado no momento da solicitação, como o estado atual de um serviço ou dados de uma query executada no momento). Servers podem notificar clientes quando resources mudam via subscriptions — relevante para recursos que mudam frequentemente e onde o LLM precisa de dados atualizados.

Tools (Ferramentas)

Tools são funções executáveis — análogos a endpoints POST ou comandos. Uma tool tem nome, descrição (que o LLM usa para decidir quando usá-la), e um schema JSON Schema dos parâmetros. O LLM invoca uma tool passando parâmetros; o servidor executa e retorna o resultado.

Tools são para operações que modificam estado ou executam lógica: criar um arquivo, submeter um formulário, executar uma query de escrita, chamar uma API externa, rodar um comando de shell. A distinção com resources é importante: resources são somente leitura e declarativos; tools têm efeitos colaterais e são imperativos.

O LLM não executa tools diretamente — ele solicita a execução, e o host/cliente decide se a executa (possivelmente pedindo confirmação do usuário). Essa separação entre solicitação e execução é o ponto de controle de segurança.

Prompts (Prompts Templates)

Prompts são templates de mensagem reutilizáveis — pré-computados pelo servidor para tarefas específicas. Um prompt template pode aceitar parâmetros e retornar uma sequência de mensagens formatada. É para encapsular instruções complexas ou workflows específicos do domínio que precisam ser consistentemente aplicados.

Por exemplo, um servidor MCP para um repositório de código pode ter um prompt "review-pr" que, dado o número de uma PR, monta automaticamente o contexto (diff, histórico, arquivos alterados) e instrui o LLM a fazer uma revisão com formato específico. O usuário invoca esse prompt em vez de montar o contexto manualmente.

Transporte: stdio vs. SSE

MCP suporta dois mecanismos de transporte, com características e casos de uso distintos:

stdio (Standard Input/Output)

No transporte stdio, o cliente lança o servidor como processo filho e se comunica via stdin/stdout com mensagens JSON. É o transporte padrão para servidores locais — ferramentas de desenvolvimento, acesso a sistema de arquivos local, integração com ferramentas de linha de comando. A configuração é simples: o cliente precisa saber o comando para lançar o servidor e os argumentos necessários.

Vantagens do stdio: sem overhead de rede, sem configuração de servidor HTTP, comunicação direta, sem problemas de autenticação de rede. Desvantagem: o servidor precisa estar instalado localmente; não pode ser compartilhado entre máquinas ou usuários.

SSE (Server-Sent Events)

No transporte SSE, o servidor é um processo HTTP que expõe endpoints específicos. O cliente se conecta via SSE para receber eventos do servidor e envia solicitações via POST. É o transporte para servidores remotos — APIs de serviços externos, servidores compartilhados entre usuários, integrações com sistemas corporativos.

SSE permite que um servidor seja acessado por múltiplos clientes simultaneamente, seja hospedado centralmente, e tenha autenticação de nível de serviço. A desvantagem é a complexidade adicional: configuração de servidor HTTP, gerenciamento de autenticação, latência de rede.

A escolha entre stdio e SSE depende do caso de uso: para ferramentas de desenvolvedor individual (acesso ao sistema de arquivos local, banco de dados local, ferramentas CLI), stdio é mais simples e adequado. Para serviços compartilhados de time ou empresa (acesso a sistemas internos, dados de produção), SSE é necessário.

Implementando um servidor MCP

A implementação de um servidor MCP segue um padrão consistente independente da linguagem: registrar as capacidades (resources, tools, prompts), implementar os handlers, e conectar ao transporte. Os SDKs oficiais estão disponíveis para TypeScript/JavaScript, Python, e Kotlin; comunidade mantém SDKs para Go, C#, Rust, e outras linguagens.

Um servidor mínimo com uma tool exposta tem três partes: definição da tool (nome, descrição, schema de parâmetros), handler da tool (a lógica que executa quando o LLM invoca), e inicialização do servidor com o transporte escolhido.

caso de uso central

O caso de uso mais comum para um servidor MCP customizado em times de engenharia: expor operações específicas do sistema interno que o LLM não consegue fazer via tool calling genérico. Acesso à base de conhecimento interna, execução de queries em banco de dados de staging, integração com sistema de tickets, consulta ao catálogo de serviços. Cada um desses é um servidor MCP separado com as tools e resources adequados.

Quando construir um servidor MCP customizado

O ecossistema MCP já tem servidores para os casos de uso mais comuns: sistema de arquivos, GitHub, banco de dados PostgreSQL/SQLite, Slack, Linear, Jira, Brave Search, e dezenas de outros. Antes de implementar um servidor customizado, verifique se existe um servidor público que resolve o problema.

Construir um servidor MCP customizado faz sentido quando: o sistema que você precisa integrar é interno e não existe integração pública; o servidor público existente não tem as permissões granulares que seu caso de uso requer; você precisa de lógica customizada (como combinar múltiplas fontes de dados em um resource); ou você está construindo um produto que outros vão usar e quer expor funcionalidade via MCP.

Não construa um servidor MCP para: substituir uma chamada direta de API que o LLM poderia fazer via tool calling (overhead desnecessário); resolver um problema de injeção de contexto que pode ser resolvido com um arquivo de contexto bem estruturado; ou generalizar antes de ter casos de uso concretos — comece com o mínimo que resolve o problema.

Segurança em servidores MCP

Servidores MCP executam com as permissões do processo que os hospeda. Se um servidor tem acesso ao sistema de arquivos completo ou a um banco de dados de produção, o LLM — através do cliente — pode potencialmente executar operações com esse escopo completo. O princípio de least privilege aplicado a MCP significa: o servidor expõe exatamente as operações necessárias, não mais.

Escopo de tools

Cada tool deve ter o escopo mínimo necessário. Em vez de uma tool "execute_query" que aceita SQL arbitrário, prefira tools específicas: "get_user_by_id", "list_active_orders", "update_order_status". Isso restringe o que o LLM pode fazer mesmo que tente formular operações não-intencionadas.

Para casos onde SQL genérico é necessário (como um servidor de banco de dados para análise), use conexões de banco de dados com permissões restritas: usuário de banco de dados somente-leitura para consultas, sem acesso a tabelas de sistema, sem permissão de DROP ou DELETE.

Validação de parâmetros

O schema JSON Schema de uma tool define a estrutura esperada dos parâmetros, mas o LLM pode gerar parâmetros que passam na validação de estrutura mas têm conteúdo problemático: SQL injection em campos de filtro, path traversal em parâmetros de arquivo, valores fora de range em operações numéricas.

O handler de cada tool deve validar os parâmetros contra o que é permitido — não apenas contra o schema de tipo. Para parâmetros de arquivo, valide que o caminho está dentro do diretório permitido. Para parâmetros de query, use queries parametrizadas, não interpolação. Para parâmetros de operação, valide contra uma allowlist de operações permitidas.

Autenticação e autorização

Para servidores stdio rodando localmente, a autenticação tipicamente herda as permissões do usuário do sistema operacional — o servidor roda como o usuário que lançou o cliente. Para servidores SSE, autenticação explícita é necessária: tokens de API, OAuth, ou certificados de cliente dependendo do contexto.

A autorização deve ser implementada no handler, não apenas na autenticação: mesmo que o cliente esteja autenticado, verifique que a operação específica é permitida para aquele cliente. Um servidor compartilhado de time pode ter clientes com permissões diferentes (leitura vs. escrita, acesso a ambientes diferentes).

Auditoria

Toda chamada de tool deve ser logada com: timestamp, cliente, ferramenta invocada, parâmetros (com dados sensíveis mascarados), e resultado. Isso é essencial para: debugging de problemas, auditoria de acesso para compliance, e identificação de padrões de uso problemático (o LLM tentando operações que não deveria).

risco específico de MCP

MCP prompt injection: um documento malicioso processado pelo LLM pode conter instruções que tentam manipular o LLM a invocar tools de formas não-intencionadas. Se seu servidor MCP processa conteúdo de fontes externas não confiáveis, implemente sanitização e limites explícitos no que as tools podem fazer — mesmo que o LLM seja instruído de outra forma.

MCP vs. Tool Calling da API

É comum confundir MCP com o mecanismo de tool use da API da Anthropic. São camadas diferentes que resolvem problemas distintos:

Tool use da API é o mecanismo de nível de modelo que permite ao LLM sinalizar que quer invocar uma função. Você define as funções disponíveis no request, o modelo responde com qual função chamar e com quais argumentos, você executa a função no seu código, e envia o resultado de volta. Toda a orquestração está no seu código.

MCP é uma camada de protocolo acima do tool use. MCP padroniza como as ferramentas são descobertas, como os servidores são gerenciados, e como os resultados são formatados — de forma que diferentes clientes (Claude Code, Cursor, aplicação customizada) possam usar os mesmos servidores sem cada um reimplementar essa camada de orquestração.

Em outras palavras: tool use da API é a primitiva; MCP é o padrão de protocolo que permite que as ferramentas sejam reutilizáveis e interoperáveis entre clientes. Se você está construindo uma aplicação específica onde você controla todo o fluxo, tool use direto da API pode ser suficiente. Se você quer que suas ferramentas funcionem com Claude Code, Cursor, e sua própria aplicação simultaneamente, MCP é a resposta.

Comparação por linguagem

C# — Servidor MCP com .NET (SDK Comunitário)
Implementação de um servidor MCP em C# usando o SDK comunitário ModelContextProtocol.NET, expondo tools para consulta ao banco de dados interno e acesso ao catálogo de serviços. Transporte stdio para uso local com Claude Code.
// McpServer/Program.cs — Servidor MCP para ferramentas internas de engenharia
using ModelContextProtocol.Server;
using ModelContextProtocol.Protocol.Types;
using Microsoft.Extensions.DependencyInjection;

var builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddMcpServer()
    .WithStdioTransport()  // comunicação via stdin/stdout
    .WithTools<EngineeringTools>();

// Dependências disponíveis nos handlers de tool
builder.Services.AddSingleton<IServiceCatalog, ServiceCatalog>();
builder.Services.AddSingleton<IQueryRunner, ReadOnlyQueryRunner>();

await builder.Build().RunAsync();

// EngineeringTools.cs — Ferramentas expostas via MCP
[McpServerToolType]
public class EngineeringTools(IServiceCatalog catalog, IQueryRunner queryRunner)
{
    [McpServerTool, Description("Lista todos os serviços registrados no catálogo interno")]
    public async Task<string> ListServices(
        [Description("Filtro opcional por equipe dona")] string? team = null)
    {
        var services = await catalog.ListAsync(team);
        return JsonSerializer.Serialize(services, JsonOptions.Indented);
    }

    [McpServerTool, Description("Retorna detalhes de um serviço específico incluindo endpoints e SLOs")]
    public async Task<string> GetServiceDetails(
        [Description("Nome do serviço conforme registrado no catálogo")] string serviceName)
    {
        var service = await catalog.GetAsync(serviceName)
            ?? throw new ToolException($"Serviço '{serviceName}' não encontrado no catálogo");
        return JsonSerializer.Serialize(service, JsonOptions.Indented);
    }

    [McpServerTool, Description("Executa query SELECT no banco de dados de staging (somente leitura)")]
    public async Task<string> RunStagingQuery(
        [Description("Query SQL SELECT. Apenas SELECT é permitido.")] string sql,
        [Description("Limite máximo de linhas retornadas (padrão: 100)")] int limit = 100)
    {
        // Validação: apenas SELECT, sem DROP/DELETE/UPDATE
        var normalized = sql.TrimStart().ToUpperInvariant();
        if (!normalized.StartsWith("SELECT"))
            throw new ToolException("Apenas queries SELECT são permitidas");
        if (normalized.Contains("DROP") || normalized.Contains("DELETE")
            || normalized.Contains("TRUNCATE"))
            throw new ToolException("Operações destrutivas não são permitidas");

        var result = await queryRunner.ExecuteSelectAsync(sql, maxRows: limit);
        return JsonSerializer.Serialize(result, JsonOptions.Indented);
    }
}

// claude_code_config.json — Configuração para Claude Code usar o servidor
// Adicionar em: ~/.claude/config.json
/*
{
  "mcpServers": {
    "engineering-tools": {
      "command": "dotnet",
      "args": ["run", "--project", "/path/to/McpServer"],
      "env": {
        "STAGING_DB_CONNECTION": "${STAGING_DB_CONNECTION}"
      }
    }
  }
}
*/
Python — Servidor MCP com SDK Oficial
Servidor MCP em Python usando o SDK oficial da Anthropic, expondo resources (documentação interna) e tools (operações de repositório). Inclui validação de parâmetros e logging de auditoria.
# mcp_server.py — Servidor MCP para acesso à documentação e repositório
import logging
import asyncio
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import (
    Resource, Tool, TextContent,
    ListResourcesResult, ReadResourceResult, CallToolResult
)

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)

DOCS_ROOT = Path("/internal/docs")  # raiz permitida — path traversal mitigado
ALLOWED_EXTENSIONS = {".md", ".txt", ".yaml", ".json"}

app = Server("internal-engineering-mcp")

@app.list_resources()
async def list_resources() -> ListResourcesResult:
    """Lista documentos de engenharia disponíveis."""
    resources = []
    for path in DOCS_ROOT.rglob("*"):
        if path.is_file() and path.suffix in ALLOWED_EXTENSIONS:
            relative = path.relative_to(DOCS_ROOT)
            resources.append(Resource(
                uri=f"docs://{relative}",
                name=str(relative),
                mimeType="text/plain" if path.suffix in {".md", ".txt"} else "application/json",
            ))
    logger.info(f"Listed {len(resources)} resources")
    return ListResourcesResult(resources=resources)

@app.read_resource()
async def read_resource(uri: str) -> ReadResourceResult:
    """Lê o conteúdo de um documento interno."""
    if not uri.startswith("docs://"):
        raise ValueError(f"URI inválida: {uri}")

    # Mitigação de path traversal: resolve e verifica que está dentro de DOCS_ROOT
    relative = uri.removeprefix("docs://")
    target = (DOCS_ROOT / relative).resolve()
    if not str(target).startswith(str(DOCS_ROOT.resolve())):
        raise PermissionError(f"Acesso negado: {uri}")

    if not target.exists():
        raise FileNotFoundError(f"Documento não encontrado: {uri}")

    logger.info(f"Read resource: {uri}")
    return ReadResourceResult(contents=[TextContent(text=target.read_text(encoding="utf-8"))])

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="search_docs",
            description="Busca texto nos documentos de engenharia internos",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Texto a buscar"},
                    "max_results": {"type": "integer", "default": 10, "minimum": 1, "maximum": 50}
                },
                "required": ["query"]
            }
        ),
        Tool(
            name="get_service_runbook",
            description="Retorna o runbook de operação de um serviço específico",
            inputSchema={
                "type": "object",
                "properties": {
                    "service": {"type": "string", "description": "Nome do serviço"}
                },
                "required": ["service"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
    logger.info(f"Tool called: {name} args={arguments}")

    if name == "search_docs":
        query = arguments["query"].lower()
        max_results = min(arguments.get("max_results", 10), 50)
        results = []
        for path in DOCS_ROOT.rglob("*.md"):
            content = path.read_text(encoding="utf-8")
            if query in content.lower():
                # retorna trecho com contexto
                idx = content.lower().index(query)
                snippet = content[max(0, idx-100):idx+200]
                results.append({"file": str(path.relative_to(DOCS_ROOT)), "snippet": snippet})
            if len(results) >= max_results:
                break
        return CallToolResult(content=[TextContent(text=str(results))])

    if name == "get_service_runbook":
        service = arguments["service"]
        # Valida que é nome simples (sem path traversal)
        if "/" in service or ".." in service:
            raise ValueError(f"Nome de serviço inválido: {service}")
        runbook_path = DOCS_ROOT / "runbooks" / f"{service}.md"
        if not runbook_path.exists():
            return CallToolResult(content=[TextContent(
                text=f"Runbook não encontrado para serviço: {service}"
            )])
        return CallToolResult(content=[TextContent(text=runbook_path.read_text())])

    raise ValueError(f"Tool desconhecida: {name}")

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(read_stream, write_stream, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())
Go — Cliente MCP Integrado em Aplicação Customizada
Implementação de um cliente MCP em Go que se conecta a um servidor via stdio — para aplicações que precisam integrar capacidades de LLM com ferramentas específicas sem usar Claude Code ou Cursor diretamente.
// internal/mcp/client.go — Cliente MCP para integração em aplicação Go
package mcp

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os/exec"
	"sync"
	"sync/atomic"
)

// JSONRPCRequest — mensagem de request do protocolo MCP (JSON-RPC 2.0)
type JSONRPCRequest struct {
	JSONRPC string         `json:"jsonrpc"`
	ID      int64          `json:"id"`
	Method  string         `json:"method"`
	Params  map[string]any `json:"params,omitempty"`
}

// JSONRPCResponse — mensagem de response do protocolo MCP
type JSONRPCResponse struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      int64           `json:"id"`
	Result  json.RawMessage `json:"result,omitempty"`
	Error   *JSONRPCError   `json:"error,omitempty"`
}

type JSONRPCError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

// StdioClient — cliente MCP via processo filho (transporte stdio)
type StdioClient struct {
	cmd     *exec.Cmd
	stdin   io.WriteCloser
	scanner *bufio.Scanner
	mu      sync.Mutex
	nextID  atomic.Int64

	pending map[int64]chan JSONRPCResponse
	pendMu  sync.Mutex
}

func NewStdioClient(ctx context.Context, command string, args ...string) (*StdioClient, error) {
	cmd := exec.CommandContext(ctx, command, args...)

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return nil, fmt.Errorf("stdin pipe: %w", err)
	}
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("stdout pipe: %w", err)
	}

	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("start server: %w", err)
	}

	c := &StdioClient{
		cmd:     cmd,
		stdin:   stdin,
		scanner: bufio.NewScanner(stdout),
		pending: make(map[int64]chan JSONRPCResponse),
	}

	go c.readLoop()
	return c, nil
}

func (c *StdioClient) readLoop() {
	for c.scanner.Scan() {
		var resp JSONRPCResponse
		if err := json.Unmarshal(c.scanner.Bytes(), &resp); err != nil {
			continue
		}
		c.pendMu.Lock()
		ch, ok := c.pending[resp.ID]
		if ok {
			delete(c.pending, resp.ID)
		}
		c.pendMu.Unlock()
		if ok {
			ch <- resp
		}
	}
}

func (c *StdioClient) call(ctx context.Context, method string, params map[string]any) (json.RawMessage, error) {
	id := c.nextID.Add(1)
	req := JSONRPCRequest{JSONRPC: "2.0", ID: id, Method: method, Params: params}

	data, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}

	ch := make(chan JSONRPCResponse, 1)
	c.pendMu.Lock()
	c.pending[id] = ch
	c.pendMu.Unlock()

	c.mu.Lock()
	_, err = fmt.Fprintf(c.stdin, "%s\n", data)
	c.mu.Unlock()
	if err != nil {
		return nil, err
	}

	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case resp := <-ch:
		if resp.Error != nil {
			return nil, fmt.Errorf("MCP error %d: %s", resp.Error.Code, resp.Error.Message)
		}
		return resp.Result, nil
	}
}

// Initialize — handshake inicial obrigatório do protocolo MCP
func (c *StdioClient) Initialize(ctx context.Context) error {
	_, err := c.call(ctx, "initialize", map[string]any{
		"protocolVersion": "2024-11-05",
		"clientInfo": map[string]any{
			"name":    "go-mcp-client",
			"version": "1.0.0",
		},
		"capabilities": map[string]any{},
	})
	return err
}

// CallTool — invoca uma tool exposta pelo servidor
func (c *StdioClient) CallTool(ctx context.Context, name string, args map[string]any) (string, error) {
	result, err := c.call(ctx, "tools/call", map[string]any{
		"name":      name,
		"arguments": args,
	})
	if err != nil {
		return "", err
	}

	var response struct {
		Content []struct {
			Type string `json:"type"`
			Text string `json:"text"`
		} `json:"content"`
	}
	if err := json.Unmarshal(result, &response); err != nil {
		return "", err
	}
	if len(response.Content) == 0 {
		return "", nil
	}
	return response.Content[0].Text, nil
}

// Close — encerra o processo servidor
func (c *StdioClient) Close() error {
	c.stdin.Close()
	return c.cmd.Wait()
}

// Exemplo de uso
func ExampleUsage(ctx context.Context) error {
	client, err := NewStdioClient(ctx, "python", "mcp_server.py")
	if err != nil {
		return err
	}
	defer client.Close()

	if err := client.Initialize(ctx); err != nil {
		return fmt.Errorf("initialize: %w", err)
	}

	result, err := client.CallTool(ctx, "search_docs", map[string]any{
		"query":       "rate limiting",
		"max_results": 5,
	})
	if err != nil {
		return err
	}
	fmt.Println(result)
	return nil
}

O ecossistema MCP em 2025

Em maio de 2025, o ecossistema MCP tem servidores para os casos de uso mais comuns mantidos pela comunidade e por empresas: GitHub, GitLab, Jira, Linear, Notion, Slack, PostgreSQL, SQLite, MongoDB, Redis, Elasticsearch, Brave Search, Puppeteer (browser automation), Docker, Kubernetes, e dezenas de outros. O repositório oficial da Anthropic mantém uma lista curada em github.com/modelcontextprotocol/servers.

Suporte a MCP em clientes também cresceu: Claude Desktop, Claude Code, Cursor, Zed, e Cline têm suporte nativo. Vários frameworks de agentes (LangChain, CrewAI, AutoGen) adicionaram adaptadores MCP. A adoção do protocolo indica que MCP está se tornando o padrão de fato para integração de ferramentas com LLMs — o LSP do ecossistema de IA.

Para engenheiros, isso significa que o investimento em entender e construir com MCP é durável: os conceitos (primitivas, transporte, segurança) são válidos independente de qual cliente ou modelo específico seja usado. Um servidor MCP construído hoje funciona com qualquer cliente que suporte o protocolo — agora e no futuro.

Exercícios práticos

  1. Configurar um servidor MCP existente: Escolha um servidor MCP da lista oficial (github.com/modelcontextprotocol/servers) que seja relevante para o seu trabalho — GitHub, banco de dados, ou documentação. Configure-o para uso com Claude Code seguindo a documentação. Use-o para fazer uma tarefa real do seu trabalho diário. Documente: o que funcionou, o que não funcionou, e onde o servidor tem limitações de escopo que você precisaria customizar.
  2. Implementar um servidor MCP simples: Identifique uma fonte de informação interna que você consulta frequentemente e que não tem servidor MCP público (wiki interna, base de decisões arquiteturais, catálogo de serviços). Implemente um servidor MCP minimal com dois resources e uma tool. Use transporte stdio. Conecte ao Claude Code e use para responder perguntas sobre o domínio que o servidor expõe. O objetivo é completar o ciclo end-to-end, não construir um servidor completo.
  3. Modelagem de segurança: Para o servidor que você implementou (ou para um servidor hipotético que teria acesso ao seu banco de dados de produção): identifique os três principais vetores de risco (o que um LLM poderia fazer de errado com acesso a esse servidor), e para cada um proponha um controle específico — validação de parâmetro, permissão de banco de dados, auditoria de log. Documente como esses controles seriam implementados no código do servidor.

Referências e leitura complementar

  1. docs Model Context Protocol — Especificação Oficial — Anthropic (2024). modelcontextprotocol.io/specification — A especificação completa do protocolo. Leitura obrigatória para implementação de servidores. Inclui schema JSON-RPC, definição das primitivas, e comportamento esperado de clientes e servidores.
  2. docs MCP Python SDK — Anthropic (2024). github.com/modelcontextprotocol/python-sdk — O SDK oficial Python usado no exemplo deste conceito. Inclui exemplos para stdio e SSE, e integração com FastAPI para servidores HTTP.
  3. docs Servidor MCP de Referência — GitHub — Anthropic (2024). github.com/modelcontextprotocol/servers/tree/main/src/github — O servidor MCP oficial para GitHub. Excelente referência de implementação de um servidor completo com múltiplas tools e resources, autenticação via token, e tratamento de rate limiting.
  4. artigo Introducing the Model Context Protocol — Anthropic (2024). anthropic.com/news/model-context-protocol — O anúncio original do MCP, que descreve a motivação do protocolo e o problema de fragmentação que resolve. Contexto útil para entender as decisões de design.
  5. artigo Language Server Protocol — Lessons Learned — Microsoft (2016). code.visualstudio.com/blogs/2016/06/27/common-language-protocol — O post original que descreve o design do LSP e os problemas que resolveu. Leitura útil pela analogia com MCP — os mesmos princípios de padronização de protocolo para reduzir fragmentação.
  6. docs Claude Code — Configuração de Servidores MCP — Anthropic (2025). docs.anthropic.com/claude-code/mcp — Documentação de como configurar servidores MCP no Claude Code via claude_code_config.json. Inclui variáveis de ambiente, permissões, e debugging de conexões.
  7. artigo MCP Security Considerations — Trail of Bits (2025). blog.trailofbits.com — Análise de segurança do protocolo MCP, incluindo vetores de ataque específicos (prompt injection via recursos, path traversal, escalada de privilégio). Leitura crítica para qualquer servidor MCP que processa conteúdo de fontes não confiáveis.
  8. docs ModelContextProtocol.NET — SDK Comunitário C# — Comunidade (2025). github.com/modelcontextprotocol/csharp-sdk — O SDK C# usado no exemplo deste conceito. Inclui integração com Microsoft.Extensions.Hosting e suporte a DI (Dependency Injection).
  9. artigo Building Production MCP Servers — Simon Willison (2025). simonwillison.net — Guia prático de como construir servidores MCP prontos para produção, incluindo considerações de deployment, monitoramento, e versionamento de servidor.
  10. docs Anthropic Tool Use API Reference — Anthropic (2025). docs.anthropic.com/claude/docs/tool-use — A documentação do tool use de nível de API — o mecanismo subjacente que MCP usa para permitir que LLMs invoquem ferramentas. Entender a primitiva ajuda a entender o protocolo que a abstrai.
  11. artigo JSON-RPC 2.0 Specification — JSON-RPC Working Group (2010). jsonrpc.org/specification — MCP é construído sobre JSON-RPC 2.0. Conhecer a especificação do protocolo subjacente é útil ao debugar problemas de comunicação entre cliente e servidor.
  12. artigo The MCP Ecosystem in 2025: State of Adoption — Latent Space (2025). latent.space — Análise do estado do ecossistema MCP após 6 meses do lançamento. Inclui quais servidores têm mais adoção, quais clientes têm melhor suporte, e onde o protocolo está evoluindo.