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.