Quem já leu o tutorial de MCP em TypeScript aqui no blog sabe como subir um servidor funcional. Esse post é o complementar denso: o protocolo MCP destrinchado por baixo dos panos, da camada de transporte até o handshake, das primitivas até as notificações, do JSON-RPC até OAuth. Sem cortar nada. Se você vai escrever servidor pra produção, integrar com cliente custom, ou simplesmente entender o que está acontecendo quando o Claude Desktop chama uma tool, é isso aqui.
MCP (Model Context Protocol) é um protocolo aberto da Anthropic, lançado em novembro de 2024 e em iteração ativa. A versão estável atual é 2025-06-18, com revisão 2025-03-26 ainda suportada para retrocompatibilidade. Toda a especificação vive em modelcontextprotocol.io. O resto desse post é o que está lá, traduzido pra português direto e organizado pra leitura sequencial.
1. Arquitetura: host, client, server
Três papéis. Host: aplicação que orquestra LLM (Claude Desktop, Cursor, Windsurf, seu agente custom). Client: componente dentro do host que mantém conexão 1:1 com um servidor. Server: processo que expõe capacidades (tools, resources, prompts). Um host pode ter N clients, cada client conectado a 1 server. Servers são burros sobre o host, conhecem só o seu próprio escopo.
A separação importa por segurança. O host decide quais servers conectar, gerencia consentimento do usuário, e roteia o que cada conversa do LLM enxerga. O server nunca conversa direto com outro server. Toda interação cruzada passa pelo host. Isso evita escalada lateral de permissão e mantém o blast radius previsível.
2. As três camadas: transport, protocol, application
Transport é o canal físico de mensagens (stdio, HTTP). Protocol é a sintaxe e semântica das mensagens (JSON-RPC 2.0). Application é o vocabulário do MCP em si (métodos como tools/call, notificações como notifications/resources/updated). As camadas são independentes: trocar transport não muda payload, trocar payload não muda transport.
3. Transports oficiais
Dois transportes padronizados pela spec.
stdio: o client spawnga o server como processo filho. JSON-RPC vai e volta por stdin e stdout, uma mensagem por linha (delimitada por \n, sem embedded newlines no JSON). stderr é livre pra logs. É o transport default pra integração local (Claude Desktop, IDE plugins). Latência mínima, sem rede, sem auth (o processo herda a permissão do host).
Streamable HTTP: introduzido em 2025-03-26, substitui o antigo HTTP+SSE. O server expõe um único endpoint que aceita POST e GET. Cliente manda mensagens via POST com Content-Type: application/json. Server responde inline (JSON simples) ou faz upgrade da resposta pra text/event-stream quando precisa streaming. O GET no mesmo endpoint abre canal SSE pra server-initiated messages (notificações, sampling reverso). Sessões são identificadas por header Mcp-Session-Id retornado no initialize e reusado em chamadas subsequentes. É o transport recomendado pra qualquer server remoto.
Implementações podem ter transports custom (WebSocket, named pipes, gRPC bridges), mas perdem interop. A spec recomenda ficar nos dois oficiais.
4. JSON-RPC 2.0: o envelope
Todo tráfego MCP é JSON-RPC 2.0. Três tipos de mensagem.
Request: pede algo e espera resposta. Tem id (string ou número, único na sessão), method, e params opcional.
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": { "name": "create_ticket", "arguments": { "title": "x" } }
}
Response: bate com o request pelo mesmo id. Tem result em sucesso, ou error em falha. Nunca os dois.
{ "jsonrpc": "2.0", "id": 42, "result": { "content": [...] } }
{ "jsonrpc": "2.0", "id": 42, "error": { "code": -32602, "message": "invalid params" } }
Notification: fire-and-forget. Sem id, sem resposta esperada. Usada pra progresso, mudança de estado, log.
{ "jsonrpc": "2.0", "method": "notifications/progress", "params": {...} }
Erros padrão JSON-RPC: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal. MCP reserva o range -32000 a -32099 pra erros específicos (ex: -32002 resource not found).
5. Lifecycle: initialize, operate, shutdown
Toda sessão MCP segue três fases.
Initialize: primeiro request que o client manda. Negocia versão de protocolo e troca capabilities. Server escolhe a versão (entre as suportadas) e devolve as próprias capabilities. Client confirma com a notificação notifications/initialized. Antes desse confirm, server não deve processar request normal nenhum (exceto ping).
// client -> server
{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": { "sampling": {}, "roots": { "listChanged": true } },
"clientInfo": { "name": "my-host", "version": "1.0.0" }
}
}
// server -> client
{
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true, "listChanged": true },
"prompts": {},
"logging": {}
},
"serverInfo": { "name": "mcp-tickets", "version": "0.1.0" }
}
}
// client -> server (notification)
{ "jsonrpc": "2.0", "method": "notifications/initialized" }
Operate: troca normal de mensagens. Dura enquanto a sessão estiver viva.
Shutdown: stdio fecha por EOF no stdin; HTTP fecha por timeout de sessão ou DELETE no endpoint com Mcp-Session-Id. Não existe método shutdown no protocolo.
6. Capability negotiation: quem suporta o quê
Cada lado anuncia o que oferece. Server expõe combinações de tools, resources, prompts, logging, completions. Client expõe sampling, roots, elicitation (nova em 2025-06-18). Subkeys habilitam features finas: listChanged: true em tools significa que o server pode emitir notifications/tools/list_changed quando a lista muda. subscribe: true em resources habilita resources/subscribe.
Regra de ouro: não chame método sem capability anunciada. Se o server não declarou prompts, mandar prompts/list é erro.
7. Primitivas do server: tools, resources, prompts
Tools: funções que o LLM chama pra executar ação (criar registro, rodar query, mandar mensagem). Cada tool tem name (snake_case, único no server), description (texto que o LLM lê pra escolher), e inputSchema (JSON Schema da entrada). 2025-06-18 adiciona outputSchema opcional e annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) que ajudam o host a decidir UX de confirmação.
Métodos: tools/list (com paginação por cursor), tools/call. Resposta de tools/call tem content (array de TextContent, ImageContent, AudioContent ou EmbeddedResource) e isError (boolean indicando falha lógica da tool, distinta de erro de protocolo). 2025-06-18 acrescenta structuredContent opcional pra retorno tipado conforme outputSchema.
Resources: dados que o cliente lê pra alimentar contexto. Identificados por URI (file://, https://, ou scheme custom como tickets://open). Têm name, description, mimeType. Métodos: resources/list, resources/read, resources/templates/list (resources parametrizados via URI template RFC 6570, ex: tickets://{id}), resources/subscribe e resources/unsubscribe. Server emite notifications/resources/updated com a URI quando um resource subscrito muda.
Prompts: templates parametrizados que o cliente injeta no LLM. Tem name, description, arguments (com tipo e required). Métodos: prompts/list, prompts/get. prompts/get devolve messages (array de {role, content}) já interpolado, pronto pra enviar ao modelo.
8. Primitivas do client: sampling, roots, elicitation
A simetria que muita gente não percebe: o protocolo é bidirecional. Server pode pedir coisas ao client.
Sampling: server invoca o LLM do client via sampling/createMessage. Útil quando a tool precisa de raciocínio LLM no meio da execução (resumir, classificar, escolher) sem o server ter LLM próprio. Cliente intercepta, mostra ao usuário (consentimento obrigatório), e devolve a resposta. Params incluem messages, modelPreferences (hints de custo/velocidade/qualidade), systemPrompt, includeContext (none/thisServer/allServers).
Roots: client expõe ao server quais diretórios/URIs ele tem permissão de operar. Server chama roots/list e descobre o escopo. Mudança emite notifications/roots/list_changed. Sem roots, o server não sabe que arquivo pode tocar.
Elicitation (novo em 2025-06-18): server pede input estruturado adicional ao usuário durante execução. elicitation/create com message e requestedSchema (JSON Schema). Cliente renderiza form, devolve resposta validada. Substitui o anti-pattern de tool com 30 parâmetros opcionais.
9. Utilities: ping, progress, cancellation, logging, completion
Ping: ping sem params, resposta vazia. Qualquer lado pode chamar pra checar liveness.
Progress: pra requests longos, quem chamou inclui _meta.progressToken. O outro lado emite notifications/progress com {progressToken, progress, total} ao longo da execução.
Cancellation: notifications/cancelled com {requestId, reason}. Best-effort, server deve parar o quanto antes mas não é obrigado.
Logging: client setta nível com logging/setLevel (debug, info, notice, warning, error, critical, alert, emergency). Server emite notifications/message com {level, logger, data}.
Completion: autocomplete pra argumento de prompt ou URI de resource template. completion/complete com ref (tipo e identificador) e argument (nome e valor parcial). Server devolve até 100 sugestões.
10. Autenticação: OAuth 2.1 em HTTP
stdio não autentica (herda permissão do processo). HTTP precisa. A spec 2025-06-18 padroniza OAuth 2.1 com PKCE obrigatório. Servidor MCP atua como Resource Server. Discovery via /.well-known/oauth-protected-resource aponta o Authorization Server. Cliente faz Authorization Code Flow com PKCE, recebe access token, envia em Authorization: Bearer em toda chamada.
Dynamic Client Registration (RFC 7591) é recomendado pra não exigir setup manual. Tokens devem ter audience checada (RFC 8707) pra evitar passthrough attack. Refresh tokens são opcionais mas indicados pra sessão longa.
11. Streamable HTTP em detalhe
O endpoint único aceita os dois métodos.
POST: corpo é JSON-RPC (single ou batch). Server responde com application/json (resposta única, modo síncrono) OU faz upgrade pra text/event-stream e streama. No segundo modo, cada SSE event tem data: contendo JSON-RPC. Stream termina quando server fecha. Útil pra tool longa que emite progress no meio.
GET: abre SSE stream pra mensagens iniciadas pelo server (notificações, sampling). Cliente mantém aberto. Server pode opcionalmente exigir Last-Event-ID pra resumir reconexão sem perder evento.
DELETE: encerra sessão (envia Mcp-Session-Id no header).
Header Mcp-Session-Id volta no response do initialize e deve ser enviado em toda request subsequente. Sessões são isoladas: dois clients no mesmo server têm sessões separadas. Server pode invalidar sessão a qualquer momento retornando 404, e o client deve reinicializar.
12. Segurança: três armadilhas que matam
Confused deputy: server roda com credenciais privilegiadas, LLM convence ele a executar ação que o usuário nunca pediu. Defesa: server confirma intenção via elicitation/create antes de ação destrutiva. Host marca tools destructiveHint: true e pede confirmação humana.
Token passthrough: client manda token do usuário pro server, server reusa em chamada upstream. Defesa: validar aud claim. Token emitido pra MCP server X não pode ser aceito por API Y.
Indirect prompt injection: server expõe resource (ex: e-mail, issue do GitHub) cujo conteúdo tem instrução maliciosa pra LLM. LLM lê, segue. Defesa: sanitize/marcar fronteira de conteúdo externo, host deve isolar tool calls disparadas a partir de conteúdo lido versus instrução direta do usuário.
13. Versionamento e evolução
Versão do protocolo é string com data (YYYY-MM-DD). Negociada no initialize: client manda preferência, server escolhe entre as suportadas. Breaking changes geram nova versão. 2025-06-18 trouxe elicitation, structured tool output, OAuth 2.1 obrigatório, remoção de batch JSON-RPC (que existia em 2025-03-26). 2025-11-25 está em draft.
14. O fluxo completo de uma chamada de tool, em ordem
- Host inicializa client, abre transport (spawn process ou HTTP connection).
- Client manda
initialize, server responde com capabilities. - Client manda
notifications/initialized. - Client manda
tools/list. Server devolve catálogo. - Host injeta o catálogo no contexto do LLM (formato proprietário do host, mas geralmente vira definição de função pro modelo).
- LLM decide chamar tool, emite tool_call.
- Host valida permissão, opcionalmente pede confirmação ao usuário.
- Client manda
tools/callcomnameearguments. - Server executa. Se demora, emite
notifications/progress. Se precisa de input extra, chamaelicitation/createreverso. - Server responde com
contenteisError. - Host injeta o
contentde volta no contexto do LLM como observação. - LLM continua o loop (mais tool call, ou resposta final).
Esse é o protocolo inteiro. Toda complexidade adicional (multiplexação, retry, rate limit, cache) vive no host ou no server, fora da spec. MCP em si é deliberadamente fino. É justamente isso que faz ele ter chance de virar padrão de mercado: pequeno o bastante pra implementar em uma tarde, estruturado o bastante pra suportar tudo que agente sério precisa fazer.
Se você está construindo server pra produção, a checklist mínima é: implementar lifecycle correto (initialize + initialized antes de qualquer outra coisa), respeitar capability negotiation (não emitir notificação que o client não pediu), retornar erro estruturado em vez de exception, marcar tool com annotations apropriadas, suportar paginação em listas grandes, e nunca confiar em input do LLM sem validação de schema. O resto é detalhe de domínio.
