IA MCP LLM Agentes Automação

MCP Servers na prática: criando ferramentas customizadas para Claude e GPT-4

Como criar MCP Servers customizados para conectar LLMs aos sistemas da sua empresa. Exemplos reais em Node.js e Python com Tools, Resources e integração com Claude e GPT-4.

N
Neryx Digital Architects
22 de abril de 2026
15 min de leitura
250 profissionais leram
Categoria: IA & Automação Público: Desenvolvedores e tech leads implementando agentes de IA com acesso a sistemas internos Etapa: Decisão

O Model Context Protocol (MCP) resolve o problema mais prático de quem implementa agentes de IA: como dar ao modelo acesso controlado e padronizado aos sistemas da sua empresa, sem reescrever a integração para cada modelo diferente.

Esse artigo cobre a criação de MCP Servers customizados — do setup inicial até exemplos reais de Tools e Resources que você pode adaptar para o seu contexto.

O que é um MCP Server e por que criar o seu

Um MCP Server é um processo que expõe capacidades — tools (ações), resources (dados) e prompts (templates) — para qualquer cliente MCP compatível. Claude Desktop, Claude Code, e cada vez mais aplicações de terceiros são clientes MCP.

A vantagem de criar o seu próprio servidor: você escreve a lógica de integração uma vez e ela fica disponível para qualquer agente que suporte MCP. Muda o modelo? O servidor continua funcionando. Adiciona um novo cliente? Zero retrabalho.

Casos de uso reais onde MCP Servers customizados fazem diferença:

  • Acesso ao banco de dados interno com segurança — o LLM executa queries via tool, nunca recebe credenciais
  • Integração com ERP/CRM — consulta pedidos, clientes, estoque em linguagem natural
  • Execução de workflows internos — aprovar despesas, criar tickets, disparar deploys
  • Acesso a documentação interna versionada — base de conhecimento atualizada em tempo real

Setup: criando um MCP Server em Node.js

O SDK oficial do MCP está disponível em Node.js e Python. Para servidores que precisam de TypeScript e integração com o ecossistema Node, a versão TypeScript é a mais madura.

mkdir meu-mcp-server && cd meu-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true
  }
}

Exemplo 1: Tool para consultar banco de dados

O padrão mais útil é expor queries controladas como tools. O LLM não acessa o banco diretamente — ele chama a tool com parâmetros validados e recebe o resultado.

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Pool } from "pg";

const db = new Pool({
  connectionString: process.env.DATABASE_URL,
});

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

// Tool: buscar pedidos por cliente
server.tool(
  "buscar_pedidos_cliente",
  "Busca os pedidos de um cliente pelo e-mail ou ID. Retorna status, valor e data.",
  {
    identificador: z.string().describe("E-mail ou ID do cliente"),
    limite: z.number().min(1).max(50).default(10).describe("Número máximo de pedidos"),
    status: z
      .enum(["todos", "pendente", "aprovado", "enviado", "entregue", "cancelado"])
      .default("todos")
      .describe("Filtrar por status"),
  },
  async ({ identificador, limite, status }) => {
    const isEmail = identificador.includes("@");

    const query = `
      SELECT 
        p.id,
        p.numero,
        p.status,
        p.valor_total,
        p.criado_em,
        c.nome as cliente_nome,
        c.email as cliente_email
      FROM pedidos p
      JOIN clientes c ON c.id = p.cliente_id
      WHERE 
        (${isEmail ? "c.email = $1" : "c.id::text = $1"})
        ${status !== "todos" ? "AND p.status = $2" : ""}
      ORDER BY p.criado_em DESC
      LIMIT ${status !== "todos" ? "$3" : "$2"}
    `;

    const params =
      status !== "todos"
        ? [identificador, status, limite]
        : [identificador, limite];

    const result = await db.query(query, params);

    if (result.rows.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `Nenhum pedido encontrado para: ${identificador}`,
          },
        ],
      };
    }

    const pedidos = result.rows
      .map(
        (p) =>
          `Pedido #${p.numero} | Status: ${p.status} | Valor: R$${p.valor_total} | Data: ${new Date(p.criado_em).toLocaleDateString("pt-BR")}`
      )
      .join("\n");

    return {
      content: [
        {
          type: "text",
          text: `Cliente: ${result.rows[0].cliente_nome} (${result.rows[0].cliente_email})\n\n${pedidos}`,
        },
      ],
    };
  }
);

// Tool: atualizar status de pedido
server.tool(
  "atualizar_status_pedido",
  "Atualiza o status de um pedido. Requer confirmação explícita antes de executar.",
  {
    numero_pedido: z.string().describe("Número do pedido (ex: PED-2026-001)"),
    novo_status: z
      .enum(["aprovado", "enviado", "entregue", "cancelado"])
      .describe("Novo status do pedido"),
    motivo: z.string().optional().describe("Motivo da mudança de status"),
  },
  async ({ numero_pedido, novo_status, motivo }) => {
    const result = await db.query(
      `UPDATE pedidos 
       SET status = $1, 
           motivo_status = $2,
           atualizado_em = NOW()
       WHERE numero = $3
       RETURNING id, numero, status`,
      [novo_status, motivo ?? null, numero_pedido]
    );

    if (result.rows.length === 0) {
      return {
        content: [{ type: "text", text: `Pedido ${numero_pedido} não encontrado.` }],
        isError: true,
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `✅ Pedido ${numero_pedido} atualizado para: ${novo_status}${motivo ? ` (${motivo})` : ""}`,
        },
      ],
    };
  }
);

// Inicia o servidor via stdio (para Claude Desktop / Claude Code)
const transport = new StdioServerTransport();
await server.connect(transport);

Exemplo 2: Resources para documentação interna

Resources expõem conteúdo estático ou dinâmico que o cliente pode incluir no contexto. Ideal para documentação, runbooks e bases de conhecimento.

// Adicionar ao server.ts — Resources de documentação
server.resource(
  "runbooks",
  "Runbooks operacionais da empresa",
  async (uri) => {
    // URI pattern: runbook://slug-do-runbook
    const slug = uri.pathname.replace(/^\//, "");

    const result = await db.query(
      "SELECT titulo, conteudo, atualizado_em FROM runbooks WHERE slug = $1 AND ativo = true",
      [slug]
    );

    if (result.rows.length === 0) {
      throw new Error(`Runbook '${slug}' não encontrado`);
    }

    const runbook = result.rows[0];

    return {
      contents: [
        {
          uri: uri.toString(),
          mimeType: "text/markdown",
          text: `# ${runbook.titulo}\n\nAtualizado em: ${new Date(runbook.atualizado_em).toLocaleDateString("pt-BR")}\n\n${runbook.conteudo}`,
        },
      ],
    };
  }
);

// Listar todos os runbooks disponíveis
server.resource(
  "runbooks://index",
  "Lista de todos os runbooks disponíveis",
  async () => {
    const result = await db.query(
      "SELECT slug, titulo, descricao FROM runbooks WHERE ativo = true ORDER BY titulo"
    );

    const lista = result.rows
      .map((r) => `- [${r.titulo}](runbook://${r.slug}): ${r.descricao}`)
      .join("\n");

    return {
      contents: [
        {
          uri: "runbooks://index",
          mimeType: "text/markdown",
          text: `# Runbooks disponíveis\n\n${lista}`,
        },
      ],
    };
  }
);

Exemplo 3: MCP Server em Python para integração com APIs externas

# server.py
import asyncio
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

app = Server("integracao-erp")

ERP_BASE_URL = "https://erp.empresa.internal/api/v2"
ERP_TOKEN = os.environ["ERP_API_TOKEN"]

@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="consultar_estoque",
            description="Consulta o estoque atual de um produto pelo SKU ou nome.",
            inputSchema={
                "type": "object",
                "properties": {
                    "produto": {
                        "type": "string",
                        "description": "SKU ou nome parcial do produto"
                    },
                    "deposito": {
                        "type": "string",
                        "description": "Código do depósito (opcional, padrão: todos)",
                        "default": "all"
                    }
                },
                "required": ["produto"]
            }
        ),
        types.Tool(
            name="criar_ordem_compra",
            description="Cria uma ordem de compra no ERP para reposição de estoque.",
            inputSchema={
                "type": "object",
                "properties": {
                    "sku": {"type": "string", "description": "SKU do produto"},
                    "quantidade": {"type": "integer", "minimum": 1},
                    "fornecedor_id": {"type": "string"},
                    "urgente": {"type": "boolean", "default": False}
                },
                "required": ["sku", "quantidade", "fornecedor_id"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    async with httpx.AsyncClient() as client:
        headers = {"Authorization": f"Bearer {ERP_TOKEN}"}

        if name == "consultar_estoque":
            resp = await client.get(
                f"{ERP_BASE_URL}/estoque",
                params={"q": arguments["produto"], "deposito": arguments.get("deposito", "all")},
                headers=headers
            )
            resp.raise_for_status()
            data = resp.json()

            if not data["items"]:
                return [types.TextContent(type="text", text=f"Produto '{arguments['produto']}' não encontrado no estoque.")]

            linhas = [
                f"SKU: {item['sku']} | {item['nome']} | Estoque: {item['quantidade']} unidades | Depósito: {item['deposito']}"
                for item in data["items"]
            ]
            return [types.TextContent(type="text", text="\n".join(linhas))]

        elif name == "criar_ordem_compra":
            resp = await client.post(
                f"{ERP_BASE_URL}/ordens-compra",
                json=arguments,
                headers=headers
            )
            resp.raise_for_status()
            oc = resp.json()
            return [types.TextContent(
                type="text",
                text=f"✅ Ordem de compra criada: #{oc['numero']} | {oc['sku']} x{oc['quantidade']} | Previsão: {oc['previsao_entrega']}"
            )]

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

asyncio.run(main())

Configurando no Claude Desktop

// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
{
  "mcpServers": {
    "empresa-db": {
      "command": "node",
      "args": ["/caminho/para/meu-mcp-server/dist/server.js"],
      "env": {
        "DATABASE_URL": "postgresql://user:pass@host:5432/db"
      }
    },
    "erp-integration": {
      "command": "python",
      "args": ["/caminho/para/erp-server/server.py"],
      "env": {
        "ERP_API_TOKEN": "seu_token_aqui"
      }
    }
  }
}

Segurança: o que não ignorar

MCP Servers têm acesso direto a sistemas internos. Algumas práticas obrigatórias:

  • Princípio do menor privilégio: o servidor de banco de dados deve ter um usuário de banco somente-leitura para tools de consulta. Apenas tools de mutação usam usuário com escrita.
  • Validação rigorosa de inputs: o Zod (TypeScript) e Pydantic (Python) são seus aliados. Nunca interpole inputs do LLM em queries SQL diretamente.
  • Rate limiting por tool: tools de mutação (update, delete, criar ordem) devem ter limite de chamadas por sessão.
  • Logging de auditoria: registre cada chamada de tool com timestamp, parâmetros e resultado. Você vai querer isso quando algo der errado.
  • Não exponha credenciais no schema das tools: o modelo nunca deve ver tokens ou senhas — eles ficam no ambiente do processo do servidor.

Testando o servidor localmente

# Usando o MCP Inspector (ferramenta oficial de debug)
npx @modelcontextprotocol/inspector node dist/server.js

# Ou diretamente via stdio
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/server.js

MCP é o padrão que está consolidando como agentes de IA se conectam a sistemas reais. Criar seu servidor customizado é a forma mais eficiente de fazer isso com controle total sobre segurança e lógica de negócio.

Se você quer implementar agentes com acesso aos sistemas da sua empresa, a Neryx tem experiência em arquitetura de agentes com MCP, RAG e integração com sistemas legados. Fale com a gente.

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.