MCP Deep Dive: construindo um servidor profissional
- ⬜🧠 Context Engineering: prompt caching, subagents e skills(Engenharia AI-Native)
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
Claude Cursor VSCode+Copilot
│ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐
│ GH │ │ GH │ │ GH │ ← cada host
│ int │ │ int │ │ int │ reimplementa
└─────┘ └─────┘ └─────┘ integrações
┌─────┐ ┌─────┐ ┌─────┐
│Slack│ │Slack│ │Slack│
└─────┘ └─────┘ └─────┘
Claude Cursor VSCode ChatGPT IDEs/CI
└──┬──────┬───────┬────────┬─────────┬───┘
│ (fala MCP: JSON-RPC stdio ou HTTP/SSE)
▼
┌────────────────────────────────────────────┐
│ MCP servers (escolhidos pelo user) │
│ github slack postgres linear fs │
└────────────────────────────────────────────┘
Anatomia: tools, resources, prompts
| Primitivo | Quem decide invocar | Quando entra no contexto |
|---|---|---|
| Tool | Modelo (com aprovação do host) | Quando o modelo decide chamar durante raciocínio |
| Resource | Host/usuário | Quando usuário anexa ou host auto-inclui (ex: arquivo aberto) |
| Prompt | Usuário (comando explícito) | Quando usuário invoca via slash ou menu |
// 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
| Aspecto | stdio | HTTP / SSE |
|---|---|---|
| Execução | Subprocess do host | Serviço remoto |
| Setup para usuário | Command + args no config | URL + token |
| Segurança | Herda processo/FS do host | Precisa de TLS + auth |
| Multi-tenancy | Não | Sim (por token/OAuth) |
| Streaming | JSON-RPC sobre stdin/stdout | SSE para eventos |
| Caso de uso | Dev local, desktop, CLI | SaaS, org-wide, cloud |
// ~/.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)
# 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.
// 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;Design de tool: regras que evitam dor
| Regra | Por que | Exemplo bom |
|---|---|---|
| Nome verbo_objeto explícito | Modelo decide por nome + description | search_customer_by_email, não getCustomer |
| Description diz quando NÃO usar | Evita tool errada em caso ambíguo | "Não use para busca fuzzy — use search_customer_fuzzy" |
| Schema estrito | Reduz tool calls inválidos | enum em status, required em ids |
| Output estruturado (JSON) | Reduz parsing errado pelo modelo | Retornar { id, name, ... } em vez de prosa |
| Idempotência quando possível | Permite retry seguro | upsert em vez de insert; read-only nunca muda estado |
| Errors úteis, não stack trace | Modelo precisa ler e decidir | "Email não encontrado. Tente search_customer_fuzzy." |
| Truncar saídas grandes | Tool result de 100k tokens destrói contexto | Paginaçã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.
// 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
Reuso entre Claude Desktop, Cursor, IDEs. Cada colega plugga a URL + seu token. Permissioning centralizado, audit unificado.
Alt: Tool calling custom em cada app — N integrações duplicadas e dessincronizadas
Alt: REST API tradicional — requer agent saber ler docs; MCP entrega schema + description prontos
📋 App próprio com um único agent LLM, sem intenção de compartilhar
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 stdio — vale só se você já for reusar as tools em outros hosts
Perguntas típicas
❓ Posso usar MCP server de um vendor sem ter Claude?
❓ Qual o risco de segurança de plugar MCP server de terceiro?
❓ MCP substitui OpenAPI/REST?
❓ Como testar um MCP server antes de publicar?
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.Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito