🧠FFVAcademy
🔌

MCP Deep Dive: construindo um servidor profissional

19 min de leitura·+90 XP
Pré-requisitos (0/1)0%

Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.

MCP (Model Context Protocol) é o padrão que transformou "plugar ferramentas em agent" de projeto custom por integração em commodity reutilizável. Este módulo mostra a arquitetura, os três primitivos (tools, resources, prompts), transports (stdio e HTTP/SSE), auth e um MCP server profissional em TypeScript e Python — incluindo rate limit, logging e permissioning.

Por que MCP virou padrão

🗺️ Antes do MCP: N × M integrações

     Claude       Cursor      VSCode+Copilot
       │            │               │
    ┌──┴──┐      ┌──┴──┐         ┌──┴──┐
    │ GH  │      │ GH  │         │ GH  │   ← cada host
    │ int │      │ int │         │ int │     reimplementa
    └─────┘      └─────┘         └─────┘     integrações
    ┌─────┐      ┌─────┐         ┌─────┐
    │Slack│      │Slack│         │Slack│
    └─────┘      └─────┘         └─────┘
🗺️ Com MCP: um servidor para muitos hosts

     Claude   Cursor   VSCode   ChatGPT   IDEs/CI
       └──┬──────┬───────┬────────┬─────────┬───┘
          │ (fala MCP: JSON-RPC stdio ou HTTP/SSE)
          ▼
   ┌────────────────────────────────────────────┐
   │        MCP servers (escolhidos pelo user)  │
   │  github   slack   postgres   linear   fs   │
   └────────────────────────────────────────────┘
💡
MCP criou um marketplace real: servidores oficiais (github, slack, postgres, fetch, filesystem), community (linear, sentry, figma), corporativos (acesso privado a sistemas internos). Em 2026, qualquer dev-host sério suporta MCP.

Anatomia: tools, resources, prompts

PrimitivoQuem decide invocarQuando entra no contexto
ToolModelo (com aprovação do host)Quando o modelo decide chamar durante raciocínio
ResourceHost/usuárioQuando usuário anexa ou host auto-inclui (ex: arquivo aberto)
PromptUsuário (comando explícito)Quando usuário invoca via slash ou menu
typescript
// MCP server em TypeScript — SDK oficial @modelcontextprotocol/sdk
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "ffv-db-server",
  version: "1.0.0",
});

// --- TOOL: ação com efeito controlado pelo modelo
server.registerTool(
  "search_customer_by_email",
  {
    description:
      "Busca dados públicos do cliente por email EXATO. " +
      "NÃO use para busca aproximada — use search_customer_fuzzy para isso. " +
      "Retorna { id, name, plan, signup_at } ou null.",
    inputSchema: {
      email: z.string().email().describe("Email exato do cliente"),
    },
  },
  async ({ email }) => {
    const row = await db.oneOrNone(
      "SELECT id, name, plan, signup_at FROM customers WHERE email = $1",
      [email.toLowerCase()],
    );
    return {
      content: [{ type: "text", text: JSON.stringify(row) }],
    };
  },
);

// --- RESOURCE: dado legível escolhido pelo host/usuário
server.registerResource(
  "customer-doc",
  new ResourceTemplate("customer://{id}/profile", { list: undefined }),
  {
    title: "Perfil do cliente",
    description: "Documento estruturado com histórico resumido.",
  },
  async (uri, { id }) => ({
    contents: [{
      uri: uri.href,
      text: await getCustomerProfileMarkdown(id),
      mimeType: "text/markdown",
    }],
  }),
);

// --- PROMPT: template invocado pelo usuário (slash command)
server.registerPrompt(
  "summarize-customer",
  {
    title: "Resumir cliente",
    description: "Gera resumo executivo do cliente",
    argsSchema: { id: z.string() },
  },
  ({ id }) => ({
    messages: [{
      role: "user",
      content: {
        type: "text",
        text:
          `Resuma em 200 palavras o cliente ${id}: plano, uso, tickets, sinal de churn.`,
      },
    }],
  }),
);

await server.connect(new StdioServerTransport());

Transports: stdio vs HTTP/SSE

AspectostdioHTTP / SSE
ExecuçãoSubprocess do hostServiço remoto
Setup para usuárioCommand + args no configURL + token
SegurançaHerda processo/FS do hostPrecisa de TLS + auth
Multi-tenancyNãoSim (por token/OAuth)
StreamingJSON-RPC sobre stdin/stdoutSSE para eventos
Caso de usoDev local, desktop, CLISaaS, org-wide, cloud
json
// ~/.claude/mcp.json — configuração de MCP servers no host
{
  "mcpServers": {
    "ffv-db": {
      "command": "node",
      "args": ["/Users/ferf/ffv-mcp/dist/server.js"],
      "env": { "DATABASE_URL": "postgres://..." }
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
    },
    "acme-saas": {
      "url": "https://mcp.acme.com/v1",
      "headers": { "Authorization": "Bearer $ACME_TOKEN" }
    }
  }
}

MCP server em Python (stdio)

python
# pip install mcp
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool
import psycopg, os

server = Server("ffv-db-server")
POOL = psycopg.connect(os.environ["DATABASE_URL"])

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="search_customer_by_email",
            description=(
                "Busca cliente por email EXATO. Não usa match aproximado. "
                "Retorna JSON {id, name, plan, signup_at} ou null."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "email": {"type": "string", "format": "email"},
                },
                "required": ["email"],
            },
        ),
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "search_customer_by_email":
        email = arguments["email"].lower()
        with POOL.cursor() as cur:
            cur.execute(
                "SELECT id, name, plan, signup_at FROM customers WHERE email=%s",
                (email,),
            )
            row = cur.fetchone()
        payload = None if not row else {
            "id": row[0], "name": row[1], "plan": row[2], "signup_at": str(row[3]),
        }
        import json
        return [TextContent(type="text", text=json.dumps(payload))]
    raise ValueError(f"Tool desconhecida: {name}")

async def main() -> None:
    async with stdio_server() as (read, write):
        await server.run(read, write, server.create_initialization_options())

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

MCP server em HTTP com auth (produção)

Para MCP servers expostos via rede, OAuth 2.1 + PKCE é o padrão. O host descobre o servidor, troca token por scope, e cada chamada HTTP carrega o Bearer. Rate limit e audit log viram obrigatórios.

typescript
// Server HTTP com Hono + MCP SDK + OAuth 2.1
import { Hono } from "hono";
import { bearerAuth } from "hono/bearer-auth";
import { HttpServerTransport } from "@modelcontextprotocol/sdk/server/http.js";

const app = new Hono();

// --- Middleware: auth + rate limit
app.use("/mcp/*", bearerAuth({
  verifyToken: async (token) => {
    const user = await validateJWT(token);
    if (!user) return false;
    c.set("user", user);                // disponível para tools
    return true;
  },
}));
app.use("/mcp/*", rateLimiter({ max: 60, windowMs: 60_000 }));   // 60/min por token

// --- Endpoint MCP
app.post("/mcp/v1", async (c) => {
  const user = c.get("user");
  const server = buildServerForUser(user);      // escopo por user
  const transport = new HttpServerTransport({ request: c.req.raw });
  await server.connect(transport);
  return transport.response;
});

// --- Endpoint OAuth metadata para discovery
app.get("/.well-known/oauth-authorization-server", (c) =>
  c.json({
    issuer: "https://mcp.ffv.com",
    authorization_endpoint: "https://mcp.ffv.com/oauth/authorize",
    token_endpoint: "https://mcp.ffv.com/oauth/token",
    grant_types_supported: ["authorization_code", "refresh_token"],
    code_challenge_methods_supported: ["S256"],
  }),
);

export default app;
⚠️
MCP HTTP recebeu em 2025 a especificação de auth OAuth 2.1 com PKCE. Qualquer servidor público deve seguir: evita token-leak, suporta revogação, e permite org-wide SSO. Evite schemes custom por questão de interoperabilidade com hosts diferentes.

Design de tool: regras que evitam dor

RegraPor queExemplo bom
Nome verbo_objeto explícitoModelo decide por nome + descriptionsearch_customer_by_email, não getCustomer
Description diz quando NÃO usarEvita tool errada em caso ambíguo"Não use para busca fuzzy — use search_customer_fuzzy"
Schema estritoReduz tool calls inválidosenum em status, required em ids
Output estruturado (JSON)Reduz parsing errado pelo modeloRetornar { id, name, ... } em vez de prosa
Idempotência quando possívelPermite retry seguroupsert em vez de insert; read-only nunca muda estado
Errors úteis, não stack traceModelo precisa ler e decidir"Email não encontrado. Tente search_customer_fuzzy."
Truncar saídas grandesTool result de 100k tokens destrói contextoPaginação + total_count, ou preview + more=true

Logging, observability e audit

MCP server em produção precisa de audit trail — cada chamada, quem chamou (token/user), argumentos, duração e resultado. Para debug e compliance.

typescript
// Middleware de log estruturado para cada tool call
server.setRequestMiddleware(async (req, next) => {
  const start = Date.now();
  const user = req.auth?.user;
  try {
    const res = await next();
    logger.info({
      event: "mcp.tool_call",
      tool: req.params?.name,
      user_id: user?.id,
      duration_ms: Date.now() - start,
      status: "ok",
    });
    return res;
  } catch (err) {
    logger.error({
      event: "mcp.tool_call",
      tool: req.params?.name,
      user_id: user?.id,
      duration_ms: Date.now() - start,
      status: "error",
      error: String(err),
    });
    throw err;
  }
});

Quando criar MCP server custom (vs usar SDK direto)

📋 Equipe interna quer expor sistema interno (CRM, DB) a agents de vários colegas

MCP server (HTTP com OAuth)

Reuso entre Claude Desktop, Cursor, IDEs. Cada colega plugga a URL + seu token. Permissioning centralizado, audit unificado.

Alt: Tool calling custom em cada appN integrações duplicadas e dessincronizadas

Alt: REST API tradicionalrequer agent saber ler docs; MCP entrega schema + description prontos

📋 App próprio com um único agent LLM, sem intenção de compartilhar

SDK nativo (Anthropic/OpenAI tool_use)

MCP adiciona complexidade que não se paga em caso single-agent single-app. SDK nativo é direto: define tool, chama LLM, roda função.

Alt: MCP local stdiovale só se você já for reusar as tools em outros hosts

Perguntas típicas

Posso usar MCP server de um vendor sem ter Claude?

Sim. MCP é aberto — OpenAI, Google e outros adotaram. Hosts como Cursor, Zed, Continue e VSCode extensions suportam MCP. A spec e SDKs (TS, Python, Go, Rust) estão em modelcontextprotocol.io.

Qual o risco de segurança de plugar MCP server de terceiro?

Sério. Tool call pode executar código, tocar DB, enviar mensagem. Best practice: (1) aprovação explícita por tool call, (2) scopes no token OAuth, (3) rodar servers não-confiáveis em sandbox (container, user separado), (4) audit log de tudo. Nunca dê token write sem necessidade.

MCP substitui OpenAPI/REST?

Não, complementa. OpenAPI é contrato HTTP genérico; MCP é contrato otimizado para agents (tools + resources + prompts com descriptions e permissions). Você pode ter um MCP server que internamente consome sua API REST — é comum.

Como testar um MCP server antes de publicar?

Use npx @modelcontextprotocol/inspector — UI que conecta ao seu servidor (stdio ou HTTP), lista tools/resources/prompts, permite invocar manualmente e inspecionar respostas. É o "Postman" do MCP.
Take-aways. MCP padronizou integração de tools/resources/prompts entre hosts. Tools = modelo decide; Resources = usuário escolhe; Prompts = usuário invoca. stdio para dev local; HTTP+OAuth para SaaS. Tool design (nome, description, schema, idempotência) é onde a qualidade se faz. Audit, rate limit e auth não são opcionais em server público. Próximo: colocar LLM API em produção com streaming, structured output, batch e retry.
🧩

Quiz rápido

4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo