⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 6.4

🏗️ Construindo um MCP server próprio

Do zero, em Python (FastMCP) ou Node (@modelcontextprotocol/sdk), expor a API interna da sua empresa para o Hermes. Cobrimos quando vale criar o server, anatomia do protocolo, implementação, autenticação, deploy via Docker e publicação no ecossistema. É o caminho mais comum de adoção corporativa.

6
Tópicos
~60
Minutos
Avançado
Nível
Integração
Tipo
1

🎯 Quando criar MCP próprio — vs usar tool direta

Nem todo problema precisa de um MCP server. Para uma chamada HTTP pontual numa skill, requests.get() resolve. MCP brilha quando há reuso entre skills, portabilidade entre agentes (Hermes, Claude Desktop, Cursor) e superfície estável para a empresa expor.

CritérioMCP serverREST APIgRPC
Discovery de tools✅ nativa❌ manual⚠️ via reflection
Schema declarativo✅ JSON Schema⚠️ OpenAPI✅ protobuf
Suporte LLM agents✅ first-class❌ via wrapper❌ raro
Streaming✅ SSE / stdio⚠️ SSE/WS✅ nativo
Latência mín.~5ms (stdio)~20ms~2ms
EcossistemaCrescendo rápidoUniversalBackend interno

✓ Criar MCP server quando

  • 5+ skills vão consumir o mesmo backend
  • Quer expor para Claude Desktop / Cursor também
  • Domínio bem definido (1 server = 1 sistema)
  • Time terceiro vai consumir sem ler seu código

✗ Não criar — usar tool direta

  • 1 skill, 1 endpoint — overhead não vale
  • Mega-server "do everything" para 8 sistemas diferentes
  • Prova de conceito que você vai jogar fora em 2 semanas
  • Dado público que já tem MCP pronto na comunidade
2

📐 Anatomia de um MCP server — Protocol overview

MCP é um protocolo JSON-RPC 2.0 sobre stdio (para servers locais) ou HTTP+SSE (para remotos). O server expõe três tipos de capability: tools (funções chamáveis), resources (dados leves) e prompts (templates reutilizáveis). O cliente descobre tudo via initialize + list_* e invoca via call_*.

🧩 Três capabilities, três usos

  • tools — side-effects e queries com parâmetros: search_orders, create_ticket, send_email
  • resources — leitura idempotente por URI: customer://12345, file:///etc/config.yaml
  • prompts — templates parametrizáveis que viram mensagens do user: review_pr, onboard_qa

Handshake JSON-RPC típico (Hermes ↔ server):

→ {"jsonrpc":"2.0","id":1,"method":"initialize",
   "params":{"protocolVersion":"2024-11-05","capabilities":{}}}
← {"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"acme-crm","version":"0.3.1"},
   "capabilities":{"tools":{},"resources":{}}}}

→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← {"jsonrpc":"2.0","id":2,"result":{"tools":[
    {"name":"search_orders","description":"...","inputSchema":{...}},
    {"name":"get_user",      "description":"...","inputSchema":{...}},
    {"name":"send_email",    "description":"...","inputSchema":{...}}
  ]}}

→ {"jsonrpc":"2.0","id":3,"method":"tools/call",
   "params":{"name":"search_orders","arguments":{"customer_id":"42","status":"open"}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[
    {"type":"text","text":"3 orders found: #1041, #1056, #1098"}
  ]}}
3

🐍 Implementação Python — FastMCP step-by-step

FastMCP é o jeito mais rápido — um decorator transforma função em tool, type hints viram schema. Vamos expor 3 tools reais de um CRM fictício: buscar pedidos, obter usuário e mandar email transacional.

# servers/acme-crm/server.py
import os
from typing import Literal
from fastmcp import FastMCP
import httpx

mcp = FastMCP("acme-crm")

CRM_BASE  = os.environ["ACME_CRM_BASE_URL"]
CRM_TOKEN = os.environ["ACME_CRM_TOKEN"]

_client = httpx.Client(
    base_url=CRM_BASE,
    headers={"Authorization": f"Bearer {CRM_TOKEN}"},
    timeout=10.0,
)

@mcp.tool()
def search_orders(
    customer_id: str,
    status: Literal["open", "paid", "shipped", "all"] = "open",
    limit: int = 20,
) -> list[dict]:
    """Search orders for a given customer. Returns at most `limit` rows."""
    r = _client.get(
        "/v2/orders",
        params={"customer": customer_id, "status": status, "limit": limit},
    )
    r.raise_for_status()
    return r.json()["data"]

@mcp.tool()
def get_user(user_id: str) -> dict:
    """Fetch full user profile (name, email, tier, lifetime_value)."""
    r = _client.get(f"/v2/users/{user_id}")
    r.raise_for_status()
    return r.json()

@mcp.tool()
def send_email(
    to: str,
    subject: str,
    body_markdown: str,
    template: str | None = None,
) -> dict:
    """Send a transactional email via the CRM mailer. Idempotent by subject+to within 24h."""
    r = _client.post("/v2/email/send", json={
        "to": to, "subject": subject,
        "body": body_markdown, "template": template,
    })
    r.raise_for_status()
    return {"message_id": r.json()["id"], "queued": True}

if __name__ == "__main__":
    mcp.run()   # default: stdio transport

Testar local com o mcp-inspector antes mesmo de plugar no Hermes:

$ pip install fastmcp httpx
$ ACME_CRM_BASE_URL=https://crm.acme.internal \
  ACME_CRM_TOKEN=sk_test_xxx \
  npx @modelcontextprotocol/inspector python server.py

# Abre UI em http://localhost:5173 — lista tools, executa call, mostra JSON-RPC bruto

💡 Dica — mcp-inspector é seu melhor amigo

O @modelcontextprotocol/inspector roda local e mostra exatamente o que o agent vê: schema gerado, requests/responses, erros. Sempre debug aqui antes de plugar no Hermes — economiza horas.

4

🟢 Implementação Node — @modelcontextprotocol/sdk

Mesmo server, em TypeScript, usando o SDK oficial. Mais verboso que FastMCP — schemas declarados com Zod — mas idêntico em capabilities. Use Node quando o time já vive no ecossistema JS ou quando precisa rodar em runtime serverless (Cloudflare Workers, Vercel).

// servers/acme-crm/server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";

const CRM_BASE  = process.env.ACME_CRM_BASE_URL!;
const CRM_TOKEN = process.env.ACME_CRM_TOKEN!;

async function crm(path: string, init: RequestInit = {}) {
  const r = await fetch(`${CRM_BASE}${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${CRM_TOKEN}`, "content-type": "application/json", ...(init.headers || {}) },
  });
  if (!r.ok) throw new Error(`CRM ${r.status}: ${await r.text()}`);
  return r.json();
}

const tools = {
  search_orders: {
    description: "Search orders for a given customer.",
    schema: z.object({
      customer_id: z.string(),
      status: z.enum(["open", "paid", "shipped", "all"]).default("open"),
      limit: z.number().int().min(1).max(100).default(20),
    }),
    handler: async (a: any) => crm(`/v2/orders?customer=${a.customer_id}&status=${a.status}&limit=${a.limit}`),
  },
  get_user: {
    description: "Fetch full user profile.",
    schema: z.object({ user_id: z.string() }),
    handler: async (a: any) => crm(`/v2/users/${a.user_id}`),
  },
  send_email: {
    description: "Send transactional email. Idempotent by subject+to within 24h.",
    schema: z.object({
      to: z.string().email(),
      subject: z.string(),
      body_markdown: z.string(),
      template: z.string().optional(),
    }),
    handler: async (a: any) => crm("/v2/email/send", {
      method: "POST",
      body: JSON.stringify({ to: a.to, subject: a.subject, body: a.body_markdown, template: a.template }),
    }),
  },
};

const server = new Server({ name: "acme-crm", version: "0.3.1" }, { capabilities: { tools: {} } });

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: Object.entries(tools).map(([name, t]) => ({
    name, description: t.description, inputSchema: zodToJsonSchema(t.schema),
  })),
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const t = (tools as any)[req.params.name];
  if (!t) throw new Error(`unknown tool ${req.params.name}`);
  const args = t.schema.parse(req.params.arguments ?? {});
  const result = await t.handler(args);
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
});

await server.connect(new StdioServerTransport());

Plugando no Hermes (igual para Python ou Node):

# ~/.hermes/config.yaml
mcpServers:
  acme-crm:
    command: python
    args: ["/opt/mcp/acme-crm/server.py"]
    env:
      ACME_CRM_BASE_URL: https://crm.acme.internal
      ACME_CRM_TOKEN: ${secrets.ACME_CRM_TOKEN}   # resolve de ~/.hermes/secrets

  # ou versão Node
  acme-crm-node:
    command: node
    args: ["/opt/mcp/acme-crm/dist/server.js"]
    env:
      ACME_CRM_BASE_URL: https://crm.acme.internal
      ACME_CRM_TOKEN: ${secrets.ACME_CRM_TOKEN}
5

🔐 Auth e secrets — OAuth, JWT, service accounts

Server interno corporativo precisa autenticar — e jamais ter token em código. Três caminhos comuns: service account com token estático (mais simples, ok para CI), JWT por usuário (quando ações precisam de auditoria por pessoa) e OAuth 2.0 / OIDC (necessário para acessos a 3rd party em nome do user).

# Versão JWT por usuário — Hermes passa user_email no contexto
from fastmcp import FastMCP, Context
import jwt, time, os

mcp = FastMCP("acme-crm")
SIGNING_KEY = os.environ["ACME_JWT_PRIVATE_KEY"]   # PEM, carregado de secret

def mint_jwt(user_email: str) -> str:
    return jwt.encode(
        {
            "sub": user_email,
            "iss": "hermes-mcp",
            "aud": "acme-crm",
            "iat": int(time.time()),
            "exp": int(time.time()) + 300,   # 5 min
        },
        SIGNING_KEY,
        algorithm="RS256",
    )

@mcp.tool()
def get_user(ctx: Context, user_id: str) -> dict:
    """Fetch user — runs AS the calling agent's user, audited downstream."""
    token = mint_jwt(ctx.session.user_email)
    r = httpx.get(
        f"{CRM_BASE}/v2/users/{user_id}",
        headers={"Authorization": f"Bearer {token}"},
        timeout=10.0,
    )
    r.raise_for_status()
    return r.json()

⚠️ PII e blast radius

Tools que retornam PII (CPF, email pessoal, endereço) devem: (1) ter scope explícito no description para o roteador, (2) registrar quem acessou o quê — log estruturado com user_email + tool_name + args_redacted, (3) sempre validar permissão do user real, nunca confiar no agente. O agente é confused deputy potencial — você é o gatekeeper.

📊 MCP servers populares na comunidade (referência)

  • filesystem — leitura/escrita escopada (oficial Anthropic)
  • github — issues, PRs, código (oficial)
  • postgres / sqlite — query read-only por padrão
  • slack — mensagens, canais, search
  • puppeteer — browser headless
  • brave-search / tavily — web search

Lista completa e curada em modelcontextprotocol/servers. Antes de criar o seu, sempre cheque se já existe.

6

🚀 Deploy e publicação — Docker, npm, pip, share

MCP servers locais rodam como subprocess do agente — basta um binário ou script. Para distribuir em escala, três caminhos: Docker (server rodando isolado, qualquer transport), npm/pip (instalação trivial, server roda como subprocess), SaaS (server remoto via HTTP+SSE, vários clientes compartilham).

# servers/acme-crm/Dockerfile
FROM python:3.12-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY server.py .

# stdio mode: Hermes spawns this as subprocess via docker run -i
# SSE mode: expose 8080 e troca para mcp.run(transport="sse")
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["python", "server.py"]

Construção típica do server, do esqueleto ao deploy, em ~90 minutos:

1

Esqueleto (10 min)

pip install fastmcp + FastMCP("name") + mcp.run()

Server vazio já responde a initialize e tools/list (lista vazia). Inspector já abre.

2

Primeira tool real (20 min)

Endpoint mais usado do backend, type hints completos, docstring clara

Testar via inspector. Esta tool é o gabarito de qualidade — descrição, args, output.

3

Auth real (20 min)

Sai do token hardcoded; passa por env var / secret manager; smoke test

Aqui é onde 60% dos servers caseiros morrem — não pule.

4

Dockerfile + publish (20 min)

Build, push para registry interno, tag semver

Imagem < 100MB com python:3.12-slim. Para pip/npm: configure pyproject.toml ou package.json com bin entry.

5

Integrar Hermes + onboarding (20 min)

Atualizar config.yaml, escrever README com snippet copia-cola, anunciar no Discord interno

Total: ~90 min do nada ao server em produção interna. Replicável.

💡 Dica final

Publique no awesome-mcp-servers via PR quando o server for útil para a comunidade. Mesmo servers internos costumam ter um "core" genérico extraível — isso multiplica feedback, atrai contribuições e fortalece o ecossistema.

Resumo do Módulo

Quando MCP: 5+ consumidores, portabilidade entre agentes, domínio único — caso contrário use tool direta.
Anatomia: JSON-RPC 2.0 + stdio/SSE; capabilities = tools, resources, prompts.
Python FastMCP: decorator + type hints = schema automático; menor cerimônia.
Node SDK: mais verboso mas igual em poder; ótimo para serverless edge.
Auth: JWT por usuário para auditoria; secrets via Hermes secret manager; PII com log e gatekeeping no server.
Deploy: Docker para infra interna; pip/npm para distribuição; ~90 min do zero ao production-ready.

🎉 Curso completo!

Você terminou as 6 trilhas e 14 módulos do curso Hermes Agent — dos fundamentos do agente, passando pela arquitetura, instalação, manutenção, deploy e agora extensibilidade avançada. Você sabe construir, operar, customizar e estender o agente para qualquer cenário.

Próximos passos recomendados:

  • Publique uma skill ou MCP server no GitHub e divulgue no Discord
  • Abra uma issue ou PR para o repo oficial — bug, doc, exemplo
  • Compartilhe o curso com um colega e construam algo juntos